|
| 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() |
0 commit comments