Skip to content

Commit cb90aee

Browse files
committed
fixup! Implement Lambda function versions (#5927)
1 parent ce68406 commit cb90aee

File tree

4 files changed

+45
-80
lines changed

4 files changed

+45
-80
lines changed

scripts/sell_unused_slots.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def _list_contribution_lambda_functions(cls) -> list[Lambda]:
7171
"""
7272
return [
7373
lambda_
74-
for lambda_ in Lambdas().list_lambdas(deployment='ALL')
74+
for lambda_ in Lambdas().list_lambdas()
7575
if lambda_.is_contribution_lambda
7676
]
7777

src/azul/lambdas.py

Lines changed: 14 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
)
1414

1515
from azul import (
16-
JSON,
1716
R,
1817
cache,
1918
config,
@@ -38,7 +37,6 @@ class Lambda:
3837
name: str
3938
role: str
4039
slot_location: Optional[str]
41-
version: str
4240

4341
@property
4442
def is_contribution_lambda(self) -> bool:
@@ -84,15 +82,13 @@ def has_notification_queue(handler) -> bool:
8482
def from_response(cls, response: 'FunctionConfigurationTypeDef') -> Self:
8583
name = response['FunctionName']
8684
role = response['Role']
87-
version = response['Version']
8885
try:
8986
slot_location = response['Environment']['Variables']['AZUL_TDR_SOURCE_LOCATION']
9087
except KeyError:
9188
slot_location = None
9289
return cls(name=name,
9390
role=role,
94-
slot_location=slot_location,
95-
version=version)
91+
slot_location=slot_location)
9692

9793
def __attrs_post_init__(self):
9894
if self.slot_location is None:
@@ -109,65 +105,27 @@ class Lambdas:
109105
def _lambda(self):
110106
return aws.lambda_
111107

112-
def list_lambdas(self,
113-
deployment: str
114-
) -> list[Lambda]:
115-
"""
116-
Return a list of AWS Lambda functions. Only the largest numbered version
117-
of each function will be included in the list.
118-
119-
:param deployment: Limit output to the specified deployment stage. If
120-
'ALL', functions from all deployments will be
121-
returned.
122-
"""
123-
paginator = self._lambda.get_paginator('list_functions')
124-
lambda_prefixes = None if deployment == 'ALL' else [
125-
config.qualified_resource_name(lambda_name, stage=deployment)
126-
for lambda_name in config.lambda_names()
127-
]
128-
functions: dict[str, JSON] = dict()
129-
for response in paginator.paginate(FunctionVersion='ALL'):
130-
for function in response['Functions']:
131-
version = function['Version']
132-
version = None if version == '$LATEST' else int(version)
133-
if version and (lambda_prefixes is None or any(
134-
function['FunctionName'].startswith(prefix)
135-
for prefix in lambda_prefixes
136-
)):
137-
name = function['FunctionName']
138-
previous_function = functions.get(name)
139-
if previous_function is None or version > int(previous_function['Version']):
140-
functions[name] = function
108+
def list_lambdas(self) -> list[Lambda]:
141109
return [
142110
Lambda.from_response(function)
143-
for function in functions.values()
111+
for response in self._lambda.get_paginator('list_functions').paginate()
112+
for function in response['Functions']
144113
]
145114

146-
def get_function(self, function_name: str) -> JSON:
147-
"""
148-
Return the Lambda client `get_function()` response for the largest
149-
numbered version of the Lambda function.
150-
"""
151-
paginator = self._lambda.get_paginator('list_versions_by_function')
152-
params = {'FunctionName': function_name}
153-
version = max([
154-
int(function['Version'])
155-
for response in paginator.paginate(**params)
156-
for function in response['Versions']
157-
if function['Version'] != '$LATEST'
158-
])
159-
return self._lambda.get_function(FunctionName=function_name,
160-
Qualifier=str(version))
161-
162115
def manage_lambdas(self, enabled: bool):
163-
for function in self.list_lambdas(deployment=config.deployment_stage):
164-
self.manage_lambda(function.name, enabled)
116+
lambda_prefixes = [
117+
config.qualified_resource_name(lambda_infix)
118+
for lambda_infix in config.lambda_names()
119+
]
120+
assert all(lambda_prefixes)
121+
for lambda_ in self.list_lambdas():
122+
if any(lambda_.name.startswith(prefix) for prefix in lambda_prefixes):
123+
self.manage_lambda(lambda_.name, enabled)
165124

166125
def manage_lambda(self, lambda_name: str, enable: bool):
167-
lambda_settings = self.get_function(function_name=lambda_name)
126+
lambda_settings = self._lambda.get_function(FunctionName=lambda_name)
168127
lambda_arn = lambda_settings['Configuration']['FunctionArn']
169128
# Lambda does not support adding tags to function aliases or versions
170-
lambda_arn, _, _ = lambda_arn.rpartition(':')
171129
lambda_tags = self._lambda.list_tags(Resource=lambda_arn)['Tags']
172130
lambda_name = lambda_settings['Configuration']['FunctionName']
173131
if enable:
@@ -211,7 +169,7 @@ def reset_lambda_roles(self):
211169
client = self._lambda
212170
lambda_names = set(config.lambda_names())
213171

214-
for lambda_ in self.list_lambdas(deployment='ALL'):
172+
for lambda_ in self.list_lambdas():
215173
for lambda_name in lambda_names:
216174
if lambda_.name.startswith(config.qualified_resource_name(lambda_name)):
217175
other_lambda_name = one(lambda_names - {lambda_name})

src/azul/queues.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -504,11 +504,11 @@ def _wait_for_queue_empty(self, queue: 'Queue'):
504504

505505
def _manage_sqs_push(self,
506506
function_name: str,
507-
function_version: str,
507+
version_or_alias: str,
508508
queue: 'Queue',
509509
enable: bool):
510510
lambda_ = aws.lambda_
511-
partial_arn = f'{function_name}:{function_version}'
511+
partial_arn = f'{function_name}:{version_or_alias}'
512512
response = lambda_.list_event_source_mappings(FunctionName=partial_arn,
513513
EventSourceArn=queue.attributes['QueueArn'])
514514
mapping_uuid = one(response['EventSourceMappings'])['UUID']
@@ -559,11 +559,6 @@ def manage_lambdas(self, queues: Mapping[str, 'Queue'], enable: bool):
559559
Enable or disable the readers and writers of the given queues.
560560
"""
561561
functions_by_queue = self.functions_by_queue()
562-
versions_by_function = {
563-
f.name: f.version
564-
for f in self._lambdas.list_lambdas(deployment=config.deployment_stage)
565-
}
566-
assert set(functions_by_queue.values()) <= set(versions_by_function)
567562

568563
with ThreadPoolExecutor(max_workers=len(queues)) as tpe:
569564
futures = []
@@ -577,11 +572,10 @@ def submit(f, *args, **kwargs):
577572
except KeyError:
578573
assert queue_name in config.fail_queue_names
579574
else:
580-
version = versions_by_function[function]
581575
if queue_name == config.notifications_queue.name:
582576
# Prevent new notifications from being added
583577
submit(self._manage_lambda, config.indexer_name, enable)
584-
submit(self._manage_sqs_push, function, version, queue, enable)
578+
submit(self._manage_sqs_push, function, 'active', queue, enable)
585579
self._handle_futures(futures)
586580
futures = [tpe.submit(self._wait_for_queue_idle, queue) for queue in queues.values()]
587581
self._handle_futures(futures)

src/azul/terraform.py

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -756,8 +756,11 @@ def tf_config(self, app_name):
756756
'${aws_vpc_endpoint.%s.id}' % app_name
757757
]
758758

759+
assert 'aws_lambda_alias' not in resources
760+
resources['aws_lambda_alias'] = dict()
761+
759762
functions = json_item_dicts(json_dict(resources['aws_lambda_function']))
760-
for _, resource in functions:
763+
for resource_name, resource in functions:
761764
assert 'layers' not in resource
762765
resource['layers'] = ['${aws_lambda_layer_version.dependencies.arn}']
763766
# Publishing a new Lambda function version each time lets us perform
@@ -781,6 +784,15 @@ def tf_config(self, app_name):
781784
resource['source_code_hash'] = '${filebase64sha256("%s")}' % package_zip
782785
resource['filename'] = package_zip
783786

787+
# Inject a Lambda function alias resource that will point to the
788+
# Lambda function version published when this and the
789+
# aws_lambda_function resources are deployed.
790+
json_dict(resources['aws_lambda_alias'])[resource_name] = {
791+
'name': 'active',
792+
'function_name': '${aws_lambda_function.%s.arn}' % resource_name,
793+
'function_version': '${aws_lambda_function.%s.version}' % resource_name
794+
}
795+
784796
assert 'aws_cloudwatch_log_group' not in resources
785797
functions = json_item_dicts(resources['aws_lambda_function'])
786798
resources['aws_cloudwatch_log_group'] = {
@@ -927,36 +939,37 @@ def tf_config(self, app_name):
927939
resource['event_source_arn'] = f'${{aws_sqs_queue.{sqs_name}.arn}}'
928940

929941
# Replace the references to unqualified Lambda function ARNs emitted by
930-
# Chalice with qualified ARNs. Note: `aws_lambda_permission` resources
931-
# doesn't support the qualified ARN syntax, instead it has a `qualifier`
932-
# argument.
942+
# Chalice with references to the Lambda function alias.
933943
#
934-
locals[app_name] = re.sub(r'(aws_lambda_function\.[^\.\s]+)\.invoke_arn',
935-
r'\1.qualified_invoke_arn',
944+
locals[app_name] = re.sub(r'\$\{aws_lambda_function\.([^\.\s]+)\.invoke_arn\}',
945+
r'${aws_lambda_alias.\1.invoke_arn}',
936946
json_str(locals[app_name]))
937947
if app_name == 'indexer':
938948
resource_arguments = [
939949
('aws_cloudwatch_event_target', 'arn'),
950+
('aws_lambda_permission', 'function_name'),
940951
('aws_lambda_event_source_mapping', 'function_name'),
941952
]
942953
elif app_name == 'service':
943954
resource_arguments = [
944955
('aws_cloudwatch_event_target', 'arn'),
956+
('aws_lambda_permission', 'function_name'),
945957
]
946958
else:
947959
assert False, app_name
948960
for resource_type, argument in resource_arguments:
949961
for _, resource in json_item_dicts(resources[resource_type]):
950-
value = json_str(resource[argument])
951-
assert value.endswith('.arn}'), resource
952-
resource[argument] = value.replace('.arn', '.qualified_arn')
962+
resource[argument] = re.sub(r'\$\{aws_lambda_function\.([^\.\s]+)\.arn\}',
963+
r'${aws_lambda_alias.\1.arn}',
964+
json_str(resource[argument]))
953965

954-
# Add a qualifier argument to `aws_lambda_permission` resources
966+
# Ensure that the Lambda permissions for the previous Lambda aliases
967+
# aren't deleted until after the permissions for the new Lambda aliases
968+
# have been created.
955969
#
956-
for _, resource in json_item_dicts(resources['aws_lambda_permission']):
957-
assert 'qualifier' not in resource, resource
958-
lambda_arn = resource['function_name']
959-
resource['qualifier'] = lambda_arn.replace('.arn', '.version')
970+
for name, resource in json_item_dicts(resources['aws_lambda_permission']):
971+
assert 'lifecycle' not in resource, resource
972+
resource['lifecycle'] = {'create_before_destroy': True}
960973

961974
return {
962975
'resource': resources,

0 commit comments

Comments
 (0)