From ba0a1098d2ffe8b9cfd9fa407a996a4a5c4abab4 Mon Sep 17 00:00:00 2001 From: Shaun Guo Date: Wed, 12 Mar 2025 10:07:40 +1100 Subject: [PATCH 1/2] Create pattern --- cloudfront-apigw-large-uploads/README.md | 129 +++++++++++++++++ cloudfront-apigw-large-uploads/app.py | 133 ++++++++++++++++++ cloudfront-apigw-large-uploads/cdk.json | 26 ++++ .../cloudfront-apigw-large-uploads.json | 44 ++++++ cloudfront-apigw-large-uploads/lambda.mjs | 27 ++++ cloudfront-apigw-large-uploads/lambda.zip | Bin 0 -> 703 bytes .../largeFile.example | Bin 0 -> 10240 bytes .../requirements.txt | 1 + .../smallFile.example | 1 + 9 files changed, 361 insertions(+) create mode 100644 cloudfront-apigw-large-uploads/README.md create mode 100644 cloudfront-apigw-large-uploads/app.py create mode 100644 cloudfront-apigw-large-uploads/cdk.json create mode 100644 cloudfront-apigw-large-uploads/cloudfront-apigw-large-uploads.json create mode 100644 cloudfront-apigw-large-uploads/lambda.mjs create mode 100644 cloudfront-apigw-large-uploads/lambda.zip create mode 100644 cloudfront-apigw-large-uploads/largeFile.example create mode 100644 cloudfront-apigw-large-uploads/requirements.txt create mode 100644 cloudfront-apigw-large-uploads/smallFile.example diff --git a/cloudfront-apigw-large-uploads/README.md b/cloudfront-apigw-large-uploads/README.md new file mode 100644 index 000000000..f5fd7e325 --- /dev/null +++ b/cloudfront-apigw-large-uploads/README.md @@ -0,0 +1,129 @@ +# Amazon Cloudfront to APIGW routing to alternative origin for large payloads +API Gateway has a [payload limit of 10mb](https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html#http-api-quotas) which cannot be increaed. This pattern shows how to use Lambda@Edge to route client POST request to an alternative origin based on payload size of the POST request or URL. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/cloudfront-apigw-large-uploads + +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. + +## 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) +* [AWS Cloud Development Kit](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) (AWS CDK) installed + +## Deployment Instructions + +1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: + ``` + git clone https://github.com/aws-samples/serverless-patterns + ``` +1. Change directory to the pattern directory: + ``` + cd cloudfront-apigw-large-uploads + ``` +1. From the command line, use AWS CDK to deploy the AWS resources for the pattern as specified in the `large_uploads_test/test_stack.py` file. + ``` + python3 -m pip install -r requirements.txt + cdk synth + cdk deploy + ``` + +## How it works +A Cloudfront distribution is created with 2 origins: +* Default Origin - API Gateway with a Lambda Integration that returns stub responses based on HTTP request method. +* Custom Origin - A mock HTTP endpoint which sends a response with information from the request. We are using echo.free.beeceptor.com but you can substitue this with other endpoint URLs. + +When a HTTP POST request is handled by CloudFront, a Lambda@Edge function is invoked as an Origin Request. This Lambda@Edge function uses the method and `ceontent-length` HTTP headers from the client request to determine whether the request should go to a custom origin. + + +## Testing + +1. Set region to us-east-1 for your CDK environment, this is because Lambda@edge functions need to exist us-east-1 region. +1. cdk bootstrap +1. cdk deploy +1. Once deployed, find the CloudFront distribution URL and use it in the following `curl` commands + +#### Test 1: +A GET request that CloudFront will route to API gateway + +`curl https:// -i` + +Example output: +``` + HTTP/2 200 + content-type: application/json + ...more headers... + + {"message": "GET request processed by API Gateway!"} +``` + +#### Test 2: +A POST request that CloudFront will route to API gateway as the payload size is smaller than `MAX_FILE_SIZE` defined in lambda.mjs. + +`curl https:// -i -X POST -F 'data=@smallFile.example' -H 'content-type: appli +cation/json'` + +Example output: +``` + HTTP/2 201 + content-type: application/json + ...more headers... + + {"message": "POST request processed by API Gateway!"} +``` + +#### Test 3: +A POST request that CloudFront will route to a custom origin as the payload size is larger than `MAX_FILE_SIZE` defined in `lambda.mjs`. + +`curl https:// -i -X POST -F 'data=@largeFile.example` + +Example output: +``` + HTTP/2 200 + content-type: application/json + ...more headers... + + { + "method": "POST", + "protocol": "https", + "host": "echo.free.beeceptor.com", + ...more response body +``` + + +#### Test 4: +A POST request to a specific URL that CloudFront will route to a custom origin based on the Behaviour defined in `app.py`. + +`curl https:///upload/ -i -X POST -F 'data=@smallFile.example` + +Example output: +``` + HTTP/2 200 + content-type: application/json + ...more headers... + + { + "method": "POST", + "protocol": "https", + "host": "echo.free.beeceptor.com", + ...more response body +``` + + +## Cleanup + +1. Delete the stack + ``` + cdk destroy + ``` + + +## Notes +There are restrictions to what the Lambda function can modify as part of the Origin Request. For example, CloudFront will not allow you to change the protocol from HTTPS to HTTP. Doing so will result in an origin configuration error from CloudFront. + + +---- +Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/cloudfront-apigw-large-uploads/app.py b/cloudfront-apigw-large-uploads/app.py new file mode 100644 index 000000000..e5319bcd3 --- /dev/null +++ b/cloudfront-apigw-large-uploads/app.py @@ -0,0 +1,133 @@ +import aws_cdk as cdk +from aws_cdk import aws_cloudfront as cloudfront +from aws_cdk import aws_cloudfront_origins as origins +from aws_cdk import aws_apigateway as apigw +from aws_cdk import aws_lambda as lambda_ +from aws_cdk import aws_iam as iam + +class CloudFrontApigwLargeUploadsStack(cdk.Stack): + def __init__(self, scope, construct_id, **kwargs): + super().__init__(scope, construct_id, **kwargs) + + # Create an API Gateway HTTP endpoint that will return a mock response + nonUploadApi = apigw.RestApi( + self, + "nonUploadApi", + endpoint_types=[apigw.EndpointType.REGIONAL], + deploy_options=apigw.StageOptions( + stage_name="mock", + throttling_rate_limit=100, + throttling_burst_limit=1000, + ), + cloud_watch_role=True, + deploy=True, + ) + + # Create a mock integration for the nonUploadApi / path that returns status 200 for GET requests + nonUploadApi.root.add_method( + "GET", + apigw.MockIntegration( + integration_responses=[ + apigw.IntegrationResponse( + status_code="200", + response_templates={ + "application/json": '{"message": "GET request processed by API Gateway!"}' + }, + ) + ], + passthrough_behavior=apigw.PassthroughBehavior.NEVER, + request_templates={"application/json": '{"statusCode": 200}'}, + ), + method_responses=[apigw.MethodResponse(status_code="200")], + ) + + # Create a mock integration for the nonUploadApi / path that returns status 200 for POST requests + nonUploadApi.root.add_method( + "POST", + apigw.MockIntegration( + integration_responses=[ + apigw.IntegrationResponse( + status_code="201", + response_templates={ + "application/json": '{"message": "POST request processed by API Gateway!"}' + }, + ) + ], + passthrough_behavior=apigw.PassthroughBehavior.NEVER, + request_templates={"application/json": '{"statusCode": 200}'}, + ), + method_responses=[apigw.MethodResponse(status_code="201")], + ) + + # Create Lamdba@edge function + edge_function = lambda_.Function( + self, + "OriginRequestFunction", + runtime=lambda_.Runtime.NODEJS_LATEST, + handler="lambda.handler", + code=lambda_.Code.from_asset("lambda.zip") + ) + + # Add Lambda@Edge permission + edge_function.add_permission( + "EdgeFunctionPermission", + principal=iam.ServicePrincipal("edgelambda.amazonaws.com"), + action="lambda:InvokeFunction" + ) + + # Add execution role permissions for Lambda@Edge + edge_function.role.add_to_policy( + iam.PolicyStatement( + effect=iam.Effect.ALLOW, + actions=[ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + resources=["arn:aws:logs:*:*:*"] + ) + ) + + # Create a CloudFront distribution with custom origin + distribution = cloudfront.Distribution( + self, + "testLargeUploadDistribution", + price_class=cloudfront.PriceClass.PRICE_CLASS_100, # Change this if you need more regions enabled in CloudFront + # Default behavior now points to API Gateway + default_behavior=cloudfront.BehaviorOptions( + origin=origins.RestApiOrigin( + nonUploadApi, + origin_path="/mock" + ), + edge_lambdas=[ + cloudfront.EdgeLambda( + function_version=edge_function.current_version, + event_type=cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST + ) + ], + viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.HTTPS_ONLY, + origin_request_policy=cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER, + cache_policy=cloudfront.CachePolicy.CACHING_DISABLED, + allowed_methods=cloudfront.AllowedMethods.ALLOW_ALL, + ), + # Additional behavior for /upload/* paths to hit a test endpoint httpbin which will provide a generic response. + additional_behaviors={ + "/upload/*": cloudfront.BehaviorOptions( + origin=origins.HttpOrigin( + "echo.free.beeceptor.com", # This is the URL of a mock file server. + protocol_policy=cloudfront.OriginProtocolPolicy.HTTPS_ONLY, + https_port=443, + origin_path="/" # The origin path is set to / for the example endpoint. It will need to be modified to the correct URI for real world use-cases. + ), + viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.HTTPS_ONLY, + cache_policy=cloudfront.CachePolicy.CACHING_DISABLED, + allowed_methods=cloudfront.AllowedMethods.ALLOW_ALL, # Allow all HTTP methods for uploads + origin_request_policy=cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER # Forward all headers and query strings + ) + }, + comment="Distribution to test alternative origin for large uploads" + ) + +app = cdk.App() +CloudFrontApigwLargeUploadsStack(app, "CloudFrontApiGatewayLargeUploads") +app.synth() diff --git a/cloudfront-apigw-large-uploads/cdk.json b/cloudfront-apigw-large-uploads/cdk.json new file mode 100644 index 000000000..5be9dde5e --- /dev/null +++ b/cloudfront-apigw-large-uploads/cdk.json @@ -0,0 +1,26 @@ +{ + "app": "python3 app.py", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "requirements*.txt", + "**/__init__.py", + "python/__pycache__", + "tests" + ] + }, + "context": { + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true + } +} \ No newline at end of file diff --git a/cloudfront-apigw-large-uploads/cloudfront-apigw-large-uploads.json b/cloudfront-apigw-large-uploads/cloudfront-apigw-large-uploads.json new file mode 100644 index 000000000..d23e04654 --- /dev/null +++ b/cloudfront-apigw-large-uploads/cloudfront-apigw-large-uploads.json @@ -0,0 +1,44 @@ +{ + "title": "Amazon Cloudfront to APIGW routing to alternative origin for large payloads", + "description": "Amazon Cloudfront to APIGW routing to alternative origin for large payloads using Lambda@Edge", + "language": "Python", + "level": "200", + "framework": "CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern shows how to use Lambda@Edge to route client POST request to an alternative origin when a POST request exceeds the 10mb payload limit for API Gateway." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/cloudfront-apigw-large-uploads", + "templateURL": "serverless-patterns/cloudfront-apigw-large-uploads", + "projectFolder": "cloudfront-apigw-large-uploads", + "templateFile": "large_uploads_test/test_stack.py" + } + }, + "deploy": { + "text": [ + "cdk deploy" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "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" + } + ] +} \ No newline at end of file diff --git a/cloudfront-apigw-large-uploads/lambda.mjs b/cloudfront-apigw-large-uploads/lambda.mjs new file mode 100644 index 000000000..d4238b0a1 --- /dev/null +++ b/cloudfront-apigw-large-uploads/lambda.mjs @@ -0,0 +1,27 @@ +// This is a copy of the file found in lambda.zip. + +// Using a small value of 300 bytes to demonstrate the behaviour of Lambda@Edge function. +// In real world applications, you would set this higher. +const MAX_FILE_SIZE = 300; + +// Using a random public facing endpoint that will respond to requests. +const LARGE_UPLOAD_ORIGIN = "echo.free.beeceptor.com"; + +export const handler = async (event) => { + const request = event.Records[0].cf.request; + const headers = request.headers; + const origin = event.Records[0].cf.request.origin; + + if (request.method === 'POST') { + if (headers['content-length'] && parseInt(headers['content-length'][0].value) < MAX_FILE_SIZE ) { + console.log(`Request size less than: ` + MAX_FILE_SIZE); + } else { + console.log(`Request has no content-length or request size is greater than: ` + MAX_FILE_SIZE); + origin.custom.domainName = LARGE_UPLOAD_ORIGIN; + request.headers.host[0].value = LARGE_UPLOAD_ORIGIN; + request.uri = '/'; + } + } + + return request; +}; \ No newline at end of file diff --git a/cloudfront-apigw-large-uploads/lambda.zip b/cloudfront-apigw-large-uploads/lambda.zip new file mode 100644 index 0000000000000000000000000000000000000000..3bc3085c620915a1b12b49739e89f6a4f929d80a GIT binary patch literal 703 zcmWIWW@Zs#U|`^2XerH(O8(a%?8(HyP{qo?zy%b|Nz6@3Nz}{DD&8CHn}69sV6XeB zdWo+WOq_C*3J~*iTq zfotxE8o|%zi7|d_E-X5vf9=8aDbMFky^_i5-B@>{#%EKWzA)-aG>~w3UQ<oMTPCf zdtUuGbnbKYx`5?+uVW5>G}PI-(c;>>!Vt?lUh`rrN)=A;4LYm*_tLDX_xB#UQ1!6# z=M~*|PhYYX%ui!pp<}06cuY5=srFEu((dbPwW<#;pZfjGFa3?uns$*bePykUt YfJlG=-mGjO5k?@i0MdoPWXiw*0Ib+M!2kdN literal 0 HcmV?d00001 diff --git a/cloudfront-apigw-large-uploads/largeFile.example b/cloudfront-apigw-large-uploads/largeFile.example new file mode 100644 index 0000000000000000000000000000000000000000..ad5d9616ffa605a54b0b409d0d4684d93dabf1ee GIT binary patch literal 10240 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 Date: Wed, 12 Mar 2025 10:47:57 +1100 Subject: [PATCH 2/2] Add diagram --- cloudfront-apigw-large-uploads/README.md | 1 + .../cloudfront-apigw-large-uploads.json | 2 +- cloudfront-apigw-large-uploads/diagram.png | Bin 0 -> 36225 bytes 3 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 cloudfront-apigw-large-uploads/diagram.png diff --git a/cloudfront-apigw-large-uploads/README.md b/cloudfront-apigw-large-uploads/README.md index f5fd7e325..ef1f61038 100644 --- a/cloudfront-apigw-large-uploads/README.md +++ b/cloudfront-apigw-large-uploads/README.md @@ -35,6 +35,7 @@ A Cloudfront distribution is created with 2 origins: * Custom Origin - A mock HTTP endpoint which sends a response with information from the request. We are using echo.free.beeceptor.com but you can substitue this with other endpoint URLs. When a HTTP POST request is handled by CloudFront, a Lambda@Edge function is invoked as an Origin Request. This Lambda@Edge function uses the method and `ceontent-length` HTTP headers from the client request to determine whether the request should go to a custom origin. +![diagram](diagram.png) ## Testing diff --git a/cloudfront-apigw-large-uploads/cloudfront-apigw-large-uploads.json b/cloudfront-apigw-large-uploads/cloudfront-apigw-large-uploads.json index d23e04654..877ee7e50 100644 --- a/cloudfront-apigw-large-uploads/cloudfront-apigw-large-uploads.json +++ b/cloudfront-apigw-large-uploads/cloudfront-apigw-large-uploads.json @@ -15,7 +15,7 @@ "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/cloudfront-apigw-large-uploads", "templateURL": "serverless-patterns/cloudfront-apigw-large-uploads", "projectFolder": "cloudfront-apigw-large-uploads", - "templateFile": "large_uploads_test/test_stack.py" + "templateFile": "app.py" } }, "deploy": { diff --git a/cloudfront-apigw-large-uploads/diagram.png b/cloudfront-apigw-large-uploads/diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..5804403e61a92caf858169fa6aad7d25426aad80 GIT binary patch literal 36225 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 literal 0 HcmV?d00001