Skip to content

Commit 9e3265a

Browse files
authored
Merge pull request #2957 from mate329/mate329-feature-apigw-python-cdk-lambda-snapstart
New serverless pattern - apigw-python-cdk-lambda-snapstart
2 parents abc0413 + e43e9a2 commit 9e3265a

8 files changed

Lines changed: 473 additions & 0 deletions

File tree

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import json
2+
import os
3+
import uuid
4+
from decimal import Decimal
5+
from typing import Any
6+
7+
import boto3
8+
from botocore.exceptions import ClientError
9+
10+
from aws_lambda_powertools import Logger
11+
from aws_lambda_powertools.event_handler import (
12+
APIGatewayRestResolver,
13+
Response,
14+
content_types,
15+
)
16+
from aws_lambda_powertools.event_handler.exceptions import BadRequestError, NotFoundError
17+
from aws_lambda_powertools.logging import correlation_paths
18+
from aws_lambda_powertools.utilities.typing import LambdaContext
19+
20+
logger = Logger(level=os.getenv("LOG_LEVEL", "INFO"))
21+
app = APIGatewayRestResolver()
22+
23+
table_name = os.environ["CAR_TABLE_NAME"]
24+
dynamodb = boto3.resource("dynamodb")
25+
table = dynamodb.Table(table_name)
26+
27+
def _json_default(value: Any) -> Any:
28+
if isinstance(value, Decimal):
29+
if value % 1 == 0:
30+
return int(value)
31+
return float(value)
32+
raise TypeError(f"Object of type {type(value)} is not JSON serializable")
33+
34+
35+
def _json_body() -> dict:
36+
"""Parse request body as JSON object; empty or missing body returns {}."""
37+
raw = app.current_event.json_body
38+
if raw is None:
39+
return {}
40+
if not isinstance(raw, dict):
41+
raise BadRequestError("Request body must be a JSON object")
42+
return raw
43+
44+
45+
@app.post("/cars")
46+
def create_car() -> Response:
47+
body = _json_body()
48+
car_id = str(uuid.uuid4())
49+
car = {
50+
"id": car_id,
51+
"make": body.get("make"),
52+
"model": body.get("model"),
53+
"year": body.get("year"),
54+
"color": body.get("color"),
55+
}
56+
table.put_item(Item=car)
57+
return Response(
58+
status_code=201,
59+
content_type=content_types.APPLICATION_JSON,
60+
body=json.dumps(car, default=_json_default),
61+
)
62+
63+
64+
@app.get("/cars/<car_id>")
65+
def get_car(car_id: str) -> Response:
66+
item = table.get_item(Key={"id": car_id}).get("Item")
67+
if not item:
68+
raise NotFoundError(f"Car with id {car_id} not found")
69+
return Response(
70+
status_code=200,
71+
content_type=content_types.APPLICATION_JSON,
72+
body=json.dumps(item, default=_json_default),
73+
)
74+
75+
76+
@app.put("/cars/<car_id>")
77+
def update_car(car_id: str) -> Response:
78+
body = _json_body()
79+
existing = table.get_item(Key={"id": car_id}).get("Item")
80+
if not existing:
81+
raise NotFoundError(f"Car with id {car_id} not found")
82+
updated = {
83+
"id": car_id,
84+
"make": body.get("make", existing.get("make")),
85+
"model": body.get("model", existing.get("model")),
86+
"year": body.get("year", existing.get("year")),
87+
"color": body.get("color", existing.get("color")),
88+
}
89+
table.put_item(Item=updated)
90+
return Response(
91+
status_code=200,
92+
content_type=content_types.APPLICATION_JSON,
93+
body=json.dumps(updated, default=_json_default),
94+
)
95+
96+
97+
@app.delete("/cars/<car_id>")
98+
def delete_car(car_id: str) -> Response:
99+
try:
100+
table.delete_item(
101+
Key={"id": car_id},
102+
ConditionExpression="attribute_exists(id)",
103+
)
104+
except ClientError as exc:
105+
if exc.response["Error"]["Code"] == "ConditionalCheckFailedException":
106+
raise NotFoundError(f"Car with id {car_id} not found") from exc
107+
raise
108+
return Response(status_code=204, body="")
109+
110+
111+
@app.exception_handler(NotFoundError)
112+
def handle_not_found(exc: NotFoundError) -> Response:
113+
return Response(
114+
status_code=404,
115+
content_type=content_types.APPLICATION_JSON,
116+
body=json.dumps({"message": str(exc)}),
117+
)
118+
119+
120+
@app.exception_handler(BadRequestError)
121+
def handle_bad_request(exc: BadRequestError) -> Response:
122+
return Response(
123+
status_code=400,
124+
content_type=content_types.APPLICATION_JSON,
125+
body=json.dumps({"message": str(exc)}),
126+
)
127+
128+
129+
@app.not_found
130+
def handle_route_not_found(_exc: Exception) -> Response:
131+
return Response(
132+
status_code=404,
133+
content_type=content_types.APPLICATION_JSON,
134+
body=json.dumps({"message": "Route not found"}),
135+
)
136+
137+
138+
@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)
139+
def handler(event: dict, context: LambdaContext) -> dict:
140+
return app.resolve(event, context)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
aws-lambda-powertools==3.25.0
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Amazon API Gateway + AWS Lambda SnapStart + Amazon DynamoDB
2+
3+
This pattern demonstrates how to create a REST API using Amazon API Gateway, AWS Lambda and Amazon DynamoDB.
4+
It's built with [Python 3.12](https://www.python.org/downloads/release/python-3128/), together with
5+
[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)
6+
to improve initialization performance of the Lambda function.
7+
8+
## Architecture
9+
10+
- API Gateway REST API (`prod` stage)
11+
- AWS Lambda functon(Python 3.12)
12+
- Lambda SnapStart enabled on published versions
13+
- Lambda `live` alias integrated with API Gateway
14+
- DynamoDB table with partition key `id`
15+
16+
## Endpoints
17+
18+
- `POST /cars`
19+
- `GET /cars/{carId}`
20+
- `PUT /cars/{carId}`
21+
- `DELETE /cars/{carId}`
22+
23+
## Requirements
24+
25+
- Python 3.12+
26+
- AWS CDK v2
27+
- AWS credentials configured
28+
29+
## Deploy
30+
31+
To deploy this stack, run the following commands from the root of the `serverless-patterns` repository:
32+
33+
```bash
34+
# Move to the pattern directory and create a Python virtual environment
35+
cd apigw-python-cdk-lambda-snapstart
36+
python3 -m venv .venv
37+
source .venv/bin/activate
38+
39+
# Install the AWS CDK for Python
40+
pip3 install -r requirements.txt
41+
42+
# Install AWS Lambda Powertools library for the CarHandler Lambda
43+
pip3 install -r CarHandler/requirements.txt -t CarHandler/
44+
45+
# Bootstrap your environment and deploy
46+
cdk bootstrap
47+
cdk deploy
48+
```
49+
50+
## Test
51+
52+
Get endpoint URL from stack outputs (`CarEndpoint`), then run:
53+
54+
```bash
55+
ENDPOINT="<put-the-CarEndpoint-output-URL-here>"
56+
57+
# Create a car (use the returned "id" in the response for GET/PUT/DELETE below)
58+
curl --location --request POST "$ENDPOINT/cars" \
59+
--header 'Content-Type: application/json' \
60+
--data-raw '{"make":"Porsche","model":"992","year":"2022","color":"White"}'
61+
62+
# Get a car by id
63+
curl --location "$ENDPOINT/cars/<car-id>"
64+
65+
# Update a car
66+
curl --location --request PUT "$ENDPOINT/cars/<car-id>" \
67+
--header 'Content-Type: application/json' \
68+
--data-raw '{"make":"Porsche","model":"992","year":"2023","color":"Racing Yellow"}'
69+
70+
# Delete a car
71+
curl --location --request DELETE "$ENDPOINT/cars/<car-id>"
72+
```
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
{
2+
"title": "Integrate AWS Lambda SnapStart function with Amazon API Gateway",
3+
"description": "This pattern demonstrates how to create a REST API using Amazon API Gateway, AWS Lambda with SnapStart, and Amazon DynamoDB.",
4+
"language": "Python",
5+
"level": "200",
6+
"framework": "AWS CDK",
7+
"introBox": {
8+
"headline": "How it works",
9+
"text": [
10+
"This pattern creates a REST API for managing car records using API Gateway and Lambda with SnapStart enabled.",
11+
"The Lambda function is Python 3.12-based and includes a live alias that's integrated with API Gateway for seamless deployments.",
12+
"Lambda SnapStart persists the initialized state of the Lambda runtime, significantly reducing cold start times for function initialization.",
13+
"DynamoDB stores car records with a partition key of 'id', providing a scalable NoSQL database backend for the REST API."
14+
]
15+
},
16+
"gitHub": {
17+
"template": {
18+
"repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/apigw-python-cdk-lambda-snapstart",
19+
"templateURL": "serverless-patterns/apigw-python-cdk-lambda-snapstart",
20+
"projectFolder": "apigw-python-cdk-lambda-snapstart",
21+
"templateFile": "app.py"
22+
}
23+
},
24+
"resources": {
25+
"bullets": [
26+
{
27+
"text": "AWS Lambda SnapStart",
28+
"link": "https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html"
29+
},
30+
{
31+
"text": "API Gateway REST API",
32+
"link": "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html"
33+
},
34+
{
35+
"text": "AWS CDK Python Reference",
36+
"link": "https://docs.aws.amazon.com/cdk/v2/guide/work-with-cdk-python.html"
37+
}
38+
]
39+
},
40+
"deploy": {
41+
"text": [
42+
"python3 -m venv .venv",
43+
"source .venv/bin/activate",
44+
"pip install -r requirements.txt",
45+
"cdk bootstrap",
46+
"cdk deploy"
47+
]
48+
},
49+
"testing": {
50+
"text": [
51+
"Get the CarEndpoint from stack outputs, then test the endpoint to create a new car record:",
52+
"curl --location --request POST \"$ENDPOINT/cars\" --header 'Content-Type: application/json' --data-raw '{\"make\":\"Porsche\",\"model\":\"992\",\"year\":\"2022\",\"color\":\"White\"}'",
53+
"Change the endpoint and HTTP method to test other operations:",
54+
"GET /cars/{carId} - Retrieve a car",
55+
"PUT /cars/{carId} - Update a car",
56+
"DELETE /cars/{carId} - Delete a car"
57+
]
58+
},
59+
"cleanup": {
60+
"text": [
61+
"Delete the stack: <code>cdk destroy</code>."
62+
]
63+
},
64+
"authors": [
65+
{
66+
"name": "Matia Rasetina",
67+
"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",
68+
"bio": "Senior Software Engineer @ Elixirr Digital",
69+
"linkedin": "matiarasetina",
70+
"twitter": ""
71+
}
72+
],
73+
"patternArch": {
74+
"icon1": {
75+
"x": 20,
76+
"y": 50,
77+
"service": "apigw",
78+
"label": "API Gateway REST API"
79+
},
80+
"icon2": {
81+
"x": 50,
82+
"y": 50,
83+
"service": "lambda",
84+
"label": "AWS Lambda"
85+
},
86+
"icon3": {
87+
"x": 80,
88+
"y": 50,
89+
"service": "dynamodb",
90+
"label": "Amazon DynamoDB"
91+
},
92+
"line1": {
93+
"from": "icon1",
94+
"to": "icon2"
95+
},
96+
"line2": {
97+
"from": "icon2",
98+
"to": "icon3"
99+
}
100+
}
101+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env python3
2+
from aws_cdk import (
3+
App, CfnOutput,
4+
Duration,
5+
Stack,
6+
RemovalPolicy,
7+
aws_apigateway as apigw,
8+
aws_dynamodb as dynamodb,
9+
aws_lambda as _lambda,
10+
)
11+
from constructs import Construct
12+
13+
class CarStoreStack(Stack):
14+
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
15+
super().__init__(scope, construct_id, **kwargs)
16+
17+
car_table = dynamodb.Table(
18+
self,
19+
"CarTable",
20+
partition_key=dynamodb.Attribute(name="id", type=dynamodb.AttributeType.STRING),
21+
billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST,
22+
removal_policy=RemovalPolicy.DESTROY,
23+
)
24+
25+
car_function = _lambda.Function(
26+
self,
27+
"CarStoreFunction",
28+
runtime=_lambda.Runtime.PYTHON_3_12,
29+
handler="handler.handler",
30+
code=_lambda.Code.from_asset("CarHandler/"),
31+
timeout=Duration.seconds(10),
32+
snap_start=_lambda.SnapStartConf.ON_PUBLISHED_VERSIONS,
33+
memory_size=256,
34+
environment={
35+
"CAR_TABLE_NAME": car_table.table_name,
36+
"LOG_LEVEL": "INFO",
37+
}
38+
)
39+
car_table.grant_read_write_data(car_function)
40+
41+
live_alias = _lambda.Alias(
42+
self,
43+
"CarStoreLiveAlias",
44+
alias_name="live",
45+
version=car_function.current_version,
46+
)
47+
48+
car_api = apigw.RestApi(
49+
self,
50+
"CarStoreApi",
51+
deploy_options=apigw.StageOptions(stage_name="prod"),
52+
)
53+
54+
integration = apigw.LambdaIntegration(live_alias, proxy=True)
55+
car_api.root.add_method("ANY", integration)
56+
car_api.root.add_resource("{proxy+}").add_method("ANY", integration)
57+
58+
CfnOutput(
59+
self,
60+
"CarEndpoint",
61+
description="API Gateway Car Endpoint",
62+
value=car_api.url,
63+
)
64+
CfnOutput(self, "CarTableName", value=car_table.table_name)
65+
66+
description = "Sample app (uksb-1tthgi812) (tag:apigw-python-cdk-lambda-snapstart)"
67+
app = App()
68+
CarStoreStack(app, "CarStoreStack", description=description)
69+
app.synth()

0 commit comments

Comments
 (0)