aws2-12 Exercise 1: The Order-Intake Stack as CDK Code: Synth, Lint, Assertions and Handler Tests ======================================================================== Run with Windows 11, Node 24.18.0, aws-cdk 2.1145.0, aws-cdk-lib 2.273.0, constructs 10.8.1, TypeScript 7.0.2, Jest 30.5.2 (node_modules is a directory junction to chapter 5's), Python 3.14 (py launcher) with cfn-lint 1.57.2, PyYAML, moto 5.2.4 (in-process mock_aws), boto3. Scratch folder C:/lwaws/c12; nothing installed globally; Docker not used. Build with: node node_modules/typescript/bin/tsc -p tsconfig.json Synthesise with: AWS_EC2_METADATA_DISABLED=true node node_modules/aws-cdk/bin/cdk synth -c hardened=true -o cdk.out.hard -q (and hardened=false -o cdk.out.draft). VERIFICATION LEVEL: 1 (no AWS account; nothing deployed); statements about the Well-Architected Framework and prices come from pages read on 2026-10-13 and are marked as such in the chapter. Outputs below are saved from the real runs of 2026-10-13. --- lib/orders-stack.ts --- import * as cdk from "aws-cdk-lib"; import { Construct } from "constructs"; import * as kms from "aws-cdk-lib/aws-kms"; import * as s3 from "aws-cdk-lib/aws-s3"; import * as cloudfront from "aws-cdk-lib/aws-cloudfront"; import * as origins from "aws-cdk-lib/aws-cloudfront-origins"; import * as apigw from "aws-cdk-lib/aws-apigatewayv2"; import * as integrations from "aws-cdk-lib/aws-apigatewayv2-integrations"; import * as lambda from "aws-cdk-lib/aws-lambda"; import * as sources from "aws-cdk-lib/aws-lambda-event-sources"; import * as logs from "aws-cdk-lib/aws-logs"; import * as sqs from "aws-cdk-lib/aws-sqs"; import * as sns from "aws-cdk-lib/aws-sns"; import * as ddb from "aws-cdk-lib/aws-dynamodb"; import * as secrets from "aws-cdk-lib/aws-secretsmanager"; import * as cw from "aws-cdk-lib/aws-cloudwatch"; import * as cwa from "aws-cdk-lib/aws-cloudwatch-actions"; export interface OrdersProps extends cdk.StackProps { /** false = the first draft of the design; true = after the review's fixes (same code, one flag) */ hardened: boolean; } export class OrdersStack extends cdk.Stack { constructor(scope: Construct, id: string, props: OrdersProps) { super(scope, id, props); const h = props.hardened; const keep = cdk.RemovalPolicy.RETAIN; const key = new kms.Key(this, "DataKey", { enableKeyRotation: true, removalPolicy: keep, description: "orders data key" }); // --- site: private bucket behind CloudFront with origin access control const site = new s3.Bucket(this, "Site", { blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, encryption: s3.BucketEncryption.S3_MANAGED, enforceSSL: h, versioned: h, removalPolicy: keep, }); // --- data path: API Lambda -> queue -> worker Lambda -> table const dlq = new sqs.Queue(this, "OrdersDlq", { encryptionMasterKey: key, retentionPeriod: cdk.Duration.days(14), enforceSSL: h }); const workerTimeout = cdk.Duration.seconds(30); const queue = new sqs.Queue(this, "OrdersQueue", { encryptionMasterKey: key, visibilityTimeout: cdk.Duration.seconds(h ? 6 * 30 + 5 : 30), // the Lambda/SQS page: at least six times the function timeout (+ batch window) deadLetterQueue: { queue: dlq, maxReceiveCount: 5 }, enforceSSL: h, }); const table = new ddb.Table(this, "Orders", { partitionKey: { name: "orderId", type: ddb.AttributeType.STRING }, billingMode: ddb.BillingMode.PAY_PER_REQUEST, encryption: ddb.TableEncryption.CUSTOMER_MANAGED, encryptionKey: key, pointInTimeRecoverySpecification: { pointInTimeRecoveryEnabled: true }, deletionProtection: h, removalPolicy: keep, }); const apiKeySecret = new secrets.Secret(this, "ApiSecret", { encryptionKey: key, description: "signing secret for the orders API", removalPolicy: keep }); const mkLog = (name: string) => h ? new logs.LogGroup(this, name, { retention: logs.RetentionDays.ONE_MONTH, removalPolicy: cdk.RemovalPolicy.DESTROY }) : undefined; const apiFn = new lambda.Function(this, "ApiFn", { runtime: lambda.Runtime.PYTHON_3_13, architecture: lambda.Architecture.ARM_64, handler: "app.handler", code: lambda.Code.fromAsset("app/api"), timeout: cdk.Duration.seconds(5), memorySize: 256, environment: { QUEUE_URL: queue.queueUrl, SECRET_ARN: apiKeySecret.secretArn }, tracing: h ? lambda.Tracing.ACTIVE : lambda.Tracing.DISABLED, logGroup: mkLog("ApiLogs"), }); queue.grantSendMessages(apiFn); apiKeySecret.grantRead(apiFn); const workerFn = new lambda.Function(this, "WorkerFn", { runtime: lambda.Runtime.PYTHON_3_13, architecture: lambda.Architecture.ARM_64, handler: "worker.handler", code: lambda.Code.fromAsset("app/worker"), timeout: workerTimeout, memorySize: 256, environment: { TABLE_NAME: table.tableName }, tracing: h ? lambda.Tracing.ACTIVE : lambda.Tracing.DISABLED, logGroup: mkLog("WorkerLogs"), }); workerFn.addEventSource(new sources.SqsEventSource(queue, { batchSize: 10, reportBatchItemFailures: true, maxConcurrency: h ? 5 : undefined })); table.grantWriteData(workerFn); // --- API (HTTP API) behind the same CloudFront domain const api = new apigw.HttpApi(this, "Api", { apiName: "orders-api", createDefaultStage: false }); api.addRoutes({ path: "/api/orders", methods: [apigw.HttpMethod.POST], integration: new integrations.HttpLambdaIntegration("Orders", apiFn) }); const stage = new apigw.HttpStage(this, "Stage", { httpApi: api, stageName: "live", autoDeploy: true, throttle: h ? { rateLimit: 50, burstLimit: 100 } : undefined, }); const dist = new cloudfront.Distribution(this, "Cdn", { defaultRootObject: "index.html", priceClass: cloudfront.PriceClass.PRICE_CLASS_100, httpVersion: cloudfront.HttpVersion.HTTP2_AND_3, defaultBehavior: { origin: origins.S3BucketOrigin.withOriginAccessControl(site), viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED, }, additionalBehaviors: { "/api/*": { origin: new origins.HttpOrigin(cdk.Fn.select(2, cdk.Fn.split("/", stage.url)), { originPath: "/live", protocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY }), viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.HTTPS_ONLY, allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL, cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED, originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER, }, }, }); // --- alarms const topic = new sns.Topic(this, "Alerts"); const act = new cwa.SnsAction(topic); const alarm = (id: string, metric: cw.IMetric, threshold: number, desc: string, evals = 1) => { const a = new cw.Alarm(this, id, { metric, threshold, evaluationPeriods: evals, comparisonOperator: cw.ComparisonOperator.GREATER_THAN_THRESHOLD, treatMissingData: cw.TreatMissingData.NOT_BREACHING, alarmDescription: desc }); a.addAlarmAction(act); }; alarm("DlqNotEmpty", dlq.metricApproximateNumberOfMessagesVisible({ period: cdk.Duration.minutes(1) }), 0, "orders landed in the dead-letter queue"); if (h) { alarm("ApiErrors", apiFn.metricErrors({ period: cdk.Duration.minutes(1) }), 0, "the API function raised errors"); alarm("WorkerErrors", workerFn.metricErrors({ period: cdk.Duration.minutes(1) }), 0, "the worker raised errors"); alarm("QueueAge", queue.metricApproximateAgeOfOldestMessage({ period: cdk.Duration.minutes(1) }), 300, "the oldest order has waited over 5 minutes", 3); alarm("Api5xx", new cw.Metric({ namespace: "AWS/ApiGateway", metricName: "5xx", dimensionsMap: { ApiId: api.apiId, Stage: "live" }, statistic: "Sum", period: cdk.Duration.minutes(1) }), 0, "the HTTP API returned 5xx"); } new cdk.CfnOutput(this, "SiteUrl", { value: `https://${dist.distributionDomainName}` }); } } --- bin/app.ts --- import * as cdk from "aws-cdk-lib"; import { OrdersStack } from "../lib/orders-stack"; const app = new cdk.App(); const hardened = String(app.node.tryGetContext("hardened") ?? "true") === "true"; new OrdersStack(app, hardened ? "Orders" : "OrdersDraft", { hardened }); cdk.Tags.of(app).add("project", "orders"); --- cdk.json --- { "app": "node dist/bin/app.js" } --- app/api/app.py --- import json import os import uuid import boto3 sqs = boto3.client("sqs") def handler(event, context): """POST /api/orders: validate, queue, answer 202 with an id the client can poll.""" try: body = json.loads(event.get("body") or "{}") sku, qty = str(body["sku"]), int(body["quantity"]) if qty < 1 or qty > 100: raise ValueError("quantity") except (KeyError, ValueError, TypeError): return {"statusCode": 400, "body": json.dumps({"message": "sku and quantity (1-100) required"})} order_id = str(uuid.uuid4()) sqs.send_message(QueueUrl=os.environ["QUEUE_URL"], MessageBody=json.dumps({"orderId": order_id, "sku": sku, "quantity": qty})) return {"statusCode": 202, "body": json.dumps({"orderId": order_id})} --- app/worker/worker.py --- import json import os import boto3 from botocore.exceptions import ClientError table = boto3.resource("dynamodb").Table(os.environ["TABLE_NAME"]) def handler(event, context): """SQS batch: write each order once (a conditional put makes redelivery harmless); report only the failures.""" failures = [] for rec in event["Records"]: try: m = json.loads(rec["body"]) table.put_item(Item={"orderId": m["orderId"], "sku": m["sku"], "quantity": m["quantity"]}, ConditionExpression="attribute_not_exists(orderId)") except ClientError as e: if e.response["Error"]["Code"] != "ConditionalCheckFailedException": failures.append({"itemIdentifier": rec["messageId"]}) except Exception: failures.append({"itemIdentifier": rec["messageId"]}) return {"batchItemFailures": failures} --- synth and lint summary (saved output of the real run) --- cdk synth -c hardened=false -> exit 0, no warnings, 24 resources, cfn-lint exit 4 (2 x W3005) cdk synth -c hardened=true -> exit 0, no warnings, 32 resources, cfn-lint exit 4 (2 x W3005) (the first synth printed: WARNING Ignoring minimumProtocolVersion: it has no effect without a custom certificate. The distribution uses the CloudFront default certificate, whose security policy is fixed at TLSv1. I removed the property.) jest: Tests: 8 passed, 8 total --- resource count of the synthesised templates (saved output of the real run) --- hardened stack Orders: 32 resources, 22 types (draft OrdersDraft: 24 resources) 1 AWS::ApiGatewayV2::Api 1 AWS::ApiGatewayV2::Integration 1 AWS::ApiGatewayV2::Route 1 AWS::ApiGatewayV2::Stage 1 AWS::CDK::Metadata 1 AWS::CloudFront::Distribution 1 AWS::CloudFront::OriginAccessControl 5 AWS::CloudWatch::Alarm 1 AWS::DynamoDB::Table 2 AWS::IAM::Policy 2 AWS::IAM::Role 1 AWS::KMS::Key 1 AWS::Lambda::EventSourceMapping 2 AWS::Lambda::Function 1 AWS::Lambda::Permission 2 AWS::Logs::LogGroup 1 AWS::S3::Bucket 1 AWS::S3::BucketPolicy 1 AWS::SNS::Topic 2 AWS::SQS::Queue 2 AWS::SQS::QueuePolicy 1 AWS::SecretsManager::Secret --- test/orders-stack.test.ts --- import * as cdk from "aws-cdk-lib"; import { Match, Template } from "aws-cdk-lib/assertions"; import { OrdersStack } from "../lib/orders-stack"; const make = (hardened: boolean) => Template.fromStack(new OrdersStack(new cdk.App(), "T", { hardened })); const hard = make(true); const draft = make(false); test("worker queue visibility timeout is at least six times the worker timeout (hardened)", () => { hard.hasResourceProperties("AWS::SQS::Queue", { VisibilityTimeout: 185, RedrivePolicy: Match.objectLike({ maxReceiveCount: 5 }) }); hard.hasResourceProperties("AWS::Lambda::Function", { Handler: "worker.handler", Timeout: 30 }); }); test("the draft has the 30-second queue timeout that the review rejects", () => { draft.hasResourceProperties("AWS::SQS::Queue", { VisibilityTimeout: 30 }); }); test("the consumer reports partial batch failures and has a concurrency cap", () => { hard.hasResourceProperties("AWS::Lambda::EventSourceMapping", { FunctionResponseTypes: ["ReportBatchItemFailures"], ScalingConfig: { MaximumConcurrency: 5 } }); }); test("table: on-demand, point-in-time recovery, deletion protection, customer-managed key", () => { hard.hasResourceProperties("AWS::DynamoDB::Table", { BillingMode: "PAY_PER_REQUEST", PointInTimeRecoverySpecification: { PointInTimeRecoveryEnabled: true }, DeletionProtectionEnabled: true, SSESpecification: { SSEEnabled: true, SSEType: "KMS" }, }); }); test("both functions are arm64 and traced", () => { const fns = hard.findResources("AWS::Lambda::Function"); expect(Object.keys(fns)).toHaveLength(2); for (const f of Object.values(fns)) { expect((f as any).Properties.Architectures).toEqual(["arm64"]); expect((f as any).Properties.TracingConfig).toEqual({ Mode: "Active" }); } }); test("five alarms, each with an action", () => { const al = hard.findResources("AWS::CloudWatch::Alarm"); expect(Object.keys(al)).toHaveLength(5); for (const a of Object.values(al)) expect((a as any).Properties.AlarmActions).toBeDefined(); }); test("stateful resources are retained", () => { for (const t of ["AWS::S3::Bucket", "AWS::DynamoDB::Table", "AWS::KMS::Key", "AWS::SecretsManager::Secret"]) { for (const r of Object.values(hard.findResources(t))) expect((r as any).DeletionPolicy).toBe("Retain"); } }); test("the site bucket is reachable only through CloudFront (origin access control, no public policy)", () => { hard.resourceCountIs("AWS::CloudFront::OriginAccessControl", 1); hard.hasResourceProperties("AWS::S3::Bucket", { PublicAccessBlockConfiguration: { BlockPublicAcls: true, BlockPublicPolicy: true, IgnorePublicAcls: true, RestrictPublicBuckets: true } }); }); --- node node_modules/jest/bin/jest.js (saved output of the real run) --- Test Suites: 1 passed, 1 total Tests: 8 passed, 8 total Snapshots: 0 total Time: 6.98 s Ran all test suites. --- flow_test.py --- """The two handlers run against moto 5.2.4 (in-process): the API function queues an order, the worker writes it. Level 1, emulator limits apply: moto will not tell us whether a real Lambda would time out, be throttled or hit a KMS permission. It does test our own code's logic.""" import importlib.util, json, os, sys import boto3 from moto import mock_aws def load(name, path): spec = importlib.util.spec_from_file_location(name, path); m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m); return m os.environ.update(AWS_DEFAULT_REGION="eu-west-2", AWS_ACCESS_KEY_ID="x", AWS_SECRET_ACCESS_KEY="x") ok = True def chk(label, cond): global ok print(("ok " if cond else "FAIL ") + label); ok &= bool(cond) with mock_aws(): sqs = boto3.client("sqs"); ddb = boto3.client("dynamodb") url = sqs.create_queue(QueueName="orders")["QueueUrl"] ddb.create_table(TableName="Orders", AttributeDefinitions=[{"AttributeName": "orderId", "AttributeType": "S"}], KeySchema=[{"AttributeName": "orderId", "KeyType": "HASH"}], BillingMode="PAY_PER_REQUEST") os.environ.update(QUEUE_URL=url, TABLE_NAME="Orders") api = load("api", "app/api/app.py"); worker = load("worker", "app/worker/worker.py") ev = lambda b: {"body": json.dumps(b) if not isinstance(b, str) else b} r = api.handler(ev({"sku": "A1", "quantity": 3}), None) chk("valid order -> 202 with an orderId", r["statusCode"] == 202 and "orderId" in json.loads(r["body"])) for label, body in [("quantity 0", {"sku": "A", "quantity": 0}), ("quantity 101", {"sku": "A", "quantity": 101}), ("missing sku", {"quantity": 1}), ("quantity 'x'", {"sku": "A", "quantity": "x"}), ("not JSON", "{oops")]: chk(f"invalid order ({label}) -> 400", api.handler(ev(body), None)["statusCode"] == 400) msgs = sqs.receive_message(QueueUrl=url, MaxNumberOfMessages=10).get("Messages", []) chk("only the valid order was queued", len(msgs) == 1) rec = {"messageId": "m1", "body": msgs[0]["Body"]} res = worker.handler({"Records": [rec]}, None) chk("worker writes it, no failures", res == {"batchItemFailures": []} and ddb.scan(TableName="Orders")["Count"] == 1) res = worker.handler({"Records": [rec]}, None) chk("redelivery of the same message is harmless (still 1 item, no failure)", res == {"batchItemFailures": []} and ddb.scan(TableName="Orders")["Count"] == 1) res = worker.handler({"Records": [rec, {"messageId": "bad", "body": "not json"}]}, None) chk("a poison message is reported alone", res == {"batchItemFailures": [{"itemIdentifier": "bad"}]}) print("\nall flow checks pass" if ok else "\nSOME FLOW CHECKS FAILED") --- py flow_test.py (saved output of the real run) --- ok valid order -> 202 with an orderId ok invalid order (quantity 0) -> 400 ok invalid order (quantity 101) -> 400 ok invalid order (missing sku) -> 400 ok invalid order (quantity 'x') -> 400 ok invalid order (not JSON) -> 400 ok only the valid order was queued ok worker writes it, no failures ok redelivery of the same message is harmless (still 1 item, no failure) ok a poison message is reported alone all flow checks pass -------------------------- Reading the result: both syntheses exit 0 with no credentials; the draft has 24 resources and the hardened stack 32 (22 types); both pass cfn-lint apart from two W3005 warnings (a redundant DependsOn the CDK adds), which are the baseline. The 8 Jest assertions assert what I already believed (I wrote stack and tests together), so they are regression guards, not proof of a good design. The 10 flow checks test my own handlers' logic against moto; they say nothing about Lambda timeouts, IAM permissions on the KMS key or event source mapping behaviour. The first synth printed a CDK warning that is a real finding: minimumProtocolVersion has no effect without a custom certificate (the default certificate's policy is fixed at TLSv1). I removed the property from the stack and recorded it in the chapter. Mistake to avoid: a security property you set in code may be ignored by the platform; read the synth output (annotations), not just the exit status.