Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions apigw-python-cdk-lambda-snapstart/CarHandler/handler.py
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
72 changes: 72 additions & 0 deletions apigw-python-cdk-lambda-snapstart/README.md
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/
Copy link
Author

Choose a reason for hiding this comment

The 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!

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Author

Choose a reason for hiding this comment

The 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>"
```
68 changes: 68 additions & 0 deletions apigw-python-cdk-lambda-snapstart/app.py
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()
15 changes: 15 additions & 0 deletions apigw-python-cdk-lambda-snapstart/cdk.json
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"
]
}
}
73 changes: 73 additions & 0 deletions apigw-python-cdk-lambda-snapstart/example-pattern.json
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": ""
}
]
}
2 changes: 2 additions & 0 deletions apigw-python-cdk-lambda-snapstart/requirements.txt
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