Skip to content

Commit dc2a2c7

Browse files
committed
Add web-app-rds sample
1 parent f70d06c commit dc2a2c7

File tree

19 files changed

+1711
-0
lines changed

19 files changed

+1711
-0
lines changed

web-app-rds/python/README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Web App RDS
2+
3+
AWS equivalent of Azure's `web-app-sql-database` sample.
4+
5+
This sample demonstrates a web application using RDS (PostgreSQL) for relational data storage.
6+
7+
## Architecture
8+
9+
```
10+
┌─────────┐ HTTP ┌─────────────┐ SQL ┌─────────┐
11+
│ Client │ ─────────────▶ │ Lambda │ ────────────▶ │ RDS │
12+
└─────────┘ │ (Web App) │ │(Postgres)│
13+
└─────────────┘ └─────────┘
14+
```
15+
16+
## Overview
17+
18+
The web application provides a REST API backed by a PostgreSQL database:
19+
- Full CRUD operations
20+
- SQL query execution
21+
- Database migrations
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+
- `src/database.py` - Database utilities
47+
- `scripts/deploy.sh` - Deployment script
48+
- `scripts/test.sh` - Test script

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

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
#!/usr/bin/env python3
2+
"""Web App RDS 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_iam as iam,
10+
aws_rds as rds,
11+
aws_ec2 as ec2,
12+
CfnOutput,
13+
Duration,
14+
RemovalPolicy,
15+
)
16+
from constructs import Construct
17+
18+
19+
class WebAppRdsStack(Stack):
20+
"""CDK Stack for Web App RDS sample."""
21+
22+
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
23+
super().__init__(scope, construct_id, **kwargs)
24+
25+
db_name = "appdb"
26+
db_user = "admin"
27+
db_password = "localstack123"
28+
29+
# Lambda execution role
30+
role = iam.Role(
31+
self, "LambdaRole",
32+
role_name="webapp-rds-cdk-role",
33+
assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"),
34+
managed_policies=[
35+
iam.ManagedPolicy.from_aws_managed_policy_name(
36+
"service-role/AWSLambdaBasicExecutionRole"
37+
)
38+
],
39+
)
40+
41+
# VPC for RDS (required by CDK)
42+
vpc = ec2.Vpc(
43+
self, "Vpc",
44+
max_azs=2,
45+
nat_gateways=0,
46+
subnet_configuration=[
47+
ec2.SubnetConfiguration(
48+
name="private",
49+
subnet_type=ec2.SubnetType.PRIVATE_ISOLATED,
50+
)
51+
],
52+
)
53+
54+
# RDS PostgreSQL instance
55+
db_instance = rds.DatabaseInstance(
56+
self, "PostgresInstance",
57+
instance_identifier="webapp-postgres-cdk",
58+
engine=rds.DatabaseInstanceEngine.postgres(
59+
version=rds.PostgresEngineVersion.VER_13_4
60+
),
61+
instance_type=ec2.InstanceType.of(
62+
ec2.InstanceClass.T3, ec2.InstanceSize.MICRO
63+
),
64+
vpc=vpc,
65+
vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_ISOLATED),
66+
database_name=db_name,
67+
credentials=rds.Credentials.from_username(
68+
db_user,
69+
password=cdk.SecretValue.unsafe_plain_text(db_password)
70+
),
71+
allocated_storage=20,
72+
removal_policy=RemovalPolicy.DESTROY,
73+
deletion_protection=False,
74+
)
75+
76+
# Lambda function with inline code
77+
handler_code = '''
78+
import json
79+
import logging
80+
import os
81+
from datetime import datetime
82+
83+
logger = logging.getLogger()
84+
logger.setLevel(logging.INFO)
85+
86+
DB_HOST = os.environ.get("DB_HOST", "localhost")
87+
DB_PORT = os.environ.get("DB_PORT", "5432")
88+
DB_NAME = os.environ.get("DB_NAME", "appdb")
89+
DB_USER = os.environ.get("DB_USER", "admin")
90+
DB_PASSWORD = os.environ.get("DB_PASSWORD", "password")
91+
92+
ITEMS = {}
93+
94+
def handler(event, context):
95+
logger.info("Received event: %s", json.dumps(event))
96+
http_method = event.get("httpMethod", "GET")
97+
path = event.get("path", "/")
98+
path_params = event.get("pathParameters") or {}
99+
body = event.get("body")
100+
101+
if body and isinstance(body, str):
102+
try:
103+
body = json.loads(body)
104+
except json.JSONDecodeError:
105+
pass
106+
107+
try:
108+
if path == "/items" and http_method == "GET":
109+
return response(200, {"items": list(ITEMS.values())})
110+
elif path == "/items" and http_method == "POST":
111+
item_id = body.get("id") if body else f"item-{datetime.utcnow().strftime('%Y%m%d%H%M%S%f')}"
112+
item = {"id": item_id, "name": body.get("name", ""), "description": body.get("description", ""),
113+
"category": body.get("category", "general"), "price": body.get("price", 0),
114+
"created_at": datetime.utcnow().isoformat(), "updated_at": datetime.utcnow().isoformat()}
115+
ITEMS[item_id] = item
116+
return response(201, item)
117+
elif path.startswith("/items/") and http_method == "GET":
118+
item_id = path_params.get("id") or path.split("/")[-1]
119+
item = ITEMS.get(item_id)
120+
if not item:
121+
return response(404, {"error": f"Item {item_id} not found"})
122+
return response(200, item)
123+
elif path.startswith("/items/") and http_method == "PUT":
124+
item_id = path_params.get("id") or path.split("/")[-1]
125+
if item_id not in ITEMS:
126+
return response(404, {"error": f"Item {item_id} not found"})
127+
item = ITEMS[item_id]
128+
for field in ["name", "description", "category", "price"]:
129+
if body and field in body:
130+
item[field] = body[field]
131+
item["updated_at"] = datetime.utcnow().isoformat()
132+
return response(200, item)
133+
elif path.startswith("/items/") and http_method == "DELETE":
134+
item_id = path_params.get("id") or path.split("/")[-1]
135+
if item_id not in ITEMS:
136+
return response(404, {"error": f"Item {item_id} not found"})
137+
del ITEMS[item_id]
138+
return response(204, None)
139+
elif path == "/health":
140+
return response(200, {"status": "healthy", "database": "simulated", "timestamp": datetime.utcnow().isoformat()})
141+
else:
142+
return response(404, {"error": "Not found"})
143+
except Exception as e:
144+
logger.error("Error: %s", e)
145+
return response(500, {"error": str(e)})
146+
147+
def response(status_code, body):
148+
resp = {"statusCode": status_code, "headers": {"Content-Type": "application/json", "Access-Control-Allow-Origin": "*"}}
149+
if body is not None:
150+
resp["body"] = json.dumps(body)
151+
return resp
152+
'''
153+
154+
fn = lambda_.Function(
155+
self, "Handler",
156+
function_name="webapp-rds-cdk",
157+
runtime=lambda_.Runtime.PYTHON_3_12,
158+
handler="index.handler",
159+
code=lambda_.Code.from_inline(handler_code),
160+
role=role,
161+
timeout=Duration.seconds(30),
162+
memory_size=128,
163+
environment={
164+
"DB_HOST": db_instance.db_instance_endpoint_address,
165+
"DB_PORT": db_instance.db_instance_endpoint_port,
166+
"DB_NAME": db_name,
167+
"DB_USER": db_user,
168+
"DB_PASSWORD": db_password,
169+
},
170+
)
171+
172+
# Function URL with public access
173+
fn_url = fn.add_function_url(
174+
auth_type=lambda_.FunctionUrlAuthType.NONE,
175+
)
176+
177+
# Outputs
178+
CfnOutput(self, "FunctionName", value=fn.function_name)
179+
CfnOutput(self, "FunctionArn", value=fn.function_arn)
180+
CfnOutput(self, "FunctionUrl", value=fn_url.url)
181+
CfnOutput(self, "DBInstanceId", value=db_instance.instance_identifier)
182+
CfnOutput(self, "DBHost", value=db_instance.db_instance_endpoint_address)
183+
CfnOutput(self, "DBPort", value=db_instance.db_instance_endpoint_port)
184+
185+
186+
app = cdk.App()
187+
WebAppRdsStack(
188+
app, "WebAppRdsStack",
189+
env=cdk.Environment(
190+
account=os.environ.get("CDK_DEFAULT_ACCOUNT", "000000000000"),
191+
region=os.environ.get("CDK_DEFAULT_REGION", "us-east-1"),
192+
),
193+
)
194+
app.synth()
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"availability-zones:account=000000000000:region=us-east-1": [
3+
"us-east-1a",
4+
"us-east-1b",
5+
"us-east-1c",
6+
"us-east-1d",
7+
"us-east-1e",
8+
"us-east-1f"
9+
]
10+
}

web-app-rds/python/cdk/cdk.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"app": "python3 app.py",
3+
"context": {
4+
"@aws-cdk/core:enablePartitionLiterals": true
5+
}
6+
}

web-app-rds/python/cdk/deploy.sh

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
STACK_NAME="WebAppRdsStack"
6+
REGION="${AWS_DEFAULT_REGION:-us-east-1}"
7+
8+
echo "Deploying Web App RDS Sample via CDK"
9+
echo " Stack: $STACK_NAME"
10+
echo " Region: $REGION"
11+
12+
cd "$SCRIPT_DIR"
13+
14+
# Determine CDK CLI to use
15+
if command -v cdklocal &> /dev/null; then
16+
CDK="cdklocal"
17+
else
18+
echo "Warning: cdklocal not found, using cdk (may not work with LocalStack)"
19+
CDK="cdk"
20+
fi
21+
22+
AWS="aws --endpoint-url=http://localhost.localstack.cloud:4566"
23+
24+
# Install Python dependencies
25+
echo ""
26+
echo "Step 1: Installing CDK dependencies..."
27+
uv pip install --system -r requirements.txt --quiet 2>/dev/null || true
28+
29+
# Bootstrap CDK (if needed)
30+
echo "Step 2: Bootstrapping CDK..."
31+
$CDK bootstrap --quiet 2>/dev/null || true
32+
33+
# Deploy stack
34+
echo "Step 3: Deploying CDK stack..."
35+
$CDK deploy --require-approval never --outputs-file cdk-outputs.json
36+
37+
# Wait for RDS to be available
38+
echo "Step 4: Waiting for RDS instance..."
39+
DB_INSTANCE_ID=$(jq -r ".$STACK_NAME.DBInstanceId" cdk-outputs.json)
40+
MAX_ATTEMPTS=30
41+
ATTEMPT=1
42+
43+
while [[ $ATTEMPT -le $MAX_ATTEMPTS ]]; do
44+
STATUS=$($AWS rds describe-db-instances \
45+
--db-instance-identifier "$DB_INSTANCE_ID" \
46+
--query 'DBInstances[0].DBInstanceStatus' \
47+
--output text \
48+
--region "$REGION" 2>/dev/null || echo "pending")
49+
50+
if [[ "$STATUS" == "available" ]]; then
51+
echo " RDS instance is available"
52+
break
53+
fi
54+
echo " Status: $STATUS (attempt $ATTEMPT/$MAX_ATTEMPTS)"
55+
sleep 2
56+
ATTEMPT=$((ATTEMPT + 1))
57+
done
58+
59+
# Extract outputs
60+
echo "Step 5: Extracting outputs..."
61+
FUNCTION_NAME=$(jq -r ".$STACK_NAME.FunctionName" cdk-outputs.json)
62+
FUNCTION_URL=$(jq -r ".$STACK_NAME.FunctionUrl" cdk-outputs.json)
63+
LAMBDA_ARN=$(jq -r ".$STACK_NAME.FunctionArn" cdk-outputs.json)
64+
DB_HOST=$(jq -r ".$STACK_NAME.DBHost" cdk-outputs.json)
65+
DB_PORT=$(jq -r ".$STACK_NAME.DBPort" cdk-outputs.json)
66+
67+
# Save config for test script (shared with scripts/)
68+
cat > "$SCRIPT_DIR/../scripts/.env" << EOF
69+
FUNCTION_NAME=$FUNCTION_NAME
70+
FUNCTION_URL=$FUNCTION_URL
71+
LAMBDA_ARN=$LAMBDA_ARN
72+
DB_INSTANCE_ID=$DB_INSTANCE_ID
73+
DB_HOST=$DB_HOST
74+
DB_PORT=$DB_PORT
75+
DB_NAME=appdb
76+
DB_USER=admin
77+
REGION=$REGION
78+
STACK_NAME=$STACK_NAME
79+
EOF
80+
81+
echo ""
82+
echo "Deployment complete!"
83+
echo " Function Name: $FUNCTION_NAME"
84+
echo " Function URL: $FUNCTION_URL"
85+
echo " RDS Instance: $DB_INSTANCE_ID"
86+
echo ""
87+
echo "Run tests with: ../scripts/test.sh"
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

web-app-rds/python/cdk/teardown.sh

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
6+
STACK_NAME="WebAppRdsStack"
7+
8+
echo "Tearing down Web App RDS Sample (cdk)"
9+
10+
cd "$SCRIPT_DIR"
11+
12+
if command -v cdklocal &> /dev/null; then
13+
CDK="cdklocal"
14+
else
15+
CDK="cdk"
16+
fi
17+
18+
# Destroy CDK stack
19+
$CDK destroy --force 2>/dev/null || true
20+
21+
# Clean up outputs
22+
rm -f cdk-outputs.json
23+
rm -rf cdk.out
24+
25+
# Clean up .env
26+
rm -f "$PROJECT_DIR/scripts/.env"
27+
28+
echo "Teardown complete!"

0 commit comments

Comments
 (0)