Skip to content

Commit 592a07f

Browse files
committed
Enhance apigw-lambda-sns pattern with production-ready improvements
1 parent 584ecdd commit 592a07f

File tree

4 files changed

+196
-31
lines changed

4 files changed

+196
-31
lines changed

apigw-lambda-sns/Readme.md

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,22 @@
22

33
The SAM template deploys a API Gateway REST API with Lambda function integration, an SNS topic and the IAM permissions required to run the application. Whenever the REST API is invoked, the Lambda function publishes a message to the SNS topic. The AWS SAM template deploys the resources and the IAM permissions required to run the application.
44

5+
## Features
6+
7+
- **API Gateway REST API** with Lambda integration
8+
- **Lambda function** that publishes messages to SNS
9+
- **SNS topic** for message publishing
10+
- **CloudWatch Alarm** monitoring API errors
11+
- **Amazon CloudWatch Synthetics Canary** for automated API endpoint monitoring
12+
- **AWS X-Ray tracing** enabled for distributed tracing on LAmbda and APIGW (incurs additional costs)
13+
- **S3 bucket** for Synthetics artifacts storage
14+
515
Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/apigw-lambda-sns/.
616

7-
Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example.
17+
Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. **Note: AWS X-Ray tracing is enabled which incurs additional charges based on traces recorded and retrieved.** You are responsible for any AWS costs incurred. No warranty is implied in this example.
818

919
## Requirements
1020

