-
Notifications
You must be signed in to change notification settings - Fork 1k
New serverless pattern - apigw-python-cdk-lambda-snapstart #2957
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
78303f0
54d5a55
a275c99
cd74c2f
23d3967
91b3a2f
1f171a0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| import json | ||
| import os | ||
| import uuid | ||
| from decimal import Decimal | ||
| from typing import Any | ||
|
|
||
| import boto3 | ||
| from botocore.exceptions import ClientError | ||
|
|
||
| from aws_lambda_powertools import Logger | ||
| from aws_lambda_powertools.event_handler import ( | ||
| APIGatewayRestResolver, | ||
| Response, | ||
| content_types, | ||
| ) | ||
| from aws_lambda_powertools.event_handler.exceptions import BadRequestError, NotFoundError | ||
| from aws_lambda_powertools.logging import correlation_paths | ||
| from aws_lambda_powertools.utilities.typing import LambdaContext | ||
|
|
||
| logger = Logger(level=os.getenv("LOG_LEVEL", "INFO")) | ||
| app = APIGatewayRestResolver() | ||
|
|
||
| table_name = os.environ["CAR_TABLE_NAME"] | ||
| dynamodb = boto3.resource("dynamodb") | ||
| table = dynamodb.Table(table_name) | ||
|
|
||
| def _json_default(value: Any) -> Any: | ||
| if isinstance(value, Decimal): | ||
| if value % 1 == 0: | ||
| return int(value) | ||
| return float(value) | ||
| raise TypeError(f"Object of type {type(value)} is not JSON serializable") | ||
|
|
||
|
|
||
| def _json_body() -> dict: | ||
| """Parse request body as JSON object; empty or missing body returns {}.""" | ||
| raw = app.current_event.json_body | ||
| if raw is None: | ||
| return {} | ||
| if not isinstance(raw, dict): | ||
| raise BadRequestError("Request body must be a JSON object") | ||
| return raw | ||
|
|
||
|
|
||
| @app.post("/cars") | ||
| def create_car() -> Response: | ||
| body = _json_body() | ||
| car_id = str(uuid.uuid4()) | ||
| car = { | ||
| "id": car_id, | ||
| "make": body.get("make"), | ||
| "model": body.get("model"), | ||
| "year": body.get("year"), | ||
| "color": body.get("color"), | ||
| } | ||
| table.put_item(Item=car) | ||
| return Response( | ||
| status_code=201, | ||
| content_type=content_types.APPLICATION_JSON, | ||
| body=json.dumps(car, default=_json_default), | ||
| ) | ||
|
|
||
|
|
||
| @app.get("/cars/<car_id>") | ||
| def get_car(car_id: str) -> Response: | ||
| item = table.get_item(Key={"id": car_id}).get("Item") | ||
| if not item: | ||
| raise NotFoundError(f"Car with id {car_id} not found") | ||
| return Response( | ||
| status_code=200, | ||
| content_type=content_types.APPLICATION_JSON, | ||
| body=json.dumps(item, default=_json_default), | ||
| ) | ||
|
|
||
|
|
||
| @app.put("/cars/<car_id>") | ||
| def update_car(car_id: str) -> Response: | ||
| body = _json_body() | ||
| existing = table.get_item(Key={"id": car_id}).get("Item") | ||
| if not existing: | ||
| raise NotFoundError(f"Car with id {car_id} not found") | ||
| updated = { | ||
| "id": car_id, | ||
| "make": body.get("make", existing.get("make")), | ||
| "model": body.get("model", existing.get("model")), | ||
| "year": body.get("year", existing.get("year")), | ||
| "color": body.get("color", existing.get("color")), | ||
| } | ||
| table.put_item(Item=updated) | ||
| return Response( | ||
| status_code=200, | ||
| content_type=content_types.APPLICATION_JSON, | ||
| body=json.dumps(updated, default=_json_default), | ||
| ) | ||
|
|
||
|
|
||
| @app.delete("/cars/<car_id>") | ||
| def delete_car(car_id: str) -> Response: | ||
| try: | ||
| table.delete_item( | ||
| Key={"id": car_id}, | ||
| ConditionExpression="attribute_exists(id)", | ||
| ) | ||
| except ClientError as exc: | ||
| if exc.response["Error"]["Code"] == "ConditionalCheckFailedException": | ||
| raise NotFoundError(f"Car with id {car_id} not found") from exc | ||
| raise | ||
| return Response(status_code=204, body="") | ||
|
|
||
|
|
||
| @app.exception_handler(NotFoundError) | ||
| def handle_not_found(exc: NotFoundError) -> Response: | ||
| return Response( | ||
| status_code=404, | ||
| content_type=content_types.APPLICATION_JSON, | ||
| body=json.dumps({"message": str(exc)}), | ||
| ) | ||
|
|
||
|
|
||
| @app.exception_handler(BadRequestError) | ||
| def handle_bad_request(exc: BadRequestError) -> Response: | ||
| return Response( | ||
| status_code=400, | ||
| content_type=content_types.APPLICATION_JSON, | ||
| body=json.dumps({"message": str(exc)}), | ||
| ) | ||
|
|
||
|
|
||
| @app.not_found | ||
| def handle_route_not_found(_exc: Exception) -> Response: | ||
| return Response( | ||
| status_code=404, | ||
| content_type=content_types.APPLICATION_JSON, | ||
| body=json.dumps({"message": "Route not found"}), | ||
| ) | ||
|
|
||
|
|
||
| @logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) | ||
| def handler(event: dict, context: LambdaContext) -> dict: | ||
| return app.resolve(event, context) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| aws-lambda-powertools==3.25.0 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| # Amazon API Gateway + AWS Lambda SnapStart + Amazon DynamoDB | ||
|
|
||
| This pattern demonstrates how to create a REST API using Amazon API Gateway, AWS Lambda and Amazon DynamoDB. | ||
| It's built with [Python 3.12](https://www.python.org/downloads/release/python-3128/), together with | ||
| [AWS Cloud Development Kit (CDK)](https://docs.aws.amazon.com/cdk/v2/guide/work-with-cdk-python.html) as the Infrastructure as Code solution. This pattern also implements the usage of [AWS Lambda SnapStart](https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html) | ||
| to improve initialization performance of the Lambda function. | ||
|
|
||
| ## Architecture | ||
|
|
||
| - API Gateway REST API (`prod` stage) | ||
| - AWS Lambda functon(Python 3.12) | ||
| - Lambda SnapStart enabled on published versions | ||
| - Lambda `live` alias integrated with API Gateway | ||
| - DynamoDB table with partition key `id` | ||
|
|
||
| ## Endpoints | ||
|
|
||
| - `POST /cars` | ||
| - `GET /cars/{carId}` | ||
| - `PUT /cars/{carId}` | ||
| - `DELETE /cars/{carId}` | ||
|
|
||
| ## Requirements | ||
|
|
||
| - Python 3.12+ | ||
| - AWS CDK v2 | ||
| - AWS credentials configured | ||
|
|
||
| ## Deploy | ||
|
|
||
| To deploy this stack, run the following commands from the root of the `serverless-patterns` repository: | ||
|
|
||
| ```bash | ||
| # Move to the pattern directory and create a Python virtual environment | ||
| cd apigw-python-cdk-lambda-snapstart | ||
| python3 -m venv .venv | ||
| source .venv/bin/activate | ||
|
|
||
| # Install the AWS CDK for Python | ||
| pip3 install -r requirements.txt | ||
|
|
||
| # Install AWS Lambda Powertools library for the CarHandler Lambda | ||
| pip3 install -r CarHandler/requirements.txt -t CarHandler/ | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hello @ellisms thank you very much for reviewing my PR, sir! one question for you please - i've put the PIP command here in instructions to install aws-lambda-powertools as you've suggested. what do you think is better - keeping the installation procedure of the library like this, or creating a Lambda layer inside the CDK code? thank you in advance!
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think either is ok. In either case, I would lock in the version number so that the user is confident they will get a known-working configuration.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. understood, then i'd like to keep it as is right now. the version number is locked, so this part of the PR should be good to go. thanks! |
||
|
|
||
| # Bootstrap your environment and deploy | ||
| cdk bootstrap | ||
| cdk deploy | ||
| ``` | ||
|
|
||
| ## Test | ||
|
|
||
| Get endpoint URL from stack outputs (`CarEndpoint`), then run: | ||
|
|
||
| ```bash | ||
| ENDPOINT="<put-the-CarEndpoint-output-URL-here>" | ||
|
|
||
| # Create a car (use the returned "id" in the response for GET/PUT/DELETE below) | ||
| curl --location --request POST "$ENDPOINT/cars" \ | ||
| --header 'Content-Type: application/json' \ | ||
| --data-raw '{"make":"Porsche","model":"992","year":"2022","color":"White"}' | ||
|
|
||
| # Get a car by id | ||
| curl --location "$ENDPOINT/cars/<car-id>" | ||
|
|
||
| # Update a car | ||
| curl --location --request PUT "$ENDPOINT/cars/<car-id>" \ | ||
| --header 'Content-Type: application/json' \ | ||
| --data-raw '{"make":"Porsche","model":"992","year":"2023","color":"Racing Yellow"}' | ||
|
|
||
| # Delete a car | ||
| curl --location --request DELETE "$ENDPOINT/cars/<car-id>" | ||
| ``` | ||
mate329 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| #!/usr/bin/env python3 | ||
| from aws_cdk import ( | ||
| App, CfnOutput, | ||
| Duration, | ||
| Stack, | ||
| RemovalPolicy, | ||
| aws_apigateway as apigw, | ||
| aws_dynamodb as dynamodb, | ||
| aws_lambda as _lambda, | ||
| ) | ||
| from constructs import Construct | ||
|
|
||
| class CarStoreStack(Stack): | ||
| def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: | ||
| super().__init__(scope, construct_id, **kwargs) | ||
|
|
||
| car_table = dynamodb.Table( | ||
| self, | ||
| "CarTable", | ||
| partition_key=dynamodb.Attribute(name="id", type=dynamodb.AttributeType.STRING), | ||
| billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST, | ||
| removal_policy=RemovalPolicy.DESTROY, | ||
| ) | ||
|
|
||
| car_function = _lambda.Function( | ||
| self, | ||
| "CarStoreFunction", | ||
| runtime=_lambda.Runtime.PYTHON_3_12, | ||
| handler="handler.handler", | ||
| code=_lambda.Code.from_asset("CarHandler/"), | ||
| timeout=Duration.seconds(10), | ||
| snap_start=_lambda.SnapStartConf.ON_PUBLISHED_VERSIONS, | ||
| memory_size=256, | ||
| environment={ | ||
| "CAR_TABLE_NAME": car_table.table_name, | ||
| "LOG_LEVEL": "INFO", | ||
| } | ||
| ) | ||
| car_table.grant_read_write_data(car_function) | ||
|
|
||
| live_alias = _lambda.Alias( | ||
| self, | ||
| "CarStoreLiveAlias", | ||
| alias_name="live", | ||
| version=car_function.current_version, | ||
| ) | ||
|
|
||
| car_api = apigw.RestApi( | ||
| self, | ||
| "CarStoreApi", | ||
| deploy_options=apigw.StageOptions(stage_name="prod"), | ||
| ) | ||
|
|
||
| integration = apigw.LambdaIntegration(live_alias, proxy=True) | ||
| car_api.root.add_method("ANY", integration) | ||
| car_api.root.add_resource("{proxy+}").add_method("ANY", integration) | ||
|
|
||
| CfnOutput( | ||
| self, | ||
| "CarEndpoint", | ||
| description="API Gateway Car Endpoint", | ||
| value=car_api.url, | ||
| ) | ||
| CfnOutput(self, "CarTableName", value=car_table.table_name) | ||
|
|
||
| app = App() | ||
| CarStoreStack(app, "CarStoreStack") | ||
| app.synth() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| { | ||
| "app": ".venv/bin/python app.py", | ||
| "watch": { | ||
| "include": [ | ||
| "**" | ||
| ], | ||
| "exclude": [ | ||
| "README.md", | ||
| "cdk*.json", | ||
| "requirements*.txt", | ||
| "**/__pycache__", | ||
| "tests" | ||
| ] | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| { | ||
| "title": "API Gateway with Lambda SnapStart and DynamoDB using Python CDK", | ||
| "description": "This pattern demonstrates how to create a REST API using API Gateway, AWS Lambda with SnapStart, and DynamoDB. Built with Python 3.12 and AWS CDK, it implements Lambda SnapStart to improve initialization performance for faster cold starts.", | ||
| "language": "Python", | ||
| "level": "200", | ||
| "framework": "AWS CDK", | ||
| "introBox": { | ||
| "headline": "How it works", | ||
| "text": [ | ||
| "This pattern creates a REST API for managing car records using API Gateway and Lambda with SnapStart enabled.", | ||
| "The Lambda function is Python 3.12 based and includes a live alias that's integrated with API Gateway for seamless deployments.", | ||
| "Lambda SnapStart persists the initialized state of the Lambda runtime, significantly reducing cold start times for function initialization.", | ||
| "DynamoDB stores car records with a partition key of 'id', providing a scalable NoSQL database backend for the REST API." | ||
| ] | ||
| }, | ||
| "gitHub": { | ||
| "template": { | ||
| "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/apigw-python-cdk-lambda-snapstart", | ||
| "templateURL": "serverless-patterns/apigw-python-cdk-lambda-snapstart", | ||
| "projectFolder": "apigw-python-cdk-lambda-snapstart", | ||
| "templateFile": "apigw-python-cdk-lambda-snapstart/app.py" | ||
| } | ||
| }, | ||
| "resources": { | ||
| "bullets": [ | ||
| { | ||
| "text": "AWS Lambda SnapStart", | ||
| "link": "https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html" | ||
| }, | ||
| { | ||
| "text": "API Gateway REST API", | ||
| "link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html" | ||
| }, | ||
| { | ||
| "text": "AWS CDK Python Reference", | ||
| "link": "https://docs.aws.amazon.com/cdk/v2/guide/work-with-cdk-python.html" | ||
| } | ||
| ] | ||
| }, | ||
| "deploy": { | ||
| "text": [ | ||
| "python3 -m venv .venv", | ||
| "source .venv/bin/activate", | ||
| "pip install -r requirements.txt", | ||
| "cdk bootstrap", | ||
| "cdk deploy" | ||
| ] | ||
| }, | ||
| "testing": { | ||
| "text": [ | ||
| "Get the CarEndpoint from stack outputs, then test the endpoint to create a new car record:", | ||
| "curl --location --request POST \"$ENDPOINT/cars\" --header 'Content-Type: application/json' --data-raw '{\"make\":\"Porsche\",\"model\":\"992\",\"year\":\"2022\",\"color\":\"White\"}'", | ||
| "Change the endpoint and HTTP method to test other operations:", | ||
| "GET /cars/{carId} - Retrieve a car", | ||
| "PUT /cars/{carId} - Update a car", | ||
| "DELETE /cars/{carId} - Delete a car" | ||
| ] | ||
| }, | ||
| "cleanup": { | ||
| "text": [ | ||
| "Delete the stack: <code>cdk destroy</code>." | ||
| ] | ||
| }, | ||
| "authors": [ | ||
| { | ||
| "name": "Matia Rasetina", | ||
| "image": "https://media.licdn.com/dms/image/v2/C4D03AQEpZLzvymfGyA/profile-displayphoto-shrink_800_800/profile-displayphoto-shrink_800_800/0/1612951581132?e=1772668800&v=beta&t=m8AkoSUFICMRk5-Gd0hEAji0N4gFSfFGuv4lbBuXcJY", | ||
| "bio": "Senior Software Engineer @ Elixirr Digital", | ||
| "linkedin": "https://www.linkedin.com/in/matiarasetina/", | ||
| "twitter": "" | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| aws-cdk-lib>=2.170.0 | ||
| constructs>=10.0.0,<11.0.0 |
Uh oh!
There was an error while loading. Please reload this page.