Skip to content

Commit 5e45bae

Browse files
authored
Add Python WebSocket example
Merge pull request #747 from awsdocs/websocket
2 parents b37cb8d + 4142ee1 commit 5e45bae

File tree

6 files changed

+1257
-0
lines changed

6 files changed

+1257
-0
lines changed
Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
# snippet-comment:[These are tags for the AWS doc team's sample catalog. Do not remove.]
2+
# snippet-sourcedescription:[lambda_util.py implements a group of utility functions to manage AWS Lambda functions.]
3+
# snippet-service:[lambda]
4+
# snippet-keyword:[AWS Lambda]
5+
# snippet-keyword:[Python]
6+
# snippet-keyword:[Code Sample]
7+
# snippet-sourcetype:[snippet]
8+
# snippet-sourcedate:[2019-07-11]
9+
# snippet-sourceauthor:[AWS]
10+
11+
# Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
12+
#
13+
# Licensed under the Apache License, Version 2.0 (the "License"). You
14+
# may not use this file except in compliance with the License. A copy of
15+
# the License is located at
16+
#
17+
# http://aws.amazon.com/apache2.0/
18+
#
19+
# or in the "license" file accompanying this file. This file is
20+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
21+
# ANY KIND, either express or implied. See the License for the specific
22+
# language governing permissions and limitations under the License.
23+
24+
25+
import json
26+
import logging
27+
import os
28+
import time
29+
import zipfile
30+
import zlib
31+
import boto3
32+
from botocore.exceptions import ClientError
33+
34+
35+
def create_lambda_deployment_package(srcfile, deployment_package):
36+
"""Create a Lambda deployment package (ZIP file)
37+
38+
:param srcfile: Lambda function source file
39+
:param deployment_package: Name of generated deployment package
40+
:return: True if deployment package created. Otherwise, False.
41+
"""
42+
43+
# Create the deployment package
44+
with zipfile.ZipFile(deployment_package, mode='w',
45+
compression=zipfile.ZIP_DEFLATED,
46+
compresslevel=zlib.Z_DEFAULT_COMPRESSION) as deploy_pkg:
47+
try:
48+
deploy_pkg.write(srcfile)
49+
except Exception as e:
50+
logging.error(e)
51+
return False
52+
return True
53+
54+
55+
def get_iam_role_arn(iam_role_name):
56+
"""Retrieve the ARN of the specified IAM role
57+
58+
:param iam_role_name: IAM role name
59+
:return: If the IAM role exists, return ARN, else None
60+
"""
61+
62+
# Try to retrieve information about the role
63+
iam_client = boto3.client('iam')
64+
try:
65+
result = iam_client.get_role(RoleName=iam_role_name)
66+
except ClientError as e:
67+
logging.error(e)
68+
return None
69+
return result['Role']['Arn']
70+
71+
72+
def iam_role_exists(iam_role_name):
73+
"""Check if the specified IAM role exists
74+
75+
:param iam_role_name: IAM role name
76+
:return: True if IAM role exists, else False
77+
"""
78+
79+
# Try to retrieve information about the role
80+
if get_iam_role_arn(iam_role_name) is None:
81+
return False
82+
return True
83+
84+
85+
def create_iam_role_for_lambda(iam_role_name):
86+
"""Create an IAM role to enable a Lambda function to call AWS services
87+
88+
:param iam_role_name: Name of IAM role
89+
:return: ARN of IAM role. If error, returns None.
90+
"""
91+
92+
# Lambda trusted relationship policy document
93+
lambda_assume_role = {
94+
'Version': '2012-10-17',
95+
'Statement': [
96+
{
97+
'Sid': '',
98+
'Effect': 'Allow',
99+
'Principal': {
100+
'Service': 'lambda.amazonaws.com'
101+
},
102+
'Action': 'sts:AssumeRole'
103+
}
104+
]
105+
}
106+
iam_client = boto3.client('iam')
107+
try:
108+
result = iam_client.create_role(RoleName=iam_role_name,
109+
AssumeRolePolicyDocument=json.dumps(lambda_assume_role))
110+
except ClientError as e:
111+
logging.error(e)
112+
return None
113+
lambda_role_arn = result['Role']['Arn']
114+
115+
# Attach the AWSLambdaBasicExecutionRole policy to the role
116+
# If planning to use AWS X-Ray, also attach the AWSXrayWriteOnlyAccess policy
117+
lambda_policy_arn = 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
118+
try:
119+
iam_client.attach_role_policy(RoleName=iam_role_name,
120+
PolicyArn=lambda_policy_arn)
121+
except ClientError as e:
122+
logging.error(e)
123+
return None
124+
125+
'''
126+
# Debug: Verify policy is attached to the role
127+
try:
128+
response = iam_client.list_attached_role_policies(RoleName=iam_role_name)
129+
except ClientError as e:
130+
logging.error(e)
131+
else:
132+
for policy in response['AttachedPolicies']:
133+
logging.debug(f'Role: {iam_role_name}, '
134+
f'Attached Policy: {policy["PolicyName"]}, '
135+
f'ARN: {policy["PolicyArn"]}')
136+
'''
137+
138+
# Return the ARN of the created IAM role
139+
return lambda_role_arn
140+
141+
142+
def deploy_lambda_function(name, iam_role, handler, deployment_package,
143+
runtime, env_vars, region):
144+
"""Deploy the Lambda function
145+
146+
:param name: Descriptive Lambda function name
147+
:param iam_role: IAM Lambda role
148+
:param handler: Name of Lambda handler function
149+
:param deployment_package: Name of deployment package
150+
:param runtime: Lambda runtime programming language
151+
:param env_vars: Environment variables for Lambda context
152+
:param region: Region to deploy the Lambda function
153+
:return: Dictionary containing information about the function, else None
154+
"""
155+
156+
# Load the deployment package into memory
157+
# Alternatively, upload it to S3
158+
with open(deployment_package, mode='rb') as pkg:
159+
deploy_pkg = pkg.read()
160+
161+
# Create the Lambda function
162+
# Note: create_function() raises an InvalidParameterValueException if a
163+
# newly-created role has not been replicated to the appropriate region yet.
164+
# To resolve this situation, the operation is retried several times.
165+
lambda_client = boto3.client('lambda', region_name=region)
166+
retry_time = 1 # number of seconds to sleep
167+
max_retry_time = 32
168+
while retry_time <= max_retry_time:
169+
try:
170+
result = lambda_client.create_function(FunctionName=name,
171+
Runtime=runtime,
172+
Role=iam_role,
173+
Handler=handler,
174+
Environment=env_vars,
175+
Code={'ZipFile': deploy_pkg})
176+
except ClientError as e:
177+
logging.error(e)
178+
logging.error(f'IAM role ARN: {iam_role}')
179+
180+
# If InvalidParameterValueException, retry a few times until the role
181+
# has replicated to all regions.
182+
if e.response['Error']['Code'] == 'InvalidParameterValueException':
183+
time.sleep(retry_time)
184+
retry_time *= 2
185+
else:
186+
return None
187+
else:
188+
return result
189+
return None
190+
191+
192+
def create_lambda_function(function_name, srcfile, handler_name, role_name,
193+
region, env_vars={}):
194+
"""Create a Lambda function
195+
196+
It is assumed that srcfile includes an extension, such as source.py or
197+
source.js. The filename minus the extension is used to construct the
198+
ZIP file deployment package, e.g., source.zip.
199+
200+
If the role_name exists, the existing role is used. Otherwise, an
201+
appropriate role is created.
202+
203+
:param function_name: Lambda function name
204+
:param srcfile: Lambda source file
205+
:param handler_name: Lambda handler name
206+
:param role_name: Lambda role name
207+
:param region: Region to locate the Lambda resource
208+
:param env_vars: Dict of environment variables for Lambda context
209+
:return: String ARN of the created Lambda function. If error, returns None.
210+
"""
211+
212+
# Parse the filename and extension in srcfile
213+
filename, ext = os.path.splitext(srcfile)
214+
215+
# Create a deployment package
216+
deployment_package = f'{filename}.zip'
217+
if not create_lambda_deployment_package(srcfile, deployment_package):
218+
return None
219+
220+
# Create Lambda IAM role if necessary
221+
if iam_role_exists(role_name):
222+
# Retrieve its ARN
223+
iam_role_arn = get_iam_role_arn(role_name)
224+
else:
225+
iam_role_arn = create_iam_role_for_lambda(role_name)
226+
if iam_role_arn is None:
227+
# Error creating IAM role
228+
return None
229+
230+
# Determine the Lambda runtime to use
231+
if ext == '.py':
232+
runtime = 'python3.7'
233+
elif ext == '.js':
234+
runtime = 'nodejs10.x'
235+
else:
236+
# Unexpected Lambda runtime
237+
return None
238+
239+
# Deploy the Lambda function
240+
microservice = deploy_lambda_function(function_name, iam_role_arn,
241+
f'{filename}.{handler_name}',
242+
deployment_package,
243+
runtime,
244+
env_vars, region)
245+
if microservice is None:
246+
return None
247+
lambda_arn = microservice['FunctionArn']
248+
logging.info(f'Created Lambda function: {function_name}')
249+
logging.info(f'ARN: {lambda_arn}')
250+
return lambda_arn
251+
252+
253+
def delete_lambda_function(function_name, region):
254+
"""Delete all versions of a Lambda function
255+
256+
:param function_name: Lambda function to delete
257+
:param region: Region containing the Lambda function
258+
:return: True if function was deleted, else False
259+
"""
260+
261+
# Delete all versions of the Lambda function
262+
lambda_client = boto3.client('lambda', region_name=region)
263+
try:
264+
lambda_client.delete_function(FunctionName=function_name)
265+
except ClientError as e:
266+
logging.error(e)
267+
return False
268+
return True
269+
270+
271+
def invoke_lambda_function_synchronous(name, parameters, region):
272+
"""Invoke a Lambda function synchronously
273+
274+
:param name: Lambda function name or ARN or partial ARN
275+
:param parameters: Dict of parameters and values to pass to function
276+
:param region: Region containing the Lambda function
277+
:return: Dict of response parameters and values. If error, returns None.
278+
"""
279+
280+
# Convert the parameters from dict -> string -> bytes
281+
params_bytes = json.dumps(parameters).encode()
282+
283+
# Invoke the Lambda function
284+
lambda_client = boto3.client('lambda', region_name=region)
285+
try:
286+
response = lambda_client.invoke(FunctionName=name,
287+
InvocationType='RequestResponse',
288+
LogType='Tail',
289+
Payload=params_bytes)
290+
except ClientError as e:
291+
logging.error(e)
292+
return None
293+
return response
294+
295+
296+
def get_lambda_arn(lambda_name, region):
297+
"""Retrieve the ARN of a Lambda function
298+
299+
:param lambda_name: Name of Lambda function
300+
:param region: Region containing the Lambda function
301+
:return: String ARN of Lambda function. If error, returns None.
302+
"""
303+
304+
# Retrieve information about the Lambda function
305+
lambda_client = boto3.client('lambda', region_name=region)
306+
try:
307+
response = lambda_client.get_function(FunctionName=lambda_name)
308+
except ClientError as e:
309+
logging.error(e)
310+
return None
311+
return response['Configuration']['FunctionArn']

0 commit comments

Comments
 (0)