aws2-12 Exercise 2: A Well-Architected Template Review: Draft Against Hardened ======================================================================== 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. --- wa_review.py --- """Well-Architected review of a synthesised CloudFormation template: checks mapped to the framework's six pillars. Each check names the DESIGN PRINCIPLE (framework pages read 2026-10-13) or the earlier chapter whose rule it applies. THE RULES WERE WRITTEN BEFORE THE FIRST RUN on either template. They check what a template can show; they cannot show that a runbook exists, that restores work, or that anyone reads the alarm. check(template_dict) -> list of (id, pillar, status, text).""" import json, re, sys def res_of(t, *types): return {k: v for k, v in t["Resources"].items() if v["Type"] in types} def lam(t): return {k: v for k, v in res_of(t, "AWS::Lambda::Function").items() if "Custom::" not in k} def alarms(t): return list(res_of(t, "AWS::CloudWatch::Alarm").values()) def has_alarm(t, metric, ns=None): return any(a["Properties"].get("MetricName") == metric and (ns is None or a["Properties"].get("Namespace") == ns) for a in alarms(t)) def statements(t): for k, v in t["Resources"].items(): if v["Type"] == "AWS::IAM::Policy": for s in v["Properties"]["PolicyDocument"]["Statement"]: yield k, s def aslist(x): return x if isinstance(x, list) else [x] def check(t): out = [] def add(i, p, ok, text, info=False): out.append((i, p, "info" if info else ("pass" if ok else "FAIL"), text)) L = lam(t); buckets = res_of(t, "AWS::S3::Bucket"); queues = res_of(t, "AWS::SQS::Queue"); tables = res_of(t, "AWS::DynamoDB::Table") secrets = res_of(t, "AWS::SecretsManager::Secret"); keys = res_of(t, "AWS::KMS::Key"); lgs = res_of(t, "AWS::Logs::LogGroup") dist = list(res_of(t, "AWS::CloudFront::Distribution").values())[0]["Properties"]["DistributionConfig"] # ---- Operational excellence 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()), "gradual deployment for the functions (small, reversible changes): a CodeDeploy deployment group exists") # ---- Security def enc(v): p = v["Properties"]; ty = v["Type"] return ("BucketEncryption" in p) if ty == "AWS::S3::Bucket" else ("KmsMasterKeyId" in p or p.get("SqsManagedSseEnabled")) if ty == "AWS::SQS::Queue" else ("SSESpecification" in p) if ty == "AWS::DynamoDB::Table" else ("KmsKeyId" in p) stores = {**buckets, **queues, **tables, **secrets} unenc = [k for k, v in stores.items() if not enc(v)] add("SEC1", "Security", not unenc, f"data at rest is encrypted on every bucket, queue, table and secret ({len(stores)} checked; unencrypted: {unenc or 'none'})") def deny_tls(pol): for s in pol["PolicyDocument"]["Statement"]: if s.get("Effect") == "Deny" and json.dumps(s.get("Condition", {})).find("aws:SecureTransport") >= 0: return True return False pols = [v for v in t["Resources"].values() if v["Type"] in ("AWS::S3::BucketPolicy", "AWS::SQS::QueuePolicy")] n_tls = sum(deny_tls(p["Properties"]) for p in pols) add("SEC2", "Security", n_tls >= len(buckets) + len(queues), f"data in transit: a policy denies non-TLS requests on every bucket and queue ({n_tls} of {len(buckets) + len(queues)})") add("SEC3", "Security", all(v["Properties"].get("TracingConfig", {}).get("Mode") == "Active" for v in L.values()), "traceability: X-Ray tracing is active on every function") star = [(k, s) for k, s in statements(t) if "*" in aslist(s.get("Resource", []))] add("SEC4", "Security", not star, f"no IAM statement with Resource '*' ({len(star)} found: {sorted({a for _, s in star for a in aslist(s['Action'])})})") add("SEC5", "Security", not res_of(t, "AWS::IAM::User", "AWS::IAM::AccessKey"), "no long-term credentials (no IAM users or access keys)") add("SEC6", "Security", any(v["Type"].startswith("AWS::WAF") for v in t["Resources"].values()) or bool(dist.get("WebACLId")), "defence in depth at the edge: a web ACL is attached to the distribution") behaviours = [dist["DefaultCacheBehavior"]] + dist.get("CacheBehaviors", []) add("SEC7", "Security", all(b["ViewerProtocolPolicy"] != "allow-all" for b in behaviours), "every CloudFront behaviour redirects or requires HTTPS") pab = lambda v: all(v["Properties"].get("PublicAccessBlockConfiguration", {}).get(k) is True for k in ("BlockPublicAcls", "BlockPublicPolicy", "IgnorePublicAcls", "RestrictPublicBuckets")) add("SEC8", "Security", all(pab(v) for v in buckets.values()), "all four public-access blocks are on for every bucket") add("SEC9", "Security", all(v["Properties"].get("EnableKeyRotation") for v in keys.values()), "automatic rotation is on for every KMS key") add("SEC10", "Security", bool(dist.get("Logging")), "traceability at the edge: CloudFront access logging is on") # ---- Reliability dlq_ok = all("RedrivePolicy" in v["Properties"] for k, v in queues.items() if "Dlq" not in k) add("REL1", "Reliability", dlq_ok, "every work queue has a dead-letter queue") wt = max((v["Properties"].get("Timeout", 3) for v in L.values() if "Worker" in v["Properties"].get("Handler", "") or "worker" in v["Properties"].get("Handler", "")), default=3) vis = [v["Properties"].get("VisibilityTimeout", 30) for k, v in queues.items() if "Dlq" not in k] add("REL2", "Reliability", all(x >= 6 * wt for x in vis), f"queue visibility timeout is at least 6 x the worker timeout (Lambda/SQS page): {vis} vs 6 x {wt} = {6*wt}") esm = list(res_of(t, "AWS::Lambda::EventSourceMapping").values()) add("REL3", "Reliability", all("ReportBatchItemFailures" in e["Properties"].get("FunctionResponseTypes", []) for e in esm), "the queue consumer reports partial batch failures") add("REL4", "Reliability", all(v["Properties"].get("PointInTimeRecoverySpecification", {}).get("PointInTimeRecoveryEnabled") for v in tables.values()), "point-in-time recovery is on for every table") add("REL5", "Reliability", all(v["Properties"].get("DeletionProtectionEnabled") for v in tables.values()), "deletion protection is on for every table") stateful = {**buckets, **tables, **keys, **secrets} add("REL6", "Reliability", all(v.get("DeletionPolicy") == "Retain" for v in stateful.values()), "every bucket, table, key and secret has DeletionPolicy Retain") stg = list(res_of(t, "AWS::ApiGatewayV2::Stage").values())[0]["Properties"] add("REL7", "Reliability", bool(stg.get("DefaultRouteSettings", {}).get("ThrottlingRateLimit")), "the HTTP API stage throttles requests (protects the queue and the functions)") add("REL8", "Reliability", all("ScalingConfig" in e["Properties"] for e in esm), "the queue consumer has a concurrency cap (protects the table)") add("REL9", "Reliability", all(v["Properties"].get("VersioningConfiguration", {}).get("Status") == "Enabled" for v in buckets.values()), "every bucket is versioned") add("REL10", "Reliability", False, "a second Region or a recovery objective: not expressible in one stack (RTO/RPO are not defined here)", info=True) # ---- Performance efficiency add("PERF1", "Performance efficiency", all(v["Properties"].get("Architectures") == ["arm64"] for v in L.values()), "functions run on arm64 (the Lambda pricing page lists a lower Arm price per GB-second)") add("PERF2", "Performance efficiency", not res_of(t, "AWS::EC2::Instance", "AWS::ECS::Service", "AWS::AutoScaling::AutoScalingGroup"), "serverless compute only (no instances to size)") cp = dist.get("CacheBehaviors", [{}])[0].get("CachePolicyId") add("PERF3", "Performance efficiency", cp == "4135ea2d-6df8-44a3-9df3-4b5a84be39ad", "the API behaviour uses the managed CachingDisabled policy (id 4135ea2d-...)") # ---- Cost optimisation add("COST1", "Cost optimization", all(v["Properties"].get("BillingMode") == "PAY_PER_REQUEST" for v in tables.values()), "tables use on-demand billing (a consumption model)") add("COST2", "Cost optimization", dist.get("PriceClass") != "PriceClass_All", f"CloudFront price class is limited ({dist.get('PriceClass')})") taggable = {**buckets, **queues, **tables, **secrets, **keys, **L} untagged = [k for k, v in taggable.items() if not any(x.get("Key") == "project" for x in v["Properties"].get("Tags", []) if isinstance(x, dict))] add("COST3", "Cost optimization", not untagged, f"a 'project' tag is on every taggable resource ({len(taggable)-len(untagged)} of {len(taggable)}; cost attribution)") add("COST4", "Cost optimization", all("RetentionInDays" in v["Properties"] for v in lgs.values()) and len(lgs) >= len(L), "log retention bounds storage cost (same facts as OPS1, counted for cost)") add("COST5", "Cost optimization", True, f"KMS keys: {len(keys)} (the KMS page prices a key at $1 a month)", info=True) # ---- Sustainability add("SUS1", "Sustainability", all(v["Properties"].get("Architectures") == ["arm64"] for v in L.values()), "efficient hardware: arm64 functions") add("SUS2", "Sustainability", not res_of(t, "AWS::EC2::Instance", "AWS::AutoScaling::AutoScalingGroup"), "managed, shared services rather than servers to keep busy") add("SUS3", "Sustainability", all("RetentionInDays" in v["Properties"] for v in lgs.values()) and len(lgs) >= len(L), "data you stop keeping stops costing energy: bounded log retention") add("SUS4", "Sustainability", False, "Region selection and per-transaction impact measurement: not checkable from a template (those pages were not read)", info=True) return out if __name__ == "__main__": t = json.load(open(sys.argv[1], encoding="utf-8")) r = check(t) for i, p, s, x in r: print(f"{s:5s} {i:6s} {p:24s} {x}") sc = [x for x in r if x[2] != "info"] print(f"\n{sum(1 for x in sc if x[2]=='pass')} of {len(sc)} scored checks pass ({len(r)-len(sc)} informational)") --- py wa_review.py cdk.out.draft/OrdersDraft.template.json (saved output of the real run) --- 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 (saved output of the real run) --- 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) pass OPS5 Operational excellence an alarm watches HTTP API 5xx pass OPS6 Operational excellence every alarm has an action (5 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) pass SEC2 Security data in transit: a policy denies non-TLS requests on every bucket and queue (3 of 3) pass SEC3 Security traceability: X-Ray tracing is active on every function FAIL SEC4 Security no IAM statement with Resource '*' (2 found: ['xray:PutTelemetryRecords', 'xray:PutTraceSegments']) 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 pass REL2 Reliability queue visibility timeout is at least 6 x the worker timeout (Lambda/SQS page): [185] 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 pass REL5 Reliability deletion protection is on for every table pass REL6 Reliability every bucket, table, key and secret has DeletionPolicy Retain pass REL7 Reliability the HTTP API stage throttles requests (protects the queue and the functions) pass REL8 Reliability the queue consumer has a concurrency cap (protects the table) pass 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) pass 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 pass 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) 32 of 36 scored checks pass (3 informational) --- checks that changed state (saved output of the real run) --- 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 -------------------------- Reading the result: the draft passes 20 of 36 scored checks, the hardened stack 32. 14 checks changed state: 13 FAIL to pass and 1 pass to FAIL (SEC4: turning on X-Ray tracing made the CDK add statements with Resource '*'). Still failing: OPS7 (no gradual deployment), SEC4, SEC6 (no web ACL), SEC10 (no CloudFront access logs). The rules were written before the first run on either template, but I then fixed the stack until it satisfied them, so a pass means 'conforms to my checklist', not 'is well-architected'. I read only some of the framework's questions (SEC 2/3/7-9, REL 3-5 and 9-13, OPS 8-10, PERF 2, COST 5-8, SUS 5), so this is not a complete framework review. Mistake to avoid: do not report a checklist score as a Well-Architected review; the process and people questions cannot be answered from a template.