diff --git a/apigw-lambda-sns/Readme.md b/apigw-lambda-sns/README.md similarity index 66% rename from apigw-lambda-sns/Readme.md rename to apigw-lambda-sns/README.md index 0337e8440..8a896eefa 100644 --- a/apigw-lambda-sns/Readme.md +++ b/apigw-lambda-sns/README.md @@ -2,13 +2,22 @@ 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. +## Features + +- **API Gateway REST API** with Lambda integration +- **Lambda function** that publishes messages to SNS +- **SNS topic** for message publishing +- **CloudWatch Alarm** monitoring API errors +- **Amazon CloudWatch Synthetics Canary** for automated API endpoint monitoring +- **AWS X-Ray tracing** enabled for distributed tracing on LAmbda and APIGW (incurs additional costs) +- **S3 bucket** for Synthetics artifacts storage + Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/apigw-lambda-sns/. -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. +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. ## Requirements - * [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. * [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured * [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) @@ -50,19 +59,27 @@ curl --location --request GET 'https://.execute-api..amazonaws.c ``` In order to receive a notification, please make sure to configure subscription in the SNS topic. +### Additional Features -## Cleanup +- **CloudWatch Alarm**: Monitor the Synthetics Canary failures. The alarm triggers when the canary fails at least once within a 5-minute period. +- **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 +- **X-Ray Tracing**: Distributed tracing is enabled for both API Gateway and Lambda to help with debugging and performance analysis. +## Cleanup + 1. Delete the stack ``` aws cloudformation delete-stack —stack-name STACK_NAME ``` -2. Confirm the stack has been deleted +2. **Manually delete the S3 bucket** - The Synthetics artifacts bucket must be manually emptied and deleted after stack deletion +3. Confirm the stack has been deleted ``` aws cloudformation list-stacks —query "StackSummaries[?contains(StackName,'STACK_NAME')].StackStatus" ``` +**Important**: You must manually delete the S3 bucket created for Synthetics artifacts after deleting the CloudFormation stack, as it will contain canary run artifacts. + ---- Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/apigw-lambda-sns/api.yaml b/apigw-lambda-sns/api.yaml index 2438adbed..ac9215e37 100644 --- a/apigw-lambda-sns/api.yaml +++ b/apigw-lambda-sns/api.yaml @@ -17,15 +17,23 @@ paths: application/json: schema: $ref: "#/components/schemas/Empty" + "400": + description: "400 response" + "500": + description: "500 response" x-amazon-apigateway-integration: httpMethod: "POST" uri: "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:LambdaFunctionName/invocations" responses: default: statusCode: "200" + ".*4\\d{2}.*": + statusCode: "400" + ".*5\\d{2}.*": + statusCode: "500" passthroughBehavior: "when_no_match" contentHandling: "CONVERT_TO_TEXT" - type: "aws" + type: "aws_proxy" components: schemas: Empty: diff --git a/apigw-lambda-sns/apigw-lambda-sns.json b/apigw-lambda-sns/apigw-lambda-sns.json new file mode 100644 index 000000000..7dbdb8035 --- /dev/null +++ b/apigw-lambda-sns/apigw-lambda-sns.json @@ -0,0 +1,66 @@ +{ + "title": "Amazon API Gateway REST API to Lambda to SNS", + "description": "Integration of Amazon API Gateway REST API with Amazon Lambda to publish to Amazon SNS with enhanced monitoring", + "language": "Python", + "level": "200", + "framework": "AWS SAM", + "introBox": { + "headline": "How it works", + "text": [ + "This sample project demonstrates how to publish to a SNS Topic whenever the REST API is invoked using Lambda function.", + "This pattern deploys a Amazon API Gateway REST API with Lambda Function integration and SNS Topic with enhanced monitoring features including CloudWatch Alarms, X-Ray tracing, and Synthetics Canary." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/apigw-lambda-sns", + "templateURL": "serverless-patterns/apigw-lambda-sns", + "projectFolder": "apigw-lambda-sns", + "templateFile": "template.yaml" + } + }, + "resources": { + "bullets": [ + { + "text": "Choosing between messaging services for serverless applications.", + "link": "https://aws.amazon.com/blogs/compute/choosing-between-messaging-services-for-serverless-applications/" + }, + { + "text": "Publishing Messages in Amazon SNS", + "link": "https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sns.html#topic" + }, + { + "text": "AWS X-Ray Distributed Tracing", + "link": "https://docs.aws.amazon.com/xray/latest/devguide/aws-xray.html" + }, + { + "text": "Amazon CloudWatch Synthetics", + "link": "https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries.html" + } + ] + }, + "deploy": { + "text": [ + "sam deploy" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "Delete the stack: sam delete.", + "Manually delete the S3 bucket created for Synthetics artifacts after deleting the CloudFormation stack." + ] + }, + "authors": [ + { + "name": "Sanskar", + "image": "https://drive.google.com/file/d/1dP8XHHevaOC-eEKOq6Gty75ZBZ428KHT/view?usp=sharing", + "bio": "Sanskar is a Serverless Cloud Engineer at Amazon Web Services and is based in India. Sanskar has avid interest in microservices and serverless patterns.", + "linkedin": "sanskar05" + } + ] +} diff --git a/apigw-lambda-sns/example-pattern.json b/apigw-lambda-sns/example-pattern.json index c4fbb79c7..edbe28bd2 100644 --- a/apigw-lambda-sns/example-pattern.json +++ b/apigw-lambda-sns/example-pattern.json @@ -1,6 +1,6 @@ { "title": "Amazon API Gateway REST API to Lambda to SNS", - "description": "Integration of Amazon API Gateway REST API with Amazon Lambda to publish to Amazon SNS", + "description": "Integration of Amazon API Gateway REST API with Amazon Lambda to publish to Amazon SNS with enhanced monitoring", "language": "Python", "level": "200", "framework": "AWS SAM", @@ -8,7 +8,7 @@ "headline": "How it works", "text": [ "This sample project demonstrates how to publish to a SNS Topic whenever the REST API is invoked using Lambda function.", - "This pattern deploys a Amazon API Gateway REST API with Lambda Function integration and SNS Topic." + "This pattern deploys a Amazon API Gateway REST API with Lambda Function integration and SNS Topic with enhanced monitoring features including CloudWatch Alarms, X-Ray tracing, and Synthetics Canary." ] }, "gitHub": { @@ -29,6 +29,14 @@ "text": "Publishing Messages in Amazon SNS", "link": "https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sns.html#topic" }, + { + "text": "AWS X-Ray Distributed Tracing", + "link": "https://docs.aws.amazon.com/xray/latest/devguide/aws-xray.html" + }, + { + "text": "Amazon CloudWatch Synthetics", + "link": "https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries.html" + }, ] }, "deploy": { @@ -43,7 +51,8 @@ }, "cleanup": { "text": [ - "Delete the stack: same delete." + "Delete the stack: sam delete.", + "Manually delete the S3 bucket created for Synthetics artifacts after deleting the CloudFormation stack." ] }, "authors": [ diff --git a/apigw-lambda-sns/src/code.py b/apigw-lambda-sns/src/code.py index c9e6df00f..0a3feb66c 100644 --- a/apigw-lambda-sns/src/code.py +++ b/apigw-lambda-sns/src/code.py @@ -11,23 +11,38 @@ def lambda_handler(event, context): logger.setLevel(logging.INFO) logger.info("request: " + json.dumps(event)) - topic_arn = os.environ.get('TOPIC_ARN') - - sns_client = boto3.client("sns") - try: + topic_arn = os.environ.get('TOPIC_ARN') + if not topic_arn: + logger.error("Missing TOPIC_ARN environment variable") + return { + "statusCode": 500, + "body": json.dumps({"error": "Server configuration error"}) + } + + sns_client = boto3.client("sns") sent_message = sns_client.publish( TargetArn=topic_arn, Message=json.dumps({'default': json.dumps(event)}) ) - if sent_message is not None: - logger.info(f"Success - Message ID: {sent_message['MessageId']}") + logger.info(f"Success - Message ID: {sent_message['MessageId']}") return { "statusCode": 200, - "body": json.dumps("Success") + "body": json.dumps({"status": "Success", "messageId": sent_message['MessageId']}) } except ClientError as e: - logger.error(e) - return None \ No newline at end of file + error_code = e.response['Error']['Code'] + error_message = e.response['Error']['Message'] + logger.error(f"ClientError: {error_code} - {error_message}") + return { + "statusCode": 500, + "body": json.dumps({"error": "Failed to publish message to SNS"}) + } + except Exception as e: + logger.error(f"Unexpected error: {str(e)}") + return { + "statusCode": 500, + "body": json.dumps({"error": "Internal server error"}) + } \ No newline at end of file diff --git a/apigw-lambda-sns/template.yaml b/apigw-lambda-sns/template.yaml index 747a0ca60..3e5c602be 100644 --- a/apigw-lambda-sns/template.yaml +++ b/apigw-lambda-sns/template.yaml @@ -10,11 +10,18 @@ Resources: Type: AWS::Serverless::Api Properties: StageName: s1 - DefinitionBody: # an OpenApi definition - 'Fn::Transform': - Name: 'AWS::Include' + TracingEnabled: true + MethodSettings: + - ResourcePath: '/*' + HttpMethod: '*' + MetricsEnabled: true + DataTraceEnabled: true + LoggingLevel: INFO + DefinitionBody: + Fn::Transform: + Name: AWS::Include Parameters: - Location: './api.yaml' + Location: ./api.yaml OpenApiVersion: 3.0.3 EndpointConfiguration: Type: REGIONAL @@ -28,22 +35,13 @@ Resources: Handler: code.lambda_handler MemorySize: 128 Timeout: 3 - Runtime: python3.8 + Runtime: python3.13 + Tracing: Active Environment: Variables: TOPIC_ARN: !Ref MySnsTopic - AssumeRolePolicyDocument: - Version: 2012-10-17 - Statement: - - Action: - - sts:AssumeRole - Effect: Allow - Principal: - Service: - - lambda.amazonaws.com + API_URL: !Sub 'https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/${RestApi.Stage}' Policies: - - S3FullAccessPolicy: - BucketName: severlesspatternlambda - SNSPublishMessagePolicy: TopicName: !GetAtt MySnsTopic.TopicName Events: @@ -53,6 +51,121 @@ Resources: Path: / Method: GET RestApiId: !Ref RestApi + + # CloudWatch Alarm for API Gateway 5XX Errors + ApiGateway5XXErrorAlarm: + Type: AWS::CloudWatch::Alarm + Properties: + AlarmName: !Sub '${AWS::StackName}-API-Gateway-5XX-Error' + AlarmDescription: Monitor API Gateway 5XX errors + MetricName: 5XXError + Namespace: AWS/ApiGateway + Dimensions: + - Name: ApiName + Value: RestApi + - Name: Stage + Value: !Ref RestApi.Stage + Statistic: Sum + Period: 300 + EvaluationPeriods: 1 + Threshold: 3 + ComparisonOperator: GreaterThanThreshold + TreatMissingData: notBreaching + + # S3 Bucket for Synthetics Canary Artifacts + SyntheticsArtifactsBucket: + Type: AWS::S3::Bucket + DeletionPolicy: Retain + Properties: + PublicAccessBlockConfiguration: + BlockPublicAcls: true + BlockPublicPolicy: true + IgnorePublicAcls: true + RestrictPublicBuckets: true + + # IAM Role for Synthetics Canary + SyntheticsCanaryRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + - synthetics.amazonaws.com + - lambda.amazonaws.com + Action: sts:AssumeRole + Policies: + - PolicyName: SyntheticsPolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - s3:PutObject + - s3:GetObject + - s3:ListBucket + - s3:ListAllMyBuckets + - s3:GetBucketLocation + Resource: + - !Sub '${SyntheticsArtifactsBucket.Arn}/*' + - !GetAtt SyntheticsArtifactsBucket.Arn + - Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + - cloudwatch:PutMetricData + - synthetics:* + Resource: '*' + + # Synthetics Canary + ApiGatewayCanary: + Type: AWS::Synthetics::Canary + Properties: + Name: !Sub '${AWS::StackName}-api-gw-canary' + RuntimeVersion: syn-python-selenium-6.0 + ExecutionRoleArn: !GetAtt SyntheticsCanaryRole.Arn + ArtifactS3Location: !Sub 's3://${SyntheticsArtifactsBucket}/' + Schedule: + Expression: 'rate(1 minute)' + DurationInSeconds: 0 + RunConfig: + TimeoutInSeconds: 60 + MemoryInMB: 960 + FailureRetentionPeriod: 30 + SuccessRetentionPeriod: 30 + StartCanaryAfterCreation: true + Code: + Handler: canary.handler + Script: !Sub | + from selenium.webdriver.common.by import By + from aws_synthetics.selenium import synthetics_webdriver as syn_webdriver + from aws_synthetics.common import synthetics_logger as logger + + def main(): + url = "https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/${RestApi.Stage}" + + # Set screenshot option + takeScreenshot = True + + browser = syn_webdriver.Chrome() + browser.get(url) + + if takeScreenshot: + browser.save_screenshot("loaded.png") + + response_code = syn_webdriver.get_http_response(url) + if not response_code or response_code == "error" or response_code < 200 or response_code > 299: + raise Exception("Failed to load page!") + logger.info("Canary successfully executed.") + + def handler(event, context): + # user defined log statements using synthetics_logger + logger.info("Selenium Python heartbeat canary.") + return main() + Outputs: lambdaArn: Value: !GetAtt lambdaFunction.Arn @@ -62,4 +175,16 @@ Outputs: Value: !Ref MySnsTopic apiGatewayInvokeURL: - Value: !Sub https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/s1 \ No newline at end of file + Value: !Sub https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/${RestApi.Stage} + + CloudWatchAlarmName: + Description: Name of the API Gateway 5XX Error Alarm + Value: !Ref ApiGateway5XXErrorAlarm + + SyntheticsCanaryName: + Description: Name of the Synthetics Canary + Value: !Ref ApiGatewayCanary + + SyntheticsArtifactsBucket: + Description: S3 Bucket for Synthetics artifacts (delete manually after stack deletion) + Value: !Ref SyntheticsArtifactsBucket \ No newline at end of file