From 44f3dd1b16b471fc313a031f992b667321a0ac97 Mon Sep 17 00:00:00 2001 From: Shaun Guo Date: Sat, 19 Jul 2025 15:01:41 +1000 Subject: [PATCH 1/3] Create pattern --- apigw-sqs-lambda-sns/.gitignore | 29 ++++ apigw-sqs-lambda-sns/README.md | 127 ++++++++++++++++++ .../apigw-sqs-lambda-sns.json | 93 +++++++++++++ apigw-sqs-lambda-sns/app.py | 8 ++ apigw-sqs-lambda-sns/cdk.json | 58 ++++++++ apigw-sqs-lambda-sns/example-pattern.json | 75 +++++++++++ apigw-sqs-lambda-sns/lambda/app.py | 57 ++++++++ apigw-sqs-lambda-sns/requirements.txt | 2 + apigw-sqs-lambda-sns/webhook_sns/__init__.py | 1 + .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 176 bytes .../webhook_sns_stack.cpython-310.pyc | Bin 0 -> 2654 bytes .../webhook_sns/webhook_sns_stack.py | 119 ++++++++++++++++ 12 files changed, 569 insertions(+) create mode 100644 apigw-sqs-lambda-sns/.gitignore create mode 100644 apigw-sqs-lambda-sns/README.md create mode 100644 apigw-sqs-lambda-sns/apigw-sqs-lambda-sns.json create mode 100644 apigw-sqs-lambda-sns/app.py create mode 100644 apigw-sqs-lambda-sns/cdk.json create mode 100644 apigw-sqs-lambda-sns/example-pattern.json create mode 100644 apigw-sqs-lambda-sns/lambda/app.py create mode 100644 apigw-sqs-lambda-sns/requirements.txt create mode 100644 apigw-sqs-lambda-sns/webhook_sns/__init__.py create mode 100644 apigw-sqs-lambda-sns/webhook_sns/__pycache__/__init__.cpython-310.pyc create mode 100644 apigw-sqs-lambda-sns/webhook_sns/__pycache__/webhook_sns_stack.cpython-310.pyc create mode 100644 apigw-sqs-lambda-sns/webhook_sns/webhook_sns_stack.py diff --git a/apigw-sqs-lambda-sns/.gitignore b/apigw-sqs-lambda-sns/.gitignore new file mode 100644 index 000000000..60b3efa1a --- /dev/null +++ b/apigw-sqs-lambda-sns/.gitignore @@ -0,0 +1,29 @@ +*.js +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out + +# Parcel default cache directory +.parcel-cache + +# npm +npm-debug.log* +.npm + +# Yarn +yarn-error.log + +# dotenv environment variables file +.env + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +Thumbs.db diff --git a/apigw-sqs-lambda-sns/README.md b/apigw-sqs-lambda-sns/README.md new file mode 100644 index 000000000..e5bd11af0 --- /dev/null +++ b/apigw-sqs-lambda-sns/README.md @@ -0,0 +1,127 @@ +# Webhook SNS Pattern - CDK Version + +This is a simplified AWS CDK implementation of a webhook integration pattern that receives webhook events via API Gateway, queues them in SQS, and processes them with Lambda to send SMS notifications via SNS. + +## Architecture + +``` +API Gateway (POST) → SQS Queue → Lambda Function → SNS (SMS) +``` + +## Components + +- **API Gateway**: REST API endpoint to receive webhook events +- **SQS Queue**: Decouples the API from processing and provides reliability +- **Lambda Function**: Processes messages and sends SMS via SNS +- **SNS**: Sends SMS messages to phone numbers + +## Prerequisites + +- AWS CLI configured with appropriate permissions +- Python 3.9+ +- AWS CDK v2 installed (`npm install -g aws-cdk`) + +## Deployment + +1. Install Python dependencies: +```bash +pip install -r requirements.txt +``` + +2. Bootstrap CDK (if not done before): +```bash +cdk bootstrap +``` + +3. Deploy the stack: +```bash +cdk deploy +``` + +4. Note the API Gateway endpoint URL from the output. + +## Testing + +### SMS Sandbox Verification (Required for New AWS Accounts) + +If your AWS account is in SMS sandbox mode (default for new accounts), you'll need to verify your phone number before receiving SMS messages: + +1. **Check if your account is in sandbox mode:** +```bash +aws sns get-sms-sandbox-account-status --region your-region +``` + +2. **If in sandbox mode, verify your phone number:** +```bash +# Add your phone number to sandbox +aws sns create-sms-sandbox-phone-number --phone-number "+your-phone-number" --region your-region + +# Check your phone for verification code, then verify it +aws sns verify-sms-sandbox-phone-number --phone-number "+your-phone-number" --one-time-password "YOUR_CODE" --region your-region +``` + +3. **Alternative: Request production access** through the AWS Console (SNS → Text messaging → Sandbox) for unrestricted SMS sending. + +### API Testing + +Send a POST request to the API Gateway endpoint with the following JSON payload: + +Example using curl (update URL with your own API domain and phoneNumber with your phone number including country code e.g. +1234567890): +```bash +curl -X POST https://your-api-id.execute-api.region.amazonaws.com/prod/ \ + -H "Content-Type: application/json" \ + -d '{"phoneNumber": "+your-phone-number", "message": "Hello from webhook!"}' +``` + +Expected response: +```json +{ + "SendMessageResponse": { + "ResponseMetadata": { + "RequestId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + }, + "SendMessageResult": { + "MD5OfMessageAttributes": null, + "MD5OfMessageBody": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "MD5OfMessageSystemAttributes": null, + "MessageId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "SequenceNumber": null + } + } +} +``` + +You should also received an SMS on your mobile with the following message: +``` +Hello from webhook! +``` + +## Important Notes + +- **Phone Number Format**: Use E.164 format (e.g., +1234567890) +- **SMS Costs**: SNS SMS messages incur charges - be mindful of costs +- **Permissions**: Ensure your AWS account has SNS SMS permissions enabled +- **PoC Only**: This is a simplified pattern for proof of concept. For production usage, consider implementing authentication/authorization, Dead Letter Queues (DLQ) for failed message processing, proper error handling and retry logic, adding monitoring and alerting (CloudWatch alarms), rate limiting, and further input validation and sanitization. + +## Use Cases + +This pattern works well for various webhook integrations including: +- Marketing automation platforms +- CRM systems +- E-commerce platforms +- Monitoring and alerting systems +- Any system that needs to send SMS notifications based on webhook events + +## Cleanup + +To remove all resources: +```bash +cdk destroy +``` + +## Customization + +- Modify `lambda/app.py` to change message processing logic +- Update the CDK stack to add additional features like DLQ, encryption, etc. +- Add API authentication/authorization as needed +- Integrate with different notification channels (email, Slack, etc.) diff --git a/apigw-sqs-lambda-sns/apigw-sqs-lambda-sns.json b/apigw-sqs-lambda-sns/apigw-sqs-lambda-sns.json new file mode 100644 index 000000000..78f3d580d --- /dev/null +++ b/apigw-sqs-lambda-sns/apigw-sqs-lambda-sns.json @@ -0,0 +1,93 @@ +{ + "title": "Webhook integration with SMS notifications", + "description": "API Gateway webhook integration that queues events in SQS and processes them with Lambda to send SMS notifications via SNS", + "language": "Python", + "level": "200", + "framework": "AWS CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern demonstrates a reliable webhook integration that receives HTTP POST requests via API Gateway, queues them in SQS for decoupling and reliability, then processes the messages with Lambda to send SMS notifications through SNS.", + "The pattern uses SQS to decouple the API from message processing, providing resilience and the ability to handle traffic spikes. Lambda processes messages from the queue and sends SMS messages to phone numbers in E.164 format." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/apigw-sqs-lambda-sns", + "templateURL": "serverless-patterns/apigw-sqs-lambda-sns", + "projectFolder": "apigw-sqs-lambda-sns", + "templateFile": "webhook_sns/webhook_sns_stack.py" + } + }, + "deploy": { + "text": [ + "pip install -r requirements.txt", + "cdk bootstrap", + "cdk deploy" + ] + }, + "testing": { + "text": [ + "Send a POST request to the API Gateway endpoint:", + "curl -X POST https://your-api-id.execute-api.region.amazonaws.com/prod/ -H \"Content-Type: application/json\" -d '{\"phoneNumber\": \"+1234567890\", \"message\": \"Test message\"}'", + "Replace the phone number with a valid number in E.164 format for actual SMS delivery." + ] + }, + "cleanup": { + "text": [ + "Delete the stack: cdk destroy." + ] + }, + "authors": [ + { + "name": "Shaun Guo", + "image": "https://media.licdn.com/dms/image/C5103AQG3KMyMdEIKpA/profile-displayphoto-shrink_800_800/0/1517283953925?e=1692835200&v=beta&t=AxJ9ST_8K_bw8nqTPDaJB2F5dnQspES9FuJ64DBScC8", + "bio": "Shaun is a Senior Technical Account Manager at Amazon Web Services based in Australia", + "linkedin": "shaun-guo" + }, + { + "name": "Robbie Cooray", + "image": "https://media.licdn.com/dms/image/v2/C5603AQFK28pyKxfNiQ/profile-displayphoto-shrink_200_200/profile-displayphoto-shrink_200_200/0/1603248682998?e=1729728000&v=beta&t=YcFUfepq9JCKMvcqzaHMhTJFks6nrsfxS6v8JpolvEc", + "bio": "Robbie is a Senior Solutions Architect at Amazon Web Services based in Australia", + "linkedin": "robbiecooray" + } + ], + "patternArch": { + "icon1": { + "x": 10, + "y": 50, + "service": "apigw", + "label": "API Gateway" + }, + "icon2": { + "x": 35, + "y": 50, + "service": "sqs", + "label": "Amazon SQS" + }, + "icon3": { + "x": 60, + "y": 50, + "service": "lambda", + "label": "AWS Lambda" + }, + "icon4": { + "x": 85, + "y": 50, + "service": "sns", + "label": "Amazon SNS" + }, + "line1": { + "from": "icon1", + "to": "icon2" + }, + "line2": { + "from": "icon2", + "to": "icon3" + }, + "line3": { + "from": "icon3", + "to": "icon4" + } + } +} diff --git a/apigw-sqs-lambda-sns/app.py b/apigw-sqs-lambda-sns/app.py new file mode 100644 index 000000000..7e3844769 --- /dev/null +++ b/apigw-sqs-lambda-sns/app.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 +import aws_cdk as cdk +from webhook_sns.webhook_sns_stack import WebhookSnsStack + +app = cdk.App() +WebhookSnsStack(app, "WebhookSnsStack") + +app.synth() diff --git a/apigw-sqs-lambda-sns/cdk.json b/apigw-sqs-lambda-sns/cdk.json new file mode 100644 index 000000000..73a0028bb --- /dev/null +++ b/apigw-sqs-lambda-sns/cdk.json @@ -0,0 +1,58 @@ +{ + "app": "python3 app.py", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "requirements*.txt", + "source.bat", + "**/__pycache__", + "**/*.pyc" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableLogging": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": false, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-iam:standardizedServicePrincipals": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableLogging": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableLogging": true, + "@aws-cdk/aws-normlizedkeys:props": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-opensearchservice:usingLatestEngineVersion": true + } +} diff --git a/apigw-sqs-lambda-sns/example-pattern.json b/apigw-sqs-lambda-sns/example-pattern.json new file mode 100644 index 000000000..2d7c9c652 --- /dev/null +++ b/apigw-sqs-lambda-sns/example-pattern.json @@ -0,0 +1,75 @@ +{ + "title": "Webhook integration with SMS notifications", + "description": "API Gateway webhook integration that queues events in SQS and processes them with Lambda to send SMS notifications via SNS", + "language": "Python", + "level": "200", + "framework": "AWS CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern demonstrates a reliable webhook integration that receives HTTP POST requests via API Gateway, queues them in SQS for decoupling and reliability, then processes the messages with Lambda to send SMS notifications through SNS.", + "The pattern uses SQS to decouple the API from message processing, providing resilience and the ability to handle traffic spikes. Lambda processes messages from the queue and sends SMS messages to phone numbers in E.164 format." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/apigw-sqs-lambda-sns", + "templateURL": "serverless-patterns/apigw-sqs-lambda-sns", + "projectFolder": "apigw-sqs-lambda-sns", + "templateFile": "webhook_sns/webhook_sns_stack.py" + } + }, + "resources": { + "bullets": [ + { + "text": "API Gateway REST API", + "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html" + }, + { + "text": "Amazon SQS", + "link": "https://docs.aws.amazon.com/sqs/latest/dg/welcome.html" + }, + { + "text": "AWS Lambda", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/welcome.html" + }, + { + "text": "Amazon SNS SMS", + "link": "https://docs.aws.amazon.com/sns/latest/dg/sms_publish-to-phone.html" + } + ] + }, + "deploy": { + "text": [ + "pip install -r requirements.txt", + "cdk bootstrap", + "cdk deploy" + ] + }, + "testing": { + "text": [ + "Send a POST request to the API Gateway endpoint:", + "curl -X POST https://your-api-id.execute-api.region.amazonaws.com/prod/ -H \"Content-Type: application/json\" -d '{\"phoneNumber\": \"+1234567890\", \"message\": \"Test message\"}'", + "Replace the phone number with a valid number in E.164 format for actual SMS delivery." + ] + }, + "cleanup": { + "text": [ + "Delete the stack: cdk destroy." + ] + }, + "authors": [ + { + "name": "Shaun Guo", + "image": "https://media.licdn.com/dms/image/C5103AQG3KMyMdEIKpA/profile-displayphoto-shrink_800_800/0/1517283953925?e=1692835200&v=beta&t=AxJ9ST_8K_bw8nqTPDaJB2F5dnQspES9FuJ64DBScC8", + "bio": "Shaun is a Senior Technical Account Manager at Amazon Web Services based in Australia", + "linkedin": "shaun-guo" + }, + { + "name": "Robbie Cooray", + "image": "https://media.licdn.com/dms/image/v2/C5603AQFK28pyKxfNiQ/profile-displayphoto-shrink_200_200/profile-displayphoto-shrink_200_200/0/1603248682998?e=1729728000&v=beta&t=YcFUfepq9JCKMvcqzaHMhTJFks6nrsfxS6v8JpolvEc", + "bio": "Robbie is a Senior Solutions Architect at Amazon Web Services based in Australia", + "linkedin": "robbiecooray" + } + ] +} diff --git a/apigw-sqs-lambda-sns/lambda/app.py b/apigw-sqs-lambda-sns/lambda/app.py new file mode 100644 index 000000000..e101bce92 --- /dev/null +++ b/apigw-sqs-lambda-sns/lambda/app.py @@ -0,0 +1,57 @@ +import json +import boto3 +import logging + +# Configure logging +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +def publish_to_sns(event_body): + """Helper function to publish message to SNS""" + sns = boto3.client('sns') + + try: + # Extract phone number and message from the event body + phone_number = event_body.get('phoneNumber') + message = event_body.get('message') + + if not phone_number or not message: + logger.error("Missing phoneNumber or message in event body") + return + + logger.info(f"Publishing message to phone number: {phone_number}") + logger.info(f"Message: {message}") + + # Publish directly to phone number (no topic needed for PoC) + response = sns.publish( + PhoneNumber=phone_number, + Message=message + ) + + logger.info(f"SNS publish response: {response}") + + except Exception as e: + logger.error(f"Error publishing to SNS: {str(e)}") + raise + +def lambda_handler(event, context): + """Main lambda handler function""" + logger.info(f"Received event: {json.dumps(event)}") + + try: + # Process each SQS record + for record in event['Records']: + event_body = json.loads(record['body']) + publish_to_sns(event_body) + + return { + 'statusCode': 200, + 'body': json.dumps('Messages processed successfully') + } + + except Exception as e: + logger.error(f"Error processing event: {str(e)}") + return { + 'statusCode': 500, + 'body': json.dumps(f'Error processing messages: {str(e)}') + } diff --git a/apigw-sqs-lambda-sns/requirements.txt b/apigw-sqs-lambda-sns/requirements.txt new file mode 100644 index 000000000..9eb8dd1aa --- /dev/null +++ b/apigw-sqs-lambda-sns/requirements.txt @@ -0,0 +1,2 @@ +aws-cdk-lib>=2.0.0 +constructs>=10.0.0 diff --git a/apigw-sqs-lambda-sns/webhook_sns/__init__.py b/apigw-sqs-lambda-sns/webhook_sns/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/apigw-sqs-lambda-sns/webhook_sns/__init__.py @@ -0,0 +1 @@ + diff --git a/apigw-sqs-lambda-sns/webhook_sns/__pycache__/__init__.cpython-310.pyc b/apigw-sqs-lambda-sns/webhook_sns/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e9e9ec9a1e3cccd315ec855adc14f50164ae9931 GIT binary patch literal 176 zcmd1j<>g`kf(O?tGZ=yNV-N=!fCL?YxR?b zXK%F)<0m~R|1b>x3Xk{%8fs7!7$KUV(41Hyp4g!?sf5)@Ev!!(VRO<9Tay;jdS=iL zJChDFJ~pUD@z(~$41Hxyy3`&UedlMeX7sDd9!u9dRc&`6h@1E)u4D`MTvhZ~oC#Ie zoxlx`shbOZH&naNgoHh0kt8BcxyOX6eK3sGSu59nKj3~A1-6{MV+ z)LL7Qu*7p4#`^8dE7XQ}^`b_d$4J&M8np5l(dycHWUuUtrfkjI@O0)~T023s4o_ok zJqD{VyCt_TuB`B5q{nh+eif`XX^XaLXN?~@ufSN}jYi+X*YvXm`bi@bV+Gd%>1)UytW+>ohjcQmSwcnp{lesuCt8hIK@%WXGF zhB+L`%#CQkxN`FG&)1=#eXJ@xjie7oDjNmt#T15G7^iaC5RrK2C_N5*F;nP`zNH*C zoia}Xz%wrZW6m~+t`|}7C;qari5Zx({KNjyr@fDJzVxOs?-i3hKa%VuhkkG2%UN&y z`M7VY7TA=UWBC>vlnIafiQblKgZ0AHy*OaYT~I5iICMjI8Arglp%;g0XM85cETW%5 zV%!sU6bHU{zU%;1-zkZ8I)#MTLP( z9ggoWZE3V1;o_NMbFHr22}MSCFKB^tbz;93GIj$lD5kpm|cC@;D@b7bdwrCv`61M5RJ)yOa_c zXBp1XbFM7?J?P{xgi|<2oO15ugtGVV-aY(Mb@jsXheGWX94-rmY6G}4=8v<_383~V z4IpB`AtY2`%@EZc=LI_Ae&qRy8>n_3#D}p5K#!!Vo#}&xxLng~AwGS^O3iaGh6M1g z-rsg<>M$>-x>AyX;|v!H^Ej3e6l|s_fBUlx1{)1t)%GmF?#4TG`!$6H3#PmYlf0!h zoYRHNPlU3w5LKs0#wor!|=h6fpneR&zeE)B+e!1K&4(ZKRV7TD{ToZs3 znmvT5Iw4_9(?Iu|gq)>rkk4>^1%qQy@@&VdRb<#>K>%EO0JC}X58ZQhqi+vzT{DEf zpH+<;W+pl4Z{@cnkDhW<>*-qu*H<^L6*F3LTle19jlTTwece=a^O0^ehH~Q7pI&4{ g7GdV16wL#zsiRTVzvLTx53VwX&{l67&H9e@UoV|VU;qFB literal 0 HcmV?d00001 diff --git a/apigw-sqs-lambda-sns/webhook_sns/webhook_sns_stack.py b/apigw-sqs-lambda-sns/webhook_sns/webhook_sns_stack.py new file mode 100644 index 000000000..899fe3b5e --- /dev/null +++ b/apigw-sqs-lambda-sns/webhook_sns/webhook_sns_stack.py @@ -0,0 +1,119 @@ +from aws_cdk import ( + Stack, + aws_apigateway as apigateway, + aws_sqs as sqs, + aws_lambda as _lambda, + aws_iam as iam, + aws_lambda_event_sources as lambda_event_sources, + CfnOutput, + Duration +) +from constructs import Construct + + +class WebhookSnsStack(Stack): + + def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: + super().__init__(scope, construct_id, **kwargs) + + # Create SQS Queue + queue = sqs.Queue( + self, "WebhookQueue", + queue_name="webhook-queue" + ) + + # Create Lambda function + lambda_function = _lambda.Function( + self, "EventProcessingFunction", + runtime=_lambda.Runtime.PYTHON_3_9, + handler="app.lambda_handler", + code=_lambda.Code.from_asset("lambda"), + timeout=Duration.seconds(30) + ) + + # Grant Lambda permission to publish to SNS + lambda_function.add_to_role_policy( + iam.PolicyStatement( + effect=iam.Effect.ALLOW, + actions=["sns:Publish"], + resources=["*"] + ) + ) + + # Add SQS event source to Lambda + lambda_function.add_event_source( + lambda_event_sources.SqsEventSource(queue) + ) + + # Create API Gateway + api = apigateway.RestApi( + self, "WebhookApi", + rest_api_name="webhook-api", + description="API Gateway for webhook integration with SQS" + ) + + # Create IAM role for API Gateway to send messages to SQS + api_role = iam.Role( + self, "ApiGatewayRole", + assumed_by=iam.ServicePrincipal("apigateway.amazonaws.com"), + inline_policies={ + "SqsSendMessagePolicy": iam.PolicyDocument( + statements=[ + iam.PolicyStatement( + effect=iam.Effect.ALLOW, + actions=["sqs:SendMessage"], + resources=[queue.queue_arn] + ) + ] + ) + } + ) + + # Create API Gateway integration with SQS + integration = apigateway.AwsIntegration( + service="sqs", + path=f"{self.account}/{queue.queue_name}", + integration_http_method="POST", + options=apigateway.IntegrationOptions( + credentials_role=api_role, + request_parameters={ + "integration.request.header.Content-Type": "'application/x-www-form-urlencoded'" + }, + request_templates={ + "application/json": "Action=SendMessage&MessageBody=$util.urlEncode($input.body)" + }, + integration_responses=[ + apigateway.IntegrationResponse( + status_code="200" + ) + ] + ) + ) + + # Add POST method to root resource + api.root.add_method( + "POST", + integration, + method_responses=[ + apigateway.MethodResponse(status_code="200") + ] + ) + + # Outputs + CfnOutput( + self, "ApiEndpoint", + value=api.url, + description="API Gateway endpoint URL" + ) + + CfnOutput( + self, "QueueName", + value=queue.queue_name, + description="SQS Queue name" + ) + + CfnOutput( + self, "QueueUrl", + value=queue.queue_url, + description="SQS Queue URL" + ) From 5118ffcb8426391dfe91352ea27007c99225b94e Mon Sep 17 00:00:00 2001 From: Shaun Guo Date: Sat, 19 Jul 2025 15:08:23 +1000 Subject: [PATCH 2/3] Remove unwatned files and add to .gitignore --- apigw-sqs-lambda-sns/.gitignore | 12 ++++++++++++ .../__pycache__/__init__.cpython-310.pyc | Bin 176 -> 0 bytes .../webhook_sns_stack.cpython-310.pyc | Bin 2654 -> 0 bytes 3 files changed, 12 insertions(+) delete mode 100644 apigw-sqs-lambda-sns/webhook_sns/__pycache__/__init__.cpython-310.pyc delete mode 100644 apigw-sqs-lambda-sns/webhook_sns/__pycache__/webhook_sns_stack.cpython-310.pyc diff --git a/apigw-sqs-lambda-sns/.gitignore b/apigw-sqs-lambda-sns/.gitignore index 60b3efa1a..ef1d024df 100644 --- a/apigw-sqs-lambda-sns/.gitignore +++ b/apigw-sqs-lambda-sns/.gitignore @@ -3,6 +3,18 @@ *.d.ts node_modules +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +.venv/ +pip-log.txt +pip-delete-this-directory.txt + # CDK asset staging directory .cdk.staging cdk.out diff --git a/apigw-sqs-lambda-sns/webhook_sns/__pycache__/__init__.cpython-310.pyc b/apigw-sqs-lambda-sns/webhook_sns/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index e9e9ec9a1e3cccd315ec855adc14f50164ae9931..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 176 zcmd1j<>g`kf(O?tGZ=yNV-N=!fCL?YxR?b zXK%F)<0m~R|1b>x3Xk{%8fs7!7$KUV(41Hyp4g!?sf5)@Ev!!(VRO<9Tay;jdS=iL zJChDFJ~pUD@z(~$41Hxyy3`&UedlMeX7sDd9!u9dRc&`6h@1E)u4D`MTvhZ~oC#Ie zoxlx`shbOZH&naNgoHh0kt8BcxyOX6eK3sGSu59nKj3~A1-6{MV+ z)LL7Qu*7p4#`^8dE7XQ}^`b_d$4J&M8np5l(dycHWUuUtrfkjI@O0)~T023s4o_ok zJqD{VyCt_TuB`B5q{nh+eif`XX^XaLXN?~@ufSN}jYi+X*YvXm`bi@bV+Gd%>1)UytW+>ohjcQmSwcnp{lesuCt8hIK@%WXGF zhB+L`%#CQkxN`FG&)1=#eXJ@xjie7oDjNmt#T15G7^iaC5RrK2C_N5*F;nP`zNH*C zoia}Xz%wrZW6m~+t`|}7C;qari5Zx({KNjyr@fDJzVxOs?-i3hKa%VuhkkG2%UN&y z`M7VY7TA=UWBC>vlnIafiQblKgZ0AHy*OaYT~I5iICMjI8Arglp%;g0XM85cETW%5 zV%!sU6bHU{zU%;1-zkZ8I)#MTLP( z9ggoWZE3V1;o_NMbFHr22}MSCFKB^tbz;93GIj$lD5kpm|cC@;D@b7bdwrCv`61M5RJ)yOa_c zXBp1XbFM7?J?P{xgi|<2oO15ugtGVV-aY(Mb@jsXheGWX94-rmY6G}4=8v<_383~V z4IpB`AtY2`%@EZc=LI_Ae&qRy8>n_3#D}p5K#!!Vo#}&xxLng~AwGS^O3iaGh6M1g z-rsg<>M$>-x>AyX;|v!H^Ej3e6l|s_fBUlx1{)1t)%GmF?#4TG`!$6H3#PmYlf0!h zoYRHNPlU3w5LKs0#wor!|=h6fpneR&zeE)B+e!1K&4(ZKRV7TD{ToZs3 znmvT5Iw4_9(?Iu|gq)>rkk4>^1%qQy@@&VdRb<#>K>%EO0JC}X58ZQhqi+vzT{DEf zpH+<;W+pl4Z{@cnkDhW<>*-qu*H<^L6*F3LTle19jlTTwece=a^O0^ehH~Q7pI&4{ g7GdV16wL#zsiRTVzvLTx53VwX&{l67&H9e@UoV|VU;qFB From 9b43970f9c357f87b57c8c0ce69b9e39e7978f2e Mon Sep 17 00:00:00 2001 From: Shaun Guo Date: Tue, 22 Jul 2025 10:13:01 +1000 Subject: [PATCH 3/3] Added API key for basic security --- apigw-sqs-lambda-sns/README.md | 20 +++++++++++-- .../webhook_sns/webhook_sns_stack.py | 30 +++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/apigw-sqs-lambda-sns/README.md b/apigw-sqs-lambda-sns/README.md index e5bd11af0..a806906ad 100644 --- a/apigw-sqs-lambda-sns/README.md +++ b/apigw-sqs-lambda-sns/README.md @@ -64,12 +64,26 @@ aws sns verify-sms-sandbox-phone-number --phone-number "+your-phone-number" --on ### API Testing -Send a POST request to the API Gateway endpoint with the following JSON payload: +The API is protected with an API key. After deployment, you'll need to retrieve the API key value before testing. -Example using curl (update URL with your own API domain and phoneNumber with your phone number including country code e.g. +1234567890): +#### Get the API Key Value + +1. **Note the API Key ID from the deployment output**, then get the actual key value: +```bash +aws apigateway get-api-key --api-key YOUR_API_KEY_ID --include-value +``` + +2. **Copy the `value` field from the response** - this is your actual API key. + +#### Send Test Request + +Send a POST request to the API Gateway endpoint with the API key header: + +Example using curl (update URL with your own API domain, API key, and phoneNumber with your phone number including country code e.g. +1234567890): ```bash curl -X POST https://your-api-id.execute-api.region.amazonaws.com/prod/ \ -H "Content-Type: application/json" \ + -H "x-api-key: YOUR_ACTUAL_API_KEY_VALUE" \ -d '{"phoneNumber": "+your-phone-number", "message": "Hello from webhook!"}' ``` @@ -91,7 +105,7 @@ Expected response: } ``` -You should also received an SMS on your mobile with the following message: +You should also receive an SMS on your mobile with the following message: ``` Hello from webhook! ``` diff --git a/apigw-sqs-lambda-sns/webhook_sns/webhook_sns_stack.py b/apigw-sqs-lambda-sns/webhook_sns/webhook_sns_stack.py index 899fe3b5e..0e1cdb2dc 100644 --- a/apigw-sqs-lambda-sns/webhook_sns/webhook_sns_stack.py +++ b/apigw-sqs-lambda-sns/webhook_sns/webhook_sns_stack.py @@ -94,11 +94,35 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: api.root.add_method( "POST", integration, + api_key_required=True, method_responses=[ apigateway.MethodResponse(status_code="200") ] ) + # Create API Key + api_key = apigateway.ApiKey( + self, "WebhookApiKey", + enabled=True, + description="API Key for webhook endpoint" + ) + + # Create Usage Plan + usage_plan = apigateway.UsagePlan( + self, "WebhookUsagePlan", + name="webhook-usage-plan", + description="Usage plan for webhook API", + api_stages=[ + apigateway.UsagePlanPerApiStage( + api=api, + stage=api.deployment_stage + ) + ] + ) + + # Associate API Key with Usage Plan + usage_plan.add_api_key(api_key) + # Outputs CfnOutput( self, "ApiEndpoint", @@ -117,3 +141,9 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: value=queue.queue_url, description="SQS Queue URL" ) + + CfnOutput( + self, "ApiKeyId", + value=api_key.key_id, + description="API Key ID for webhook endpoint" + )