Capstone

AWS Intermediate/Advanced

Chapter 12 ยท Capstone: A Well-Architected Application

This chapter builds one small system from the pieces of the course and then reviews it. The system takes orders: a static site served by CloudFront (AWS Intermediate/Advanced 10), an HTTP API with a Lambda function (AWS Intermediate/Advanced 8), a queue with a dead-letter queue and a worker that reports partial batch failures (AWS Intermediate/Advanced 9), a DynamoDB table and a secret encrypted with a customer-managed KMS key (AWS Intermediate/Advanced 3), alarms (Observability), all defined as code with the CDK (AWS Intermediate/Advanced 5). The review uses the six pillars of the AWS Well-Architected Framework.

How every example in this chapter was verified
Nothing was deployed and no request travelled through anything AWS runs. There is no AWS account on the machine that wrote this course.
  • Run here (level 1): cdk synth (aws-cdk 2.1145.0, aws-cdk-lib 2.273.0, TypeScript) producing two real CloudFormation templates; cfn-lint 1.57.2 on both; eight Jest assertions on the stack; the two Lambda handlers run against the moto 5.2.4 emulator; a checker I wrote that applies 36 checks to a synthesised template, grouped by pillar; 29 one-change regressions run against cfn-lint and that checker; and a cost sketch from pricing pages.
  • From the documentation, not run (level 3): the framework text (read on 2026-10-13 through a page-fetch tool that summarises pages) and what every service does at run time.
  • Needs an account, not run (level 2): cdk bootstrap, diff, deploy and destroy. The commands are in AWS Intermediate/Advanced 5; they were not run.
The AWS Pricing Calculator was not used (the outline asked for it; it is an interactive tool that I could not drive). The cost sketch uses rates from the pricing pages and says which lines it could not price. Docker was not needed.

The Design

browser --> CloudFront (one distribution, one domain) | /* -> private S3 bucket (origin access control) static site | /api/* -> HTTP API stage "live" -> ApiFn (Lambda, arm64) POST /api/orders | validate, answer 202 + orderId v SQS work queue --(5 failed receives)--> dead-letter queue --> alarm | batches of 10, partial-batch reporting v WorkerFn (Lambda) --conditional put--> DynamoDB table (PITR, KMS key) one customer-managed KMS key (queues, table, secret) | one Secrets Manager secret | five alarms -> one SNS topic

The stack is one construct class with one flag, hardened. With the flag off it is my first draft; with it on it contains the changes that the review (below) asked for. Same code, two templates, so the difference between them is exactly what the review found. The core of it:

// --- 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);
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, }, }, });

The handlers are deliberately small. The API function validates (a SKU and a quantity from 1 to 100), puts the order on the queue and answers 202 with an id. The worker writes each order with a conditional put, so a redelivered message does nothing, and returns only the failed message ids.

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}

Synthesised and Linted for Real

Run here: cdk synth -c hardened=false and -c hardened=true, each exit 0, without credentials (synthesis needs none, as in AWS Intermediate/Advanced 5). The first run printed one warning from the CDK itself, and it is a real finding: I had set a minimum TLS version on the distribution, and the CDK said "Ignoring 'minimumProtocolVersion': it has no effect without a custom 'certificate'. The distribution uses the CloudFront default certificate, whose security policy is fixed at TLSv1." A TLS floor needs a custom domain and a certificate (AWS Intermediate/Advanced 10), which this design does not have, so I removed the property and the warning went away. Remember that the stack therefore accepts whatever the default certificate's policy accepts.

cat out/resources.out
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

Both templates passed cfn-lint with two W3005 warnings each (a DependsOn that the CDK adds although a GetAtt already enforces it; harmless, and I counted them as the baseline, not as findings). Eight Jest assertions on the stack pass: the queue's visibility timeout (185 seconds, from the Lambda/SQS page's rule of six times the function timeout plus the batch window of 5), the redrive count, partial-batch reporting with a concurrency cap, the table's settings, arm64 and tracing on both functions, five alarms with actions, Retain on stateful resources, and the origin access control with public access blocked. These tests assert what I already believed (I wrote the stack and the tests together), so they guard against regressions rather than prove the design right.

Run here: the handlers against moto in memory. Ten checks pass: a valid order returns 202, five kinds of invalid order return 400 and are not queued, the worker writes the order, a redelivery changes nothing, and a poison message among good ones is the only one reported as failed.

py flow_test.py
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

That tests my code's logic. It says nothing about the real services: Lambda timeouts, IAM permissions for the function to use the KMS key, throttling and partial-batch handling in the event source mapping are all run-time behaviour that chapters 8 and 9 showed an emulator does not enforce.

The Review

What the framework says

From the documentation (read 2026-10-13): the framework has six pillars: operational excellence, security, reliability, performance efficiency, cost optimization and sustainability. Each has design principles, best-practice areas and numbered questions. What I read:

PillarIts design principles (names as printed)Best-practice areas
Operational excellence (7)Organize teams around business outcomes; Implement observability for actionable insights; Safely automate where possible; Make frequent, small, reversible changes; Refine operations procedures frequently; Anticipate failure; Learn from all operational events and metrics; Use managed servicesOrganization, Prepare, Operate, Evolve
Security (7)Implement a strong identity foundation; Maintain traceability; Apply security at all layers; Automate security best practices; Protect data in transit and at rest; Keep people away from data; Prepare for security eventsSecurity foundations, Identity and access management, Detection, Infrastructure protection, Data protection, Incident response, Application security
Reliability (5)Automatically recover from failure; Test recovery procedures; Scale horizontally to increase aggregate workload availability; Stop guessing capacity; Manage change through automationFoundations, Workload architecture, Change management, Failure management
Performance efficiency (5)Democratize advanced technologies; Go global in minutes; Use serverless architectures; Experiment more often; Consider mechanical sympathyArchitecture selection, Compute and hardware, Data management, Networking and content delivery, Process and culture
Cost optimization (5)Implement Cloud Financial Management; Adopt a consumption model; Measure overall efficiency; Stop spending money on undifferentiated heavy lifting; Analyze and attribute expenditurePractice Cloud Financial Management, Expenditure and usage awareness, Cost-effective resources, Manage demand and supply resources, Optimize over time
Sustainability (6)Understand your impact; Establish sustainability goals; Maximize utilization; Anticipate and adopt new, more efficient hardware and software offerings; Use managed services; Reduce the downstream impact of your cloud workloadsRegion selection, Alignment to demand, Software and architecture, Data management, Hardware and services, Process and culture

The outline asked for the framework's own questions. I read only some of them, from the area pages that matter most for this system: SEC 2 and 3 (identities and permissions for people and machines), SEC 7 to 9 (data classification, at rest, in transit), REL 3 to 5 (service architecture and distributed-system interactions), REL 9 to 13 (backup, fault isolation, component failure, testing, disaster recovery), OPS 8 to 10 (observability, operations health, events), PERF 2 (compute), COST 5 to 8 (service selection, size, pricing models, data transfer) and SUS 5 (hardware and services). I did not read the question pages for the other areas, so this review is not a complete framework review and must not be reported as one. The pillar pages also say, correctly, that answering questions is a conversation about people and process, which a template cannot do.

What a template can and cannot show

Run here: wa_review.py reads a synthesised template and applies 36 scored checks, each tagged with the pillar and the design principle or earlier chapter it comes from, plus 3 informational lines. I wrote the rules before running them on either template, from the pages above and chapters 3, 5, 8, 9, 10 and 11. They check things like: log groups have a retention period, an alarm exists for the dead-letter queue and for each function's errors, every bucket, queue, table and secret is encrypted, policies deny non-TLS requests, no IAM statement uses Resource: *, the queue's visibility timeout is at least six times the worker's timeout, the consumer reports partial batch failures, tables have point-in-time recovery and deletion protection, stateful resources are retained, the API stage throttles, functions are arm64, tables bill per request, and every taggable resource has a project tag. A sample of the rules:

add("OPS1", "Operational excellence", len(lgs) >= len(L) and all("RetentionInDays" in v["Properties"] for v in lgs.values()), f"every Lambda has a log group with a retention period ({len(lgs)} log groups for {len(L)} functions)") add("OPS2", "Operational excellence", has_alarm(t, "ApproximateNumberOfMessagesVisible", "AWS/SQS"), "an alarm watches the dead-letter queue") add("OPS3", "Operational excellence", sum(1 for a in alarms(t) if a["Properties"].get("MetricName") == "Errors" and a["Properties"].get("Namespace") == "AWS/Lambda") >= len(L), "each Lambda function has an Errors alarm") add("OPS4", "Operational excellence", has_alarm(t, "ApproximateAgeOfOldestMessage", "AWS/SQS"), "an alarm shows the consumer falling behind (age of the oldest message)") add("OPS5", "Operational excellence", has_alarm(t, "5xx", "AWS/ApiGateway"), "an alarm watches HTTP API 5xx") add("OPS6", "Operational excellence", all(a["Properties"].get("AlarmActions") for a in alarms(t)), f"every alarm has an action ({len(alarms(t))} alarms)") add("OPS7", "Operational excellence", any(v["Type"] == "AWS::CodeDeploy::DeploymentGroup" for v in t["Resources"].values()),

The result on the draft and on the hardened template:

py wa_review.py cdk.out.draft/OrdersDraft.template.json
FAIL OPS1 Operational excellence every Lambda has a log group with a retention period (0 log groups for 2 functions) pass OPS2 Operational excellence an alarm watches the dead-letter queue FAIL OPS3 Operational excellence each Lambda function has an Errors alarm FAIL OPS4 Operational excellence an alarm shows the consumer falling behind (age of the oldest message) FAIL OPS5 Operational excellence an alarm watches HTTP API 5xx pass OPS6 Operational excellence every alarm has an action (1 alarms) FAIL OPS7 Operational excellence gradual deployment for the functions (small, reversible changes): a CodeDeploy deployment group exists pass SEC1 Security data at rest is encrypted on every bucket, queue, table and secret (5 checked; unencrypted: none) FAIL SEC2 Security data in transit: a policy denies non-TLS requests on every bucket and queue (0 of 3) FAIL SEC3 Security traceability: X-Ray tracing is active on every function pass SEC4 Security no IAM statement with Resource '*' (0 found: []) pass SEC5 Security no long-term credentials (no IAM users or access keys) FAIL SEC6 Security defence in depth at the edge: a web ACL is attached to the distribution pass SEC7 Security every CloudFront behaviour redirects or requires HTTPS pass SEC8 Security all four public-access blocks are on for every bucket pass SEC9 Security automatic rotation is on for every KMS key FAIL SEC10 Security traceability at the edge: CloudFront access logging is on pass REL1 Reliability every work queue has a dead-letter queue FAIL REL2 Reliability queue visibility timeout is at least 6 x the worker timeout (Lambda/SQS page): [30] vs 6 x 30 = 180 pass REL3 Reliability the queue consumer reports partial batch failures pass REL4 Reliability point-in-time recovery is on for every table FAIL REL5 Reliability deletion protection is on for every table pass REL6 Reliability every bucket, table, key and secret has DeletionPolicy Retain FAIL REL7 Reliability the HTTP API stage throttles requests (protects the queue and the functions) FAIL REL8 Reliability the queue consumer has a concurrency cap (protects the table) FAIL REL9 Reliability every bucket is versioned info REL10 Reliability a second Region or a recovery objective: not expressible in one stack (RTO/RPO are not defined here) pass PERF1 Performance efficiency functions run on arm64 (the Lambda pricing page lists a lower Arm price per GB-second) pass PERF2 Performance efficiency serverless compute only (no instances to size) pass PERF3 Performance efficiency the API behaviour uses the managed CachingDisabled policy (id 4135ea2d-...) pass COST1 Cost optimization tables use on-demand billing (a consumption model) pass COST2 Cost optimization CloudFront price class is limited (PriceClass_100) pass COST3 Cost optimization a 'project' tag is on every taggable resource (8 of 8; cost attribution) FAIL COST4 Cost optimization log retention bounds storage cost (same facts as OPS1, counted for cost) info COST5 Cost optimization KMS keys: 1 (the KMS page prices a key at $1 a month) pass SUS1 Sustainability efficient hardware: arm64 functions pass SUS2 Sustainability managed, shared services rather than servers to keep busy FAIL SUS3 Sustainability data you stop keeping stops costing energy: bounded log retention info SUS4 Sustainability Region selection and per-transaction impact measurement: not checkable from a template (those pages were not read) 20 of 36 scored checks pass (3 informational)
py wa_review.py cdk.out.hard/Orders.template.json
pass OPS1 Operational excellence every Lambda has a log group with a retention period (2 log groups for 2 functions) pass OPS2 Operational excellence an alarm watches the dead-letter queue pass OPS3 Operational excellence each Lambda function has an Errors alarm pass OPS4 Operational excellence an alarm shows the consumer falling behind (age of the oldest message)
diff of the two reviews
check draft hardened OPS1 FAIL pass OPS3 FAIL pass OPS4 FAIL pass OPS5 FAIL pass SEC2 FAIL pass SEC3 FAIL pass SEC4 pass FAIL REL2 FAIL pass REL5 FAIL pass REL7 FAIL pass REL8 FAIL pass REL9 FAIL pass COST4 FAIL pass SUS3 FAIL pass 14 checks changed state; still FAIL in both: OPS7, SEC6, SEC10

The draft passed 20 of 36; the hardened version passes 32 of 36. The fixes were ordinary: log retention (and the log groups to hold it), alarms for the functions, the queue's age and the API's 5xx, TLS-only policies on the bucket and queues, X-Ray tracing, a visibility timeout of 185 instead of 30, deletion protection on the table, a throttle on the API stage, a concurrency cap on the consumer and versioning on the site bucket. One fix created a new finding: turning on X-Ray tracing made the CDK add two statements with Resource: *, so SEC4 went from pass to FAIL. That is the framework's trade-off language in a small way: traceability against least privilege. I have not read whether X-Ray actions can be scoped to a resource, so I accepted it and recorded it.

A caution about the score. The rules are mine, and I fixed the stack until it satisfied them. A pass means "conforms to my checklist", not "is well-architected". Each pillar has principles that no template can show: that someone owns the alarm (OPS 10), that restores work (REL 9, REL 12), that the recovery objectives were set from business needs (REL 13), that the data was classified (SEC 7), that cost is reviewed monthly, that the Region was chosen for its impact (the sustainability pages on Region selection were not read).

The four that still fail, and the decisions behind them

CheckFindingDecision
OPS7The functions are deployed all at once; no alias and CodeDeploy group.Fixable with a lambda.Alias and a LambdaDeploymentGroup and the arithmetic of AWS Intermediate/Advanced 11 (a canary shorter than the alarm's detection time protects nothing). Left open to keep the design small.
SEC4Two X-Ray statements with Resource: *, added by the CDK for tracing.Accepted for now; the alternative is to turn tracing off, which fails SEC3.
SEC6No web ACL on the distribution.An open decision about cost ($5 a month per web ACL and $1 per rule from the pricing page, plus request charges) and about where a CloudFront web ACL must be created, which I did not read. Rate limiting at the API stage covers part of it.
SEC10No CloudFront access logs.Not added: logging needs a log bucket with its own policy, and I did not read the page on the requirements.

Three informational items stay open too: no second Region and no stated recovery objectives (REL 13 asks for them), a KMS key at about $1 a month (the KMS page), and Region selection for sustainability (not read). A finding with an owner and a date is a decision; a finding nobody wrote down is a risk.

Does the Review Catch Regressions?

A review that runs once is a document; one that runs on every change is a guardrail. Run here: 29 one-change edits to the hardened template, as if someone had edited the stack (visibility timeout back to 30, redrive removed, recovery off, deletion protection off, encryption removed, a public-access block off, rotation off, allow-all on a behaviour, an alarm without an action, an IAM user added, and so on), each run through cfn-lint and through the review. The review counts as catching a change when a check that passed now fails.

py mutate_wa.py
change lint review (control: unchanged hardened template) 0 0 baseline FAILs: ['OPS7', 'SEC10', 'SEC4', 'SEC6'] work queue visibility timeout 185 -> 30 0 1 REL2 work queue loses its redrive policy 0 1 REL1 worker timeout 30 -> 300 (queue timeout now too short) 1 1 E3505 REL2 table point-in-time recovery off 0 1 REL4 table deletion protection off 0 1 REL5 table encryption removed 0 1 SEC1 table DeletionPolicy Retain -> Delete 0 1 REL6 site bucket BlockPublicPolicy false 0 1 SEC8 KMS key rotation off 0 1 SEC9 CloudFront default behaviour allow-all 0 1 SEC7 CloudFront price class all 0 1 COST2 DLQ alarm loses its action 0 1 OPS6 DLQ alarm removed 0 1 OPS2 consumer loses partial-batch reporting 0 1 REL3 consumer concurrency cap removed 0 1 REL8 worker on x86_64 0 1 PERF1,SUS1 API function tracing PassThrough 0 1 SEC3 API stage throttle removed 0 1 REL7 queue policy's TLS deny removed 0 1 SEC2 an IAM user and access key added 0 1 SEC5 a statement with Action * on Resource * added 0 0 API log group retention removed 0 1 COST4,OPS1,SUS3 table tags removed 0 1 COST3 table billing PROVISIONED without capacity 1 1 E3639 COST1 worker memory 256 -> 64 1 0 E3034 worker timeout 900 -> 901 1 1 E3505,E3717 REL2 work queue retention 14 days -> 0 seconds 1 0 E3034 API route method POST -> POSTT 0 0 event source batch size 10 -> 11 0 0 29 changes: cfn-lint 5, review 24, both 3, neither 3

The review flagged 24 of 29; cfn-lint flagged 5; both 3; neither 3. cfn-lint caught only range and consistency errors (a memory size of 64, a retention of 0 seconds, a provisioned table without capacity, a timeout of 901 seconds) and one meaning rule that I did not expect: E3505 reported that the queue's visibility timeout (185) was less than the function timeout (300). My chapter 9 result for a differently shaped template was that cfn-lint 1.57.2 missed that kind of mismatch; I did not investigate why this one was caught. It does not know the six-times rule, so it passed a visibility timeout of 30. Three changes were missed by both: a statement with Action: * on Resource: * added to the API function's policy, a route method typo (POSTT) and a batch size of 11. The first of those is a design flaw in my harness, not just in the tools: SEC4 already failed on the baseline because of X-Ray, so a new wildcard statement could not turn a pass into a fail. A check that is already failing cannot report a regression. If you accept a finding, put the accepted items in an allowlist so that anything else still fails.

What It Might Cost

The pricing calculator was not used. From the pricing pages (2026-10-13; mostly worked examples, US East (N. Virginia) where a Region is named): HTTP API $1.00 per million requests; Lambda $0.20 per million requests and $0.0000133334 per GB-second on Arm (that figure from one example), with 1 million requests and 400,000 GB-seconds free; DynamoDB on demand $0.625 per million writes, $0.25 per GB stored and $0.20 per GB-month for point-in-time recovery; CloudWatch alarms $0.10 each (10 free), logs $0.50 per GB (5 GB free); KMS $1 per key and Secrets Manager $0.40 per secret. I could not read the SQS request price (only a 1 million requests free tier) or CloudFront's pay-as-you-go prices (the flat-rate plans are Free $0, Pro $15, Business $200 and Premium $1,000 per distribution). A sketch for three volumes, with the assumptions printed:

py cost12.py
== 100,000 orders a month (1 KB item, batch of 10, 100 ms API / 200 ms per message, 256 MB Arm) == API Gateway HTTP API $ 0.10 Lambda requests $ 0.00 (free tier applied; without it $0.02) Lambda duration $ 0.00 (7,500 GB-s; without free tier $0.10) DynamoDB (writes, 0.5 GB stored, PITR) $0.29 CloudWatch alarms $ 0.00 (5 alarms inside the 10-alarm free tier; $0.50 without it) CloudWatch logs $ 0.00 (0.2 GB ingested; $0.10 without the 5 GB free tier) KMS key $1.00 + secret $0.40 (request charges not estimated) priced lines total $ 1.79 (without any free tier $2.51) SQS requests (not priced): 120,000 -> free tier covers 1,000,000 == 1,000,000 orders a month (1 KB item, batch of 10, 100 ms API / 200 ms per message, 256 MB Arm) == API Gateway HTTP API $ 1.00 Lambda requests $ 0.02 (free tier applied; without it $0.22) Lambda duration $ 0.00 (75,000 GB-s; without free tier $1.00) DynamoDB (writes, 0.5 GB stored, PITR) $0.85 CloudWatch alarms $ 0.00 (5 alarms inside the 10-alarm free tier; $0.50 without it) CloudWatch logs $ 0.00 (2.0 GB ingested; $1.00 without the 5 GB free tier) KMS key $1.00 + secret $0.40 (request charges not estimated) priced lines total $ 3.27 (without any free tier $5.97) SQS requests (not priced): 1,200,000 -> free tier covers 1,000,000 == 10,000,000 orders a month (1 KB item, batch of 10, 100 ms API / 200 ms per message, 256 MB Arm) == API Gateway HTTP API $ 10.00 Lambda requests $ 2.00 (free tier applied; without it $2.20) Lambda duration $ 4.67 (750,000 GB-s; without free tier $10.00) DynamoDB (writes, 0.5 GB stored, PITR) $6.47 CloudWatch alarms $ 0.00 (5 alarms inside the 10-alarm free tier; $0.50 without it) CloudWatch logs $ 7.50 (20.0 GB ingested; $10.00 without the 5 GB free tier) KMS key $1.00 + secret $0.40 (request charges not estimated) priced lines total $ 32.04 (without any free tier $40.58) SQS requests (not priced): 12,000,000 -> free tier covers 1,000,000 NOT in these totals: CloudFront (flat-rate plans read: Free $0, Pro $15, Business $200, Premium $1,000 per distribution; pay-as-you-go not read), SQS beyond the free 1M requests, S3 storage and requests, X-Ray, SNS, KMS request charges, data transfer, a WAF web ACL ($5 + $1 per rule + $0.60 per million requests, if you add one).

The priced lines stay small at these volumes (about $1.79, $3.27 and $32.04 a month for 100,000, a million and ten million orders, free tiers applied; the free tiers are not available to every account, and the "without free tier" figures are printed beside them). They leave out CloudFront, SQS beyond its free million requests, S3, X-Ray, SNS, KMS request charges, data transfer and any WAF. At ten million orders the logs ($7.50) and the API ($10) matter more than the table. Cost optimization in the framework starts with measuring: tag everything (check COST3 enforces the project tag) and look at the bill by tag.

If You Have an Account (not run)

Level 2: set a budget alert first (AWS Fundamentals 9). The commands for cdk bootstrap, diff, deploy and destroy are in AWS Intermediate/Advanced 5. Specific to this stack: the key, the site bucket, the table and the secret are RemovalPolicy.RETAIN, so cdk destroy leaves them behind and you must delete them yourself (the key's deletion is scheduled, not immediate; I did not read the waiting-period details here); deletion protection on the table must be turned off before it can be deleted; the site is empty until you upload an index.html; and the first deployment of a distribution takes a while. I give no copy-paste commands for the manual deletions because I did not read the CLI pages for them. Nothing was deployed, so none of this is verified.

Cost and Clean-Up

Nothing in this chapter created anything in AWS. On this machine: the scratch folder C:/lwaws/c12 (a CDK project whose node_modules is a directory junction to chapter 5's, two cdk.out folders and a Python environment). Delete the junction itself, not through it, or delete chapter 5's folder first. moto ran in-process; Docker was not used.

What was and was not checked
Run here: two real syntheses and cfn-lint runs; 8 Jest assertions; a 10-check handler flow test on moto; a 36-check review of both templates; 29 regression edits against cfn-lint and the review; a cost sketch. From the documentation, not run: the framework's pillars, principles and the questions I list; every service's run-time behaviour. Not read: the question pages for most best-practice areas, the Operational Excellence, Security, Reliability, Cost Optimization and Sustainability whitepapers beyond their introductions, the AWS Pricing Calculator, SQS and CloudFront pay-as-you-go prices, how a CloudFront web ACL must be created, X-Ray resource scoping, KMS key deletion details. Not checked at all: any deployment, any real request, any restore, any alarm firing.

Course Complete

Twelve chapters, and one question asked of every example: how do you know? The levels were: level 1 run on this machine without an account, level 2 needs an account, level 3 from the documentation. The recurring results, in the order you met them:

ChapterWhat the tools could checkWhat they missed
1 to 3 Accounts, IAM, KMSPolicy evaluation models tested on documented cases; policy lintingMuch of the real evaluation (resource policies with wildcard principals, RCPs); how plain negated condition operators combine values was unverified
4 CloudFormationcfn-lint caught 18 of 22 mistakesFour meaning errors (an Fn::Sub in a condition, a bad mapping key)
5 and 6 CDK, containerscdk synth for real; containers measured in Dockersynth exited 0 on an invalid Fargate size (only cfn-lint flagged it)
7 EKSeksctl's schema caught 7 of 10 config mistakes; cfn-lint 10 of 15The kind-cluster experiments could not run (Docker stopped)
8 API Gatewaysam local enforced routing, authorizers and CORSRequest validation, API keys, throttling and the JWT authorizer were not enforced locally; cfn-lint caught 10 of 25 (1.53.3)
9 Messagingmoto matched SQS and SNS behaviour; Step Functions Local reproduced retry timing exactlymoto enforced 6 of 12 SQS limits; cfn-lint caught 15 of 30
10 Edge and DNSModels of the documented tables; a real DNS parsermoto accepted all 9 invalid inputs; cfn-lint caught 15 of 32
11 Deliveryactionlint caught 14 of 26 workflow mistakes; a trust-policy model showed what a policy admitscfn-lint caught 8 of 26 and 5 of 16; moto accepted oversize parameters
12 CapstoneA 36-check review caught 24 of 29 regressionscfn-lint caught 5 of 29; the review's own accepted finding hid a regression

Three habits carry over. Linters check ranges and types, not meaning: keep your own checks for the relationships between resources, and write them from the documentation before you see the results. Emulators are lax: use them to test that your code calls an API and handles its answers, never to learn what is valid. Say how you know: a statement from the documentation, a model of the documentation, a test against an emulator and an observation of the real service are four different things, and this course labelled which was which. What it could not do is the part that needs your account: deploy, watch a real alarm fire, restore a table, read a real bill.

Where next. Kubernetes Intermediate/Advanced continues the container material that chapter 7 sketched for EKS; Terraform / Infrastructure as Code is the other way to write what chapters 4 and 5 wrote in CloudFormation and CDK; Observability goes deeper into the alarms, logs and traces that this chapter only counted. A third AWS course (data and analytics, machine learning or networking in depth) can follow the same pattern.

Practice

Exercise 1

Define the order-intake system as a CDK stack in TypeScript with one hardened flag. Synthesise both versions without credentials, lint them, count the resources, write eight Jest assertions, and run the two Lambda handlers against an emulator, including a redelivered message and a poison message.

๐Ÿ“„ View solution
Exercise 2

Write a checker that reviews a synthesised template against the six Well-Architected pillars (write the rules before you run them), run it on the draft and the hardened stack, explain every check that changed state (including the one that got worse) and record the decisions behind the checks that still fail.

๐Ÿ“„ View solution
Exercise 3

Make 29 one-change regressions of the hardened template and score cfn-lint and your review on them; work out why the new wildcard statement was not caught and design an allowlist for accepted findings. Sketch the monthly cost for three volumes from the pricing pages, with free tiers shown both ways, and list what you could not price.

๐Ÿ“„ View solution

Chapter 12 Quick Reference

  • One construct, one hardened flag: the difference between draft and hardened templates is exactly what the review found (20 of 36 checks passed, then 32 of 36)
  • The six pillars and their principles are on the framework pages; a template can show encryption, retention, alarms, throttles, deletion protection and tags, not ownership, restores, recovery objectives or data classification
  • A TLS minimum on CloudFront needs a custom certificate: the CDK says the default certificate's policy is fixed at TLSv1
  • Fixing one finding can create another: tracing added wildcard X-Ray statements and SEC4 went from pass to FAIL
  • A check that already fails cannot report a regression: keep accepted findings in an allowlist so anything else still fails
  • On 29 regressions the review caught 24 and cfn-lint 5; both caught 3 and neither caught 3; cfn-lint's E3505 compares the queue's visibility timeout with the function timeout but not the six-times rule
  • Cost sketch (priced lines only, free tiers applied): about $1.79, $3.27 and $32.04 a month at 100,000, 1 million and 10 million orders; CloudFront, SQS beyond the free tier, S3, X-Ray, SNS and data transfer are not in it
  • Retained resources (key, bucket, table, secret) survive cdk destroy; delete them by hand, and turn off the table's deletion protection first
Course complete
This is the last chapter of AWS Intermediate/Advanced. Pointers: Kubernetes Intermediate/Advanced, Terraform / Infrastructure as Code and Observability.