11-
1221
* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources.
1322
* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured
1423
* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
@@ -50,19 +59,27 @@ curl --location --request GET 'https://<api_id>.execute-api.<region>.amazonaws.c
5059
```
5160
In order to receive a notification, please make sure to configure subscription in the SNS topic.
5261
62+
### Additional Features
5363
54-
## Cleanup
64+
- **CloudWatch Alarm**: Monitor the Synthetics Canary failures. The alarm triggers when the canary fails at least once within a 5-minute period.
65+
- **Synthetics Canary**: Automatically tests the API endpoint every minute to ensure availability. If you want to alarm on this, you must manually create a CloudWatch Alarm or update the template
66+
- **X-Ray Tracing**: Distributed tracing is enabled for both API Gateway and Lambda to help with debugging and performance analysis.
5567
5668
69+
## Cleanup
70+
5771
1. Delete the stack
5872
```
5973
aws cloudformation delete-stack —stack-name STACK_NAME
6074
```
61-
2. Confirm the stack has been deleted
75+
2. **Manually delete the S3 bucket** - The Synthetics artifacts bucket must be manually emptied and deleted after stack deletion
76+
3. Confirm the stack has been deleted
6277
```
6378
aws cloudformation list-stacks —query "StackSummaries[?contains(StackName,'STACK_NAME')].StackStatus"
6479
```
6580
81+
**Important**: You must manually delete the S3 bucket created for Synthetics artifacts after deleting the CloudFormation stack, as it will contain canary run artifacts.
82+
6683
----
6784
Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
6885

apigw-lambda-sns/api.yaml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,23 @@ paths:
1717
application/json:
1818
schema:
1919
$ref: "#/components/schemas/Empty"
20+
"400":
21+
description: "400 response"
22+
"500":
23+
description: "500 response"
2024
x-amazon-apigateway-integration:
2125
httpMethod: "POST"
2226
uri: "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:LambdaFunctionName/invocations"
2327
responses:
2428
default:
2529
statusCode: "200"
30+
".*4\\d{2}.*":
31+
statusCode: "400"
32+
".*5\\d{2}.*":
33+
statusCode: "500"
2634
passthroughBehavior: "when_no_match"
2735
contentHandling: "CONVERT_TO_TEXT"
28-
type: "aws"
36+
type: "aws_proxy"
2937
components:
3038
schemas:
3139
Empty:

apigw-lambda-sns/src/code.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,38 @@ def lambda_handler(event, context):
1111
logger.setLevel(logging.INFO)
1212
logger.info("request: " + json.dumps(event))
1313

14-
topic_arn = os.environ.get('TOPIC_ARN')
15-
16-
sns_client = boto3.client("sns")
17-
1814
try:
15+
topic_arn = os.environ.get('TOPIC_ARN')
16+
if not topic_arn:
17+
logger.error("Missing TOPIC_ARN environment variable")
18+
return {
19+
"statusCode": 500,
20+
"body": json.dumps({"error": "Server configuration error"})
21+
}
22+
23+
sns_client = boto3.client("sns")
1924
sent_message = sns_client.publish(
2025
TargetArn=topic_arn,
2126
Message=json.dumps({'default': json.dumps(event)})
2227
)
2328

24-
if sent_message is not None:
25-
logger.info(f"Success - Message ID: {sent_message['MessageId']}")
29+
logger.info(f"Success - Message ID: {sent_message['MessageId']}")
2630
return {
2731
"statusCode": 200,
28-
"body": json.dumps("Success")
32+
"body": json.dumps({"status": "Success", "messageId": sent_message['MessageId']})
2933
}
3034

3135
except ClientError as e:
32-
logger.error(e)
33-
return None
36+
error_code = e.response['Error']['Code']
37+
error_message = e.response['Error']['Message']
38+
logger.error(f"ClientError: {error_code} - {error_message}")
39+
return {
40+
"statusCode": 500,
41+
"body": json.dumps({"error": "Failed to publish message to SNS"})
42+
}
43+
except Exception as e:
44+
logger.error(f"Unexpected error: {str(e)}")
45+
return {
46+
"statusCode": 500,
47+
"body": json.dumps({"error": "Internal server error"})
48+
}

apigw-lambda-sns/template.yaml

Lines changed: 142 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,18 @@ Resources:
1010
Type: AWS::Serverless::Api
1111
Properties:
1212
StageName: s1
13-
DefinitionBody: # an OpenApi definition
14-
'Fn::Transform':
15-
Name: 'AWS::Include'
13+
TracingEnabled: true
14+
MethodSettings:
15+
- ResourcePath: '/*'
16+
HttpMethod: '*'
17+
MetricsEnabled: true
18+
DataTraceEnabled: true
19+
LoggingLevel: INFO
20+
DefinitionBody:
21+
Fn::Transform:
22+
Name: AWS::Include
1623
Parameters:
17-
Location: './api.yaml'
24+
Location: ./api.yaml
1825
OpenApiVersion: 3.0.3
1926
EndpointConfiguration:
2027
Type: REGIONAL
@@ -28,22 +35,13 @@ Resources:
2835
Handler: code.lambda_handler
2936
MemorySize: 128
3037
Timeout: 3
31-
Runtime: python3.8
38+
Runtime: python3.13
39+
Tracing: Active
3240
Environment:
3341
Variables:
3442
TOPIC_ARN: !Ref MySnsTopic
35-
AssumeRolePolicyDocument:
36-
Version: 2012-10-17
37-
Statement:
38-
- Action:
39-
- sts:AssumeRole
40-
Effect: Allow
41-
Principal:
42-
Service:
43-
- lambda.amazonaws.com
43+
API_URL: !Sub 'https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/${RestApi.Stage}'
4444
Policies:
45-
- S3FullAccessPolicy:
46-
BucketName: severlesspatternlambda
4745
- SNSPublishMessagePolicy:
4846
TopicName: !GetAtt MySnsTopic.TopicName
4947
Events:
@@ -53,6 +51,121 @@ Resources:
5351
Path: /
5452
Method: GET
5553
RestApiId: !Ref RestApi
54+
55+
# CloudWatch Alarm for API Gateway 5XX Errors
56+
ApiGateway5XXErrorAlarm:
57+
Type: AWS::CloudWatch::Alarm
58+
Properties:
59+
AlarmName: !Sub '${AWS::StackName}-API-Gateway-5XX-Error'
60+
AlarmDescription: Monitor API Gateway 5XX errors
61+
MetricName: 5XXError
62+
Namespace: AWS/ApiGateway
63+
Dimensions:
64+
- Name: ApiName
65+
Value: RestApi
66+
- Name: Stage
67+
Value: !Ref RestApi.Stage
68+
Statistic: Sum
69+
Period: 300
70+
EvaluationPeriods: 1
71+
Threshold: 3
72+
ComparisonOperator: GreaterThanThreshold
73+
TreatMissingData: notBreaching
74+
75+
# S3 Bucket for Synthetics Canary Artifacts
76+
SyntheticsArtifactsBucket:
77+
Type: AWS::S3::Bucket
78+
DeletionPolicy: Retain
79+
Properties:
80+
PublicAccessBlockConfiguration:
81+
BlockPublicAcls: true
82+
BlockPublicPolicy: true
83+
IgnorePublicAcls: true
84+
RestrictPublicBuckets: true
85+
86+
# IAM Role for Synthetics Canary
87+
SyntheticsCanaryRole:
88+
Type: AWS::IAM::Role
89+
Properties:
90+
AssumeRolePolicyDocument:
91+
Version: '2012-10-17'
92+
Statement:
93+
- Effect: Allow
94+
Principal:
95+
Service:
96+
- synthetics.amazonaws.com
97+
- lambda.amazonaws.com
98+
Action: sts:AssumeRole
99+
Policies:
100+
- PolicyName: SyntheticsPolicy
101+
PolicyDocument:
102+
Version: '2012-10-17'
103+
Statement:
104+
- Effect: Allow
105+
Action:
106+
- s3:PutObject
107+
- s3:GetObject
108+
- s3:ListBucket
109+
- s3:ListAllMyBuckets
110+
- s3:GetBucketLocation
111+
Resource:
112+
- !Sub '${SyntheticsArtifactsBucket.Arn}/*'
113+
- !GetAtt SyntheticsArtifactsBucket.Arn
114+
- Effect: Allow
115+
Action:
116+
- logs:CreateLogGroup
117+
- logs:CreateLogStream
118+
- logs:PutLogEvents
119+
- cloudwatch:PutMetricData
120+
- synthetics:*
121+
Resource: '*'
122+
123+
# Synthetics Canary
124+
ApiGatewayCanary:
125+
Type: AWS::Synthetics::Canary
126+
Properties:
127+
Name: !Sub '${AWS::StackName}-api-gw-canary'
128+
RuntimeVersion: syn-python-selenium-6.0
129+
ExecutionRoleArn: !GetAtt SyntheticsCanaryRole.Arn
130+
ArtifactS3Location: !Sub 's3://${SyntheticsArtifactsBucket}/'
131+
Schedule:
132+
Expression: 'rate(1 minute)'
133+
DurationInSeconds: 0
134+
RunConfig:
135+
TimeoutInSeconds: 60
136+
MemoryInMB: 960
137+
FailureRetentionPeriod: 30
138+
SuccessRetentionPeriod: 30
139+
StartCanaryAfterCreation: true
140+
Code:
141+
Handler: canary.handler
142+
Script: !Sub |
143+
from selenium.webdriver.common.by import By
144+
from aws_synthetics.selenium import synthetics_webdriver as syn_webdriver
145+
from aws_synthetics.common import synthetics_logger as logger
146+
147+
def main():
148+
url = "https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/${RestApi.Stage}"
149+
150+
# Set screenshot option
151+
takeScreenshot = True
152+
153+
browser = syn_webdriver.Chrome()
154+
browser.get(url)
155+
156+
if takeScreenshot:
157+
browser.save_screenshot("loaded.png")
158+
159+
response_code = syn_webdriver.get_http_response(url)
160+
if not response_code or response_code == "error" or response_code < 200 or response_code > 299:
161+
raise Exception("Failed to load page!")
162+
logger.info("Canary successfully executed.")
163+
164+
def handler(event, context):
165+
# user defined log statements using synthetics_logger
166+
logger.info("Selenium Python heartbeat canary.")
167+
return main()
168+
56169
Outputs:
57170
lambdaArn:
58171
Value: !GetAtt lambdaFunction.Arn
@@ -62,4 +175,16 @@ Outputs:
62175
Value: !Ref MySnsTopic
63176

64177
apiGatewayInvokeURL:
65-
Value: !Sub https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/s1
178+
Value: !Sub https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/${RestApi.Stage}
179+
180+
CloudWatchAlarmName:
181+
Description: Name of the API Gateway 5XX Error Alarm
182+
Value: !Ref ApiGateway5XXErrorAlarm
183+
184+
SyntheticsCanaryName:
185+
Description: Name of the Synthetics Canary
186+
Value: !Ref ApiGatewayCanary
187+
188+
SyntheticsArtifactsBucket:
189+
Description: S3 Bucket for Synthetics artifacts (delete manually after stack deletion)
190+
Value: !Ref SyntheticsArtifactsBucket

0 commit comments

Comments
 (0)