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.
- 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,deployanddestroy. The commands are in AWS Intermediate/Advanced 5; they were not run.
The Design
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:
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.
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.
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.
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:
| Pillar | Its 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 services | Organization, 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 events | Security 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 automation | Foundations, Workload architecture, Change management, Failure management |
| Performance efficiency (5) | Democratize advanced technologies; Go global in minutes; Use serverless architectures; Experiment more often; Consider mechanical sympathy | Architecture 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 expenditure | Practice 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 workloads | Region 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:
The result on the draft and on the hardened template:
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
| Check | Finding | Decision |
|---|---|---|
| OPS7 | The 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. |
| SEC4 | Two X-Ray statements with Resource: *, added by the CDK for tracing. | Accepted for now; the alternative is to turn tracing off, which fails SEC3. |
| SEC6 | No 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. |
| SEC10 | No 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.
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:
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.
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:
| Chapter | What the tools could check | What they missed |
|---|---|---|
| 1 to 3 Accounts, IAM, KMS | Policy evaluation models tested on documented cases; policy linting | Much of the real evaluation (resource policies with wildcard principals, RCPs); how plain negated condition operators combine values was unverified |
| 4 CloudFormation | cfn-lint caught 18 of 22 mistakes | Four meaning errors (an Fn::Sub in a condition, a bad mapping key) |
| 5 and 6 CDK, containers | cdk synth for real; containers measured in Docker | synth exited 0 on an invalid Fargate size (only cfn-lint flagged it) |
| 7 EKS | eksctl's schema caught 7 of 10 config mistakes; cfn-lint 10 of 15 | The kind-cluster experiments could not run (Docker stopped) |
| 8 API Gateway | sam local enforced routing, authorizers and CORS | Request validation, API keys, throttling and the JWT authorizer were not enforced locally; cfn-lint caught 10 of 25 (1.53.3) |
| 9 Messaging | moto matched SQS and SNS behaviour; Step Functions Local reproduced retry timing exactly | moto enforced 6 of 12 SQS limits; cfn-lint caught 15 of 30 |
| 10 Edge and DNS | Models of the documented tables; a real DNS parser | moto accepted all 9 invalid inputs; cfn-lint caught 15 of 32 |
| 11 Delivery | actionlint caught 14 of 26 workflow mistakes; a trust-policy model showed what a policy admits | cfn-lint caught 8 of 26 and 5 of 16; moto accepted oversize parameters |
| 12 Capstone | A 36-check review caught 24 of 29 regressions | cfn-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
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.
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 solutionMake 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 solutionChapter 12 Quick Reference
- One construct, one
hardenedflag: 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
E3505compares 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