Skip to content

Commit f70d06c

Browse files
committed
Add web-app-dynamodb sample
1 parent 5672cc1 commit f70d06c

File tree

15 files changed

+1143
-0
lines changed

15 files changed

+1143
-0
lines changed

web-app-dynamodb/python/README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Web App DynamoDB
2+
3+
AWS equivalent of Azure's `web-app-cosmosdb-nosql-api` sample.
4+
5+
This sample demonstrates a web application using DynamoDB for NoSQL data storage.
6+
7+
## Architecture
8+
9+
```
10+
┌─────────┐ HTTP ┌─────────────┐ Query ┌──────────┐
11+
│ Client │ ─────────────▶ │ Lambda │ ────────────▶ │ DynamoDB │
12+
└─────────┘ │ (Web App) │ │ (NoSQL) │
13+
└─────────────┘ └──────────┘
14+
```
15+
16+
## Overview
17+
18+
The web application provides a REST API for managing items in DynamoDB:
19+
- Create, read, update, delete operations
20+
- Query by partition key
21+
- Scan with filters
22+
23+
## Prerequisites
24+
25+
- LocalStack Pro running with `LOCALSTACK_AUTH_TOKEN`
26+
- AWS CLI or awslocal installed
27+
- Python 3.10+
28+
29+
## Deployment
30+
31+
```bash
32+
cd scripts
33+
./deploy.sh
34+
```
35+
36+
## Testing
37+
38+
```bash
39+
cd scripts
40+
./test.sh
41+
```
42+
43+
## Files
44+
45+
- `src/app.py` - Web application Lambda
46+
- `scripts/deploy.sh` - Deployment script
47+
- `scripts/test.sh` - Test script

web-app-dynamodb/python/cdk/app.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
#!/usr/bin/env python3
2+
"""Web App DynamoDB Sample - CDK Stack."""
3+
4+
import os
5+
import aws_cdk as cdk
6+
from aws_cdk import (
7+
Stack,
8+
aws_lambda as lambda_,
9+
aws_dynamodb as dynamodb,
10+
aws_iam as iam,
11+
CfnOutput,
12+
Duration,
13+
RemovalPolicy,
14+
)
15+
from constructs import Construct
16+
17+
18+
class WebAppDynamoDBStack(Stack):
19+
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
20+
super().__init__(scope, construct_id, **kwargs)
21+
22+
# DynamoDB Table
23+
table = dynamodb.Table(
24+
self, "ItemsTable",
25+
table_name="cdk-items",
26+
partition_key=dynamodb.Attribute(
27+
name="id",
28+
type=dynamodb.AttributeType.STRING
29+
),
30+
billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST,
31+
removal_policy=RemovalPolicy.DESTROY,
32+
)
33+
34+
# Lambda handler code - must match src/app.py event format
35+
handler_code = '''
36+
import json
37+
import os
38+
import boto3
39+
from datetime import datetime
40+
from decimal import Decimal
41+
42+
class DecimalEncoder(json.JSONEncoder):
43+
def default(self, obj):
44+
if isinstance(obj, Decimal):
45+
return float(obj)
46+
return super().default(obj)
47+
48+
ENDPOINT_URL = os.environ.get('LOCALSTACK_HOSTNAME')
49+
if ENDPOINT_URL:
50+
ENDPOINT_URL = f"http://{ENDPOINT_URL}:4566"
51+
dynamodb = boto3.resource('dynamodb', endpoint_url=ENDPOINT_URL)
52+
TABLE_NAME = os.environ['TABLE_NAME']
53+
54+
def handler(event, context):
55+
method = event.get('httpMethod', 'GET')
56+
path = event.get('path', '/')
57+
path_params = event.get('pathParameters') or {}
58+
body = event.get('body')
59+
if body and isinstance(body, str):
60+
try:
61+
body = json.loads(body)
62+
except:
63+
pass
64+
65+
table = dynamodb.Table(TABLE_NAME)
66+
67+
if path == '/items' and method == 'GET':
68+
result = table.scan()
69+
return response(200, {'items': result.get('Items', [])})
70+
elif path == '/items' and method == 'POST':
71+
if not body:
72+
return response(400, {'error': 'Invalid request body'})
73+
item_id = body.get('id') or f"item-{datetime.utcnow().strftime('%Y%m%d%H%M%S%f')}"
74+
item = {'id': item_id, 'name': body.get('name', ''), 'description': body.get('description', ''),
75+
'category': body.get('category', 'general'), 'price': Decimal(str(body.get('price', 0))),
76+
'createdAt': datetime.utcnow().isoformat(), 'updatedAt': datetime.utcnow().isoformat()}
77+
table.put_item(Item=item)
78+
return response(201, item)
79+
elif path.startswith('/items/') and method == 'GET':
80+
item_id = path_params.get('id') or path.split('/')[-1]
81+
result = table.get_item(Key={'id': item_id})
82+
item = result.get('Item')
83+
if not item:
84+
return response(404, {'error': f'Item {item_id} not found'})
85+
return response(200, item)
86+
elif path.startswith('/items/') and method == 'PUT':
87+
item_id = path_params.get('id') or path.split('/')[-1]
88+
result = table.get_item(Key={'id': item_id})
89+
if 'Item' not in result:
90+
return response(404, {'error': f'Item {item_id} not found'})
91+
update_expr = 'SET updatedAt = :ua'
92+
expr_values = {':ua': datetime.utcnow().isoformat()}
93+
expr_names = {}
94+
if 'name' in body:
95+
update_expr += ', #n = :n'
96+
expr_values[':n'] = body['name']
97+
expr_names['#n'] = 'name'
98+
if 'price' in body:
99+
update_expr += ', price = :p'
100+
expr_values[':p'] = Decimal(str(body['price']))
101+
update_args = {'Key': {'id': item_id}, 'UpdateExpression': update_expr,
102+
'ExpressionAttributeValues': expr_values, 'ReturnValues': 'ALL_NEW'}
103+
if expr_names:
104+
update_args['ExpressionAttributeNames'] = expr_names
105+
result = table.update_item(**update_args)
106+
return response(200, result.get('Attributes'))
107+
elif path.startswith('/items/') and method == 'DELETE':
108+
item_id = path_params.get('id') or path.split('/')[-1]
109+
result = table.get_item(Key={'id': item_id})
110+
if 'Item' not in result:
111+
return response(404, {'error': f'Item {item_id} not found'})
112+
table.delete_item(Key={'id': item_id})
113+
return response(204, None)
114+
return response(404, {'error': 'Not found'})
115+
116+
def response(status_code, body):
117+
resp = {'statusCode': status_code, 'headers': {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'}}
118+
if body is not None:
119+
resp['body'] = json.dumps(body, cls=DecimalEncoder)
120+
return resp
121+
'''
122+
123+
# Lambda function
124+
fn = lambda_.Function(
125+
self, "Handler",
126+
function_name="cdk-webapp-dynamodb",
127+
runtime=lambda_.Runtime.PYTHON_3_12,
128+
handler="index.handler",
129+
code=lambda_.Code.from_inline(handler_code),
130+
timeout=Duration.seconds(30),
131+
memory_size=128,
132+
environment={
133+
"TABLE_NAME": table.table_name,
134+
},
135+
)
136+
137+
# Grant DynamoDB access
138+
table.grant_read_write_data(fn)
139+
140+
# Function URL
141+
fn_url = fn.add_function_url(
142+
auth_type=lambda_.FunctionUrlAuthType.NONE,
143+
)
144+
145+
# Outputs
146+
CfnOutput(self, "FunctionName", value=fn.function_name)
147+
CfnOutput(self, "FunctionUrl", value=fn_url.url)
148+
CfnOutput(self, "TableName", value=table.table_name)
149+
150+
151+
app = cdk.App()
152+
WebAppDynamoDBStack(
153+
app, "WebAppDynamoDBStack",
154+
env=cdk.Environment(
155+
account=os.environ.get("CDK_DEFAULT_ACCOUNT", "000000000000"),
156+
region=os.environ.get("CDK_DEFAULT_REGION", "us-east-1"),
157+
),
158+
)
159+
app.synth()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"app": "python3 app.py"
3+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
STACK_NAME="WebAppDynamoDBStack"
6+
7+
echo "Deploying Web App DynamoDB via CDK"
8+
9+
cd "$SCRIPT_DIR"
10+
11+
if command -v cdklocal &> /dev/null; then
12+
CDK="cdklocal"
13+
else
14+
CDK="cdk"
15+
fi
16+
17+
if command -v awslocal &> /dev/null; then
18+
AWS="awslocal"
19+
else
20+
AWS="aws --endpoint-url=http://localhost.localstack.cloud:4566"
21+
fi
22+
23+
echo "Step 1: Installing CDK dependencies..."
24+
uv pip install --system -r requirements.txt --quiet 2>/dev/null || true
25+
26+
echo "Step 2: Bootstrapping CDK..."
27+
$CDK bootstrap --quiet 2>/dev/null || true
28+
29+
echo "Step 3: Deploying CDK stack..."
30+
$CDK deploy --require-approval never --outputs-file cdk-outputs.json
31+
32+
echo "Step 4: Extracting outputs..."
33+
FUNCTION_NAME=$(jq -r ".$STACK_NAME.FunctionName" cdk-outputs.json)
34+
FUNCTION_URL=$(jq -r ".$STACK_NAME.FunctionUrl" cdk-outputs.json)
35+
TABLE_NAME=$(jq -r ".$STACK_NAME.TableName" cdk-outputs.json)
36+
37+
cat > "$SCRIPT_DIR/../scripts/.env" << EOF
38+
FUNCTION_NAME=$FUNCTION_NAME
39+
FUNCTION_URL=$FUNCTION_URL
40+
TABLE_NAME=$TABLE_NAME
41+
REGION=us-east-1
42+
EOF
43+
44+
echo ""
45+
echo "Deployment complete!"
46+
echo " Function: $FUNCTION_NAME"
47+
echo " Table: $TABLE_NAME"
48+
echo " URL: $FUNCTION_URL"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
aws-cdk-lib>=2.100.0
2+
constructs>=10.0.0
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
STACK_NAME="web-app-dynamodb-stack"
6+
REGION="${AWS_DEFAULT_REGION:-us-east-1}"
7+
8+
echo "Deploying Web App DynamoDB via CloudFormation"
9+
10+
# Use aws CLI directly with endpoint-url to avoid awslocal --s3-endpoint-url bug
11+
AWS="aws --endpoint-url=http://localhost.localstack.cloud:4566"
12+
13+
cd "$SCRIPT_DIR"
14+
15+
echo "Step 1: Deploying CloudFormation stack..."
16+
$AWS cloudformation deploy \
17+
--stack-name "$STACK_NAME" \
18+
--template-file template.yml \
19+
--capabilities CAPABILITY_NAMED_IAM \
20+
--region "$REGION" \
21+
--no-fail-on-empty-changeset
22+
23+
echo "Step 2: Extracting outputs..."
24+
OUTPUTS=$($AWS cloudformation describe-stacks --stack-name "$STACK_NAME" --query 'Stacks[0].Outputs' --region "$REGION")
25+
26+
FUNCTION_NAME=$(echo "$OUTPUTS" | jq -r '.[] | select(.OutputKey=="FunctionName") | .OutputValue')
27+
FUNCTION_URL=$(echo "$OUTPUTS" | jq -r '.[] | select(.OutputKey=="FunctionUrl") | .OutputValue')
28+
TABLE_NAME=$(echo "$OUTPUTS" | jq -r '.[] | select(.OutputKey=="TableName") | .OutputValue')
29+
30+
cat > "$SCRIPT_DIR/../scripts/.env" << EOF
31+
FUNCTION_NAME=$FUNCTION_NAME
32+
FUNCTION_URL=$FUNCTION_URL
33+
TABLE_NAME=$TABLE_NAME
34+
REGION=$REGION
35+
EOF
36+
37+
echo ""
38+
echo "Deployment complete!"
39+
echo " Function: $FUNCTION_NAME"
40+
echo " Table: $TABLE_NAME"
41+
echo " URL: $FUNCTION_URL"

0 commit comments

Comments
 (0)