Skip to content

Commit d275c23

Browse files
Add script for releasing the lambda layer.
Merge pull request #7 from mirelap-amazon/main
2 parents a288355 + f18824e commit d275c23

File tree

3 files changed

+204
-1
lines changed

3 files changed

+204
-1
lines changed

DEVELOPMENT.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,15 @@
2929

3030
## How to release the Lambda Layer.
3131

32-
Check internal instructions.
32+
The layer is used for profiling AWS lambda functions. The layer contains only our module source code as `boto3` is already available in a lambda environment.
33+
34+
Check internal instructions for what credentials to use.
35+
36+
1. Checkout the last version of the `main` branch locally after you did the release to PyPI.
37+
38+
2. Run the following command in this package to publish a new version for the layer that will be available to the public immediately.
39+
```
40+
python release_layer.py
41+
```
42+
43+
3. Update the documentation with the ARN that was printed.

codeguru_profiler_lambda_exec

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/bin/bash
2+
3+
# This bash script is used to bootstrap the lambda layer. It should be included in the layer zip archive and can then
4+
# be used by a user by adding this environment variable to their lambda configuration:
5+
# AWS_LAMBDA_EXEC_WRAPPER = /opt/codeguru_profiler_lambda_exec
6+
7+
# This replaces the environment variables so that a codeguru profiler function is called by lambda framework
8+
# instead of the customer's function, then our function will call the customer's function.
9+
# Note that after loading the original handler we will reset the _HANDLER variable to its original value.
10+
export HANDLER_ENV_NAME_FOR_CODEGURU=$_HANDLER
11+
export _HANDLER="codeguru_profiler_agent.aws_lambda.lambda_handler.call_handler"
12+
13+
exec "$@"

release_layer.py

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import codecs
2+
import re
3+
import subprocess
4+
import tempfile
5+
import shutil
6+
import os
7+
import json
8+
9+
# The following values are used in the documentation, so any change of them requires updates to the documentation.
10+
LAYER_NAME = 'AWSCodeGuruProfilerPythonAgentLambdaLayer'
11+
SUPPORTED_VERSIONS = ['3.6', '3.7', '3.8']
12+
EXEC_SCRIPT_FILE_NAME = 'codeguru_profiler_lambda_exec'
13+
14+
# We should release in all the regions that lambda layer is supported, not just the ones CodeGuru Profiler Service supports.
15+
# See this link for supported regions: https://docs.aws.amazon.com/general/latest/gr/lambda-service.html
16+
LAMBDA_LAYER_SUPPORTED_REGIONS = ['us-east-1', 'us-east-2', 'us-west-1', 'us-west-2',
17+
'ap-south-1', 'ap-northeast-2', 'ap-southeast-1', 'ap-southeast-2', 'ap-northeast-1',
18+
'ap-east-1',
19+
'ca-central-1',
20+
'eu-central-1', 'eu-west-1', 'eu-west-2', 'eu-west-3', 'eu-north-1', 'eu-south-1',
21+
'af-south-1', 'me-south-1', 'sa-east-1',
22+
'cn-north-1', 'cn-northwest-1',
23+
'us-gov-west-1', 'us-gov-east-1']
24+
25+
# Now we do not release for some of those regions yet:
26+
# - China regions are not available through the lambda console: cn-north-1, cn-northwest-1
27+
# - Some regions are opt-in, customers have to manually activate them to use so we will wait for customers to ask
28+
# for them: me-south-1, eu-south-1, af-south-1, ap-east-1
29+
# - US gov regions are also skipped for now: us-gov-west-1, us-gov-east-1
30+
SKIPPED_REGIONS = ['cn-north-1', 'cn-northwest-1', 'us-gov-west-1', 'us-gov-east-1',
31+
'me-south-1', 'eu-south-1', 'af-south-1', 'ap-east-1']
32+
REGIONS_TO_RELEASE_TO = sorted(set(LAMBDA_LAYER_SUPPORTED_REGIONS) - set(SKIPPED_REGIONS))
33+
34+
here = os.path.abspath(os.path.dirname(__file__))
35+
36+
37+
def confirm(prompt_str, answer_true='y', answer_false='n'):
38+
"""
39+
Just a manual prompt to ask for confirmation.
40+
This gives time for engineers to check the archive we have generated before publishing.
41+
"""
42+
prompt = '%s (%s|%s): ' % (prompt_str, answer_true, answer_false)
43+
44+
while True:
45+
answer = input(prompt).lower()
46+
if answer == answer_true:
47+
return True
48+
elif answer == answer_false:
49+
return False
50+
else:
51+
print('Please enter ' + answer_true + ' or ' + answer_false)
52+
53+
54+
def read(*parts):
55+
return codecs.open(os.path.join(here, *parts), 'r').read()
56+
57+
58+
def find_version(*file_paths):
59+
"""
60+
To keep track of which PyPI version corresponds to which layer version we cross reference the PyPI x¬version in the layer description.
61+
This function is used to read the PyPi directly from the source code.
62+
"""
63+
version_file = read(*file_paths)
64+
version_match = re.search(r"^__agent_version__ = ['\"]([^'\"]*)['\"]", version_file, re.M)
65+
if version_match:
66+
return version_match.group(1)
67+
raise RuntimeError('Unable to find version string.')
68+
69+
70+
def build_libraries():
71+
"""
72+
Build the module that will be later used to generate the archive for the layer.
73+
"""
74+
print('Building the module.')
75+
build_command = ['python setup.py build']
76+
subprocess.run(build_command, shell=True)
77+
78+
79+
def build_layer_archive():
80+
temporary_directory = tempfile.mkdtemp()
81+
print('Created temporary directory for the layer archive: ' + str(temporary_directory))
82+
layer_content_path = os.path.join(temporary_directory, 'layer')
83+
84+
# building the module
85+
build_libraries()
86+
87+
# copy the built module for each supported version
88+
for version in SUPPORTED_VERSIONS:
89+
shutil.copytree(os.path.join('build', 'lib', 'codeguru_profiler_agent'),
90+
os.path.join(layer_content_path, 'python', 'lib', 'python' + version, 'site-packages',
91+
'codeguru_profiler_agent'))
92+
93+
# copy the exec script, shutil.copyfile does not copy the permissions (i.e. script is executable) while copy2 does.
94+
shutil.copy2(EXEC_SCRIPT_FILE_NAME, os.path.join(layer_content_path, EXEC_SCRIPT_FILE_NAME))
95+
96+
shutil.make_archive(os.path.join(temporary_directory, 'layer'), 'zip', layer_content_path)
97+
return os.path.join(temporary_directory, 'layer.zip')
98+
99+
100+
def _disable_pager_for_aws_cli():
101+
"""
102+
By default AWS CLI v2 returns all output through your operating system’s default pager program
103+
This can mess up with scripts calling aws commands, disable it by setting an environment variable.
104+
See https://docs.aws.amazon.com/cli/latest/userguide/cli-usage-pagination.html
105+
"""
106+
os.environ['AWS_PAGER'] = ''
107+
108+
109+
def publish_new_version(layer_name, path_to_archive, region, module_version):
110+
cmd = ['aws', '--region', region, 'lambda', 'publish-layer-version',
111+
'--layer-name', layer_name,
112+
'--zip-file', 'fileb://' + path_to_archive,
113+
'--description', 'Python agent layer for AWS CodeGuru Profiler. Module version = ' + module_version,
114+
'--license-info', 'ADSL', # https://spdx.org/licenses/ADSL.html
115+
'--compatible-runtimes']
116+
cmd += ['python' + v for v in SUPPORTED_VERSIONS]
117+
result = subprocess.run(cmd, capture_output=True, text=True)
118+
if result.returncode != 0:
119+
print(str(result.stderr))
120+
raise RuntimeError('Failed to publish layer')
121+
output = json.loads(result.stdout)
122+
return str(output['Version']), output['LayerVersionArn']
123+
124+
125+
def add_permission_to_layer(layer_name, region, version, principal=None):
126+
if not principal:
127+
principal = '*'
128+
print(' - Adding permission to use the layer to: ' + principal)
129+
state_id = 'UniversalReadPermissions' if principal == '*' else 'ReadPermissions-' + principal
130+
cmd = ['aws', 'lambda', 'add-layer-version-permission',
131+
'--layer-name', layer_name,
132+
'--region', region,
133+
'--version-number', version,
134+
'--statement-id', state_id,
135+
'--principal', principal,
136+
'--action', 'lambda:GetLayerVersion']
137+
result = subprocess.run(cmd, capture_output=True, text=True)
138+
if result.returncode != 0:
139+
print(str(result.stderr))
140+
raise RuntimeError('Failed to add permission to layer')
141+
142+
143+
def publish_layer(path_to_archive, module_version, regions=None, layer_name=None, customer_accounts=None):
144+
print('Publishing module version {} from archive {}.'.format(module_version, path_to_archive))
145+
_disable_pager_for_aws_cli()
146+
for region in regions:
147+
print('Publishing layer in region ' + region)
148+
new_version, arn = publish_new_version(layer_name, path_to_archive, region, module_version)
149+
print(' ' + arn)
150+
for account_id in customer_accounts:
151+
add_permission_to_layer(layer_name, region, new_version, account_id)
152+
153+
154+
def main():
155+
from argparse import ArgumentParser
156+
usage = 'python %(prog)s [-r region] [-a account] [--role role]'
157+
parser = ArgumentParser(usage=usage)
158+
parser.add_argument('-n', '--layer-name', dest='layer_name', help='Name of the layer, default is ' + LAYER_NAME)
159+
parser.add_argument('-r', '--region', dest='region',
160+
help='Region in which you want to create the layer or add permission, '
161+
'default is all supported regions')
162+
163+
args = parser.parse_args()
164+
layer_name = args.layer_name if args.layer_name else LAYER_NAME
165+
regions = [args.region] if args.region else REGIONS_TO_RELEASE_TO
166+
customer_accounts = ['*']
167+
module_version = find_version('codeguru_profiler_agent/agent_metadata', 'agent_metadata.py')
168+
169+
archive = build_layer_archive()
170+
print('Preparing to publish archive ' + archive)
171+
if confirm('Publish the layer? Check the archive before responding. '):
172+
publish_layer(path_to_archive=archive, module_version=module_version, regions=regions,
173+
layer_name=layer_name, customer_accounts=customer_accounts)
174+
else:
175+
print('Nothing was published.')
176+
177+
178+
if __name__ == '__main__':
179+
main()

0 commit comments

Comments
 (0)