aws2-5 Exercise 1: Rebuild the CloudFormation Stack in the CDK, Synthesize It, and Compare ============================================================================================= Run with Node 24.18.0 on Windows 11 in a scratch folder (not installed globally): aws-cdk 2.1145.0 (CLI), aws-cdk-lib 2.273.0, constructs 10.8.1, typescript 7.0.2, jest 30.5.2, @types/jest, @types/node; cfn-lint 1.57.2 from the chapter 4 environment. Setup: npm install --save-exact aws-cdk@2.1145.0 aws-cdk-lib@2.273.0 constructs@10.8.1 typescript@7.0.2 jest@30.5.2 @types/jest @types/node ; build with: node node_modules/typescript/bin/tsc -p tsconfig.json ; synth with: node node_modules/aws-cdk/bin/cdk synth -c environment=dev ; test with: node node_modules/jest/bin/jest.js. VERIFICATION LEVEL: 1 (run here; no AWS account, no AWS credentials, nothing deployed). Outputs below are saved from the real runs of 2026-10-13 because some steps (the failing test, the narrowed grants, the snapshot update) happen in sequence and cannot be replayed by one command. --- package.json --- { "name": "reports-cdk", "version": "1.0.0", "private": true, "scripts": { "build": "tsc -p tsconfig.json", "test": "node node_modules/jest/bin/jest.js" }, "jest": { "testMatch": [ "**/dist/test/**/*.test.js" ], "testEnvironment": "node" }, "dependencies": { "@types/jest": "30.0.0", "@types/node": "26.6.5", "aws-cdk": "2.1145.0", "aws-cdk-lib": "2.273.0", "constructs": "10.8.1", "jest": "30.5.2", "typescript": "7.0.2" } } --- tsconfig.json (TypeScript 7 rejects moduleResolution 'node'; this uses node16) --- { "compilerOptions": { "target": "ES2022", "module": "node16", "moduleResolution": "node16", "lib": ["es2022"], "outDir": "dist", "rootDir": ".", "strict": true, "noUnusedLocals": true, "noImplicitReturns": true, "esModuleInterop": true, "skipLibCheck": true, "types": ["node", "jest"] }, "include": ["bin/**/*.ts", "lib/**/*.ts", "test/**/*.ts"], "exclude": ["node_modules", "dist", "cdk.out"] } --- cdk.json --- { "app": "node dist/bin/app.js", "context": { "environment": "dev" } } --- bin/app.ts --- import * as cdk from "aws-cdk-lib"; import { RequireBucketVersioning } from "../lib/aspects"; import { Environment, ReportsStack } from "../lib/reports-stack"; const app = new cdk.App(); const environment = (app.node.tryGetContext("environment") ?? "dev") as Environment; const retentionDays = Number(app.node.tryGetContext("retentionDays") ?? 30); const keyAdminRoleArn = app.node.tryGetContext("keyAdminRoleArn") ?? "arn:aws:iam::111122223333:role/KeyAdmin"; const account = app.node.tryGetContext("account"); // set both to make the stack environment-specific; leave them out for an environment-agnostic stack new ReportsStack(app, `Reports-${environment}`, { environment, retentionDays, keyAdminRoleArn, env: account ? { account, region: app.node.tryGetContext("region") ?? "eu-west-1" } : undefined, }); cdk.Tags.of(app).add("project", "reports"); cdk.Aspects.of(app).add(new RequireBucketVersioning()); --- lib/reports-stack.ts (final version: the first version used bucket.grantReadWrite(role)) --- import * as cdk from "aws-cdk-lib"; import { Construct } from "constructs"; import * as cloudwatch from "aws-cdk-lib/aws-cloudwatch"; import * as iam from "aws-cdk-lib/aws-iam"; import * as kms from "aws-cdk-lib/aws-kms"; import * as s3 from "aws-cdk-lib/aws-s3"; import * as sqs from "aws-cdk-lib/aws-sqs"; export type Environment = "dev" | "staging" | "prod"; // the same table the CloudFormation template kept in its Mappings section; here it is an ordinary object, read while the app runs export const SETTINGS: Record = { dev: { visibilityTimeout: 30, maxReceive: 3 }, staging: { visibilityTimeout: 60, maxReceive: 5 }, prod: { visibilityTimeout: 120, maxReceive: 5 }, }; export interface ReportsStackProps extends cdk.StackProps { readonly environment: Environment; readonly retentionDays: number; readonly keyAdminRoleArn: string; } export class ReportsStack extends cdk.Stack { public readonly bucket: s3.Bucket; public readonly queue: sqs.Queue; public readonly key: kms.Key; constructor(scope: Construct, id: string, props: ReportsStackProps) { super(scope, id, props); const s = SETTINGS[props.environment]; if (props.retentionDays < 1 || props.retentionDays > 3650) { throw new Error(`retentionDays must be 1 to 3650, got ${props.retentionDays}`); // a parameter constraint, checked at synth time } this.key = new kms.Key(this, "DataKey", { description: `Data key for ${this.stackName} (${props.environment})`, enableKeyRotation: true, alias: `alias/${this.stackName}-data`, removalPolicy: cdk.RemovalPolicy.RETAIN, }); this.key.addToResourcePolicy(new iam.PolicyStatement({ sid: "Allow access for Key Administrators", principals: [new iam.ArnPrincipal(props.keyAdminRoleArn)], actions: ["kms:Create*", "kms:Describe*", "kms:Enable*", "kms:List*", "kms:Put*", "kms:Update*", "kms:Revoke*", "kms:Disable*", "kms:Get*", "kms:Delete*", "kms:TagResource", "kms:UntagResource", "kms:ScheduleKeyDeletion", "kms:CancelKeyDeletion"], resources: ["*"], })); this.bucket = new s3.Bucket(this, "ReportBucket", { encryption: s3.BucketEncryption.KMS, encryptionKey: this.key, bucketKeyEnabled: true, versioned: true, blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, enforceSSL: true, lifecycleRules: [{ id: "expire-reports", expiration: cdk.Duration.days(props.retentionDays) }], removalPolicy: cdk.RemovalPolicy.RETAIN, }); const dlq = new sqs.Queue(this, "WorkDeadLetterQueue", { retentionPeriod: cdk.Duration.days(14), encryption: sqs.QueueEncryption.KMS, encryptionMasterKey: this.key, }); this.queue = new sqs.Queue(this, "WorkQueue", { visibilityTimeout: cdk.Duration.seconds(s.visibilityTimeout), encryption: sqs.QueueEncryption.KMS, encryptionMasterKey: this.key, deadLetterQueue: { queue: dlq, maxReceiveCount: s.maxReceive }, }); const role = new iam.Role(this, "WorkerRole", { assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com") }); this.queue.grantConsumeMessages(role); // intent-based grants: CDK writes the IAM statements this.bucket.grantRead(role); // grantReadWrite also allows s3:DeleteObject*: the worker only reads and writes this.bucket.grantPut(role); if (props.environment === "prod") { // an ordinary 'if', evaluated when the app runs: no CloudFormation Condition is emitted const alarm = new cloudwatch.Alarm(this, "BacklogAlarm", { alarmDescription: "Work queue backlog is growing", metric: this.queue.metricApproximateNumberOfMessagesVisible({ period: cdk.Duration.minutes(5), statistic: "Maximum" }), threshold: 1000, evaluationPeriods: 1, comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD, treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING, }); new cdk.CfnOutput(this, "AlarmName", { value: alarm.alarmName, description: "Backlog alarm (production only)" }); } new cdk.CfnOutput(this, "BucketName", { value: this.bucket.bucketName, description: "Name of the report bucket", exportName: `${this.stackName}-BucketName` }); new cdk.CfnOutput(this, "QueueUrl", { value: this.queue.queueUrl, description: "URL of the work queue" }); new cdk.CfnOutput(this, "KeyArn", { value: this.key.keyArn, description: "ARN of the data key" }); } } --- cdk synth -c environment=dev: messages on the error stream (the template went to a file) --- AI agent detected 84 feature flags are not configured. Run 'cdk flags --unstable=flags' to learn more. (exit status 0; about 18 s the first time, 7 to 8 s later) --- cdk synth -c environment=dev (the generated template, YAML, 372 lines) (saved output of the real run) --- Resources: DataKey17AFBB84: Type: AWS::KMS::Key Properties: Description: Data key for Reports-dev (dev) EnableKeyRotation: true KeyPolicy: Statement: - Action: kms:* Effect: Allow Principal: AWS: Fn::Join: - "" - - "arn:" - Ref: AWS::Partition - ":iam::" - Ref: AWS::AccountId - :root Resource: "*" - Action: - kms:Create* - kms:Describe* - kms:Enable* - kms:List* - kms:Put* - kms:Update* - kms:Revoke* - kms:Disable* - kms:Get* - kms:Delete* - kms:TagResource - kms:UntagResource - kms:ScheduleKeyDeletion - kms:CancelKeyDeletion Effect: Allow Principal: AWS: arn:aws:iam::111122223333:role/KeyAdmin Resource: "*" Sid: Allow access for Key Administrators Version: "2012-10-17" Tags: - Key: project Value: reports UpdateReplacePolicy: Retain DeletionPolicy: Retain Metadata: aws:cdk:path: Reports-dev/DataKey/Resource DataKeyAliasF989EB84: Type: AWS::KMS::Alias Properties: AliasName: alias/Reports-dev-data TargetKeyId: Fn::GetAtt: - DataKey17AFBB84 - Arn Metadata: aws:cdk:path: Reports-dev/DataKey/Alias/Resource ReportBucket577F0FCD: Type: AWS::S3::Bucket Properties: BucketEncryption: ServerSideEncryptionConfiguration: - BucketKeyEnabled: true ServerSideEncryptionByDefault: KMSMasterKeyID: Fn::GetAtt: - DataKey17AFBB84 - Arn SSEAlgorithm: aws:kms LifecycleConfiguration: Rules: - ExpirationInDays: 30 Id: expire-reports Status: Enabled PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true Tags: - Key: project Value: reports VersioningConfiguration: Status: Enabled UpdateReplacePolicy: Retain DeletionPolicy: Retain Metadata: aws:cdk:path: Reports-dev/ReportBucket/Resource ReportBucketPolicyA15D85AD: Type: AWS::S3::BucketPolicy Properties: Bucket: Ref: ReportBucket577F0FCD PolicyDocument: Statement: - Action: s3:* Condition: Bool: aws:SecureTransport: "false" Effect: Deny Principal: AWS: "*" Resource: - Fn::GetAtt: - ReportBucket577F0FCD - Arn - Fn::Join: - "" - - Fn::GetAtt: - ReportBucket577F0FCD - Arn - /* Version: "2012-10-17" Metadata: aws:cdk:path: Reports-dev/ReportBucket/Policy/Resource WorkDeadLetterQueue12144CB6: Type: AWS::SQS::Queue Properties: KmsMasterKeyId: Fn::GetAtt: - DataKey17AFBB84 - Arn MessageRetentionPeriod: 1209600 Tags: - Key: project Value: reports UpdateReplacePolicy: Delete DeletionPolicy: Delete Metadata: aws:cdk:path: Reports-dev/WorkDeadLetterQueue/Resource WorkQueue94013F35: Type: AWS::SQS::Queue Properties: KmsMasterKeyId: Fn::GetAtt: - DataKey17AFBB84 - Arn RedrivePolicy: deadLetterTargetArn: Fn::GetAtt: - WorkDeadLetterQueue12144CB6 - Arn maxReceiveCount: 3 Tags: - Key: project Value: reports VisibilityTimeout: 30 UpdateReplacePolicy: Delete DeletionPolicy: Delete Metadata: aws:cdk:path: Reports-dev/WorkQueue/Resource WorkerRole8DD27D41: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Action: sts:AssumeRole Effect: Allow Principal: Service: lambda.amazonaws.com Version: "2012-10-17" Tags: - Key: project Value: reports Metadata: aws:cdk:path: Reports-dev/WorkerRole/Resource WorkerRoleDefaultPolicy1750E153: Type: AWS::IAM::Policy Properties: PolicyDocument: Statement: - Action: - sqs:ReceiveMessage - sqs:ChangeMessageVisibility - sqs:GetQueueUrl - sqs:DeleteMessage - sqs:GetQueueAttributes Effect: Allow Resource: Fn::GetAtt: - WorkQueue94013F35 - Arn - Action: kms:Decrypt Effect: Allow Resource: Fn::GetAtt: - DataKey17AFBB84 - Arn - Action: - s3:GetObject* - s3:GetBucket* - s3:List* Effect: Allow Resource: - Fn::GetAtt: - ReportBucket577F0FCD - Arn - Fn::Join: - "" - - Fn::GetAtt: - ReportBucket577F0FCD - Arn - /* - Action: - kms:Decrypt - kms:DescribeKey Effect: Allow Resource: Fn::GetAtt: - DataKey17AFBB84 - Arn - Action: - s3:PutObject - s3:PutObjectLegalHold - s3:PutObjectRetention - s3:PutObjectTagging - s3:PutObjectVersionTagging - s3:Abort* Effect: Allow Resource: Fn::Join: - "" - - Fn::GetAtt: - ReportBucket577F0FCD - Arn - /* - Action: - kms:Encrypt - kms:ReEncrypt* - kms:GenerateDataKey* - kms:Decrypt Effect: Allow Resource: Fn::GetAtt: - DataKey17AFBB84 - Arn Version: "2012-10-17" PolicyName: WorkerRoleDefaultPolicy1750E153 Roles: - Ref: WorkerRole8DD27D41 Metadata: aws:cdk:path: Reports-dev/WorkerRole/DefaultPolicy/Resource CDKMetadata: Type: AWS::CDK::Metadata Properties: Analytics: v2:deflate64:H4sIAAAAAAAA/0WLQQ6CMBREz8K+fBFMdKsuWah4AFNLTT5tQf2thDS9u2khYfXezGRKKPcVFBkfKRetyjU+wd8tF4rxkR5eGQJfy4mdX33EUSOnGJIERhX4kxNK2lguNuM6aBTTWs85MPoQ+JuTTsYtSWDIDfhm0KlLXO/LMUS/OPt2NrB+aCV0tPmVO9geoMg6Qsy/rrdoJDQz/74PUpfdAAAA Metadata: aws:cdk:path: Reports-dev/CDKMetadata/Default Condition: CDKMetadataAvailable Outputs: BucketName: Description: Name of the report bucket Value: Ref: ReportBucket577F0FCD Export: Name: Reports-dev-BucketName QueueUrl: Description: URL of the work queue Value: Ref: WorkQueue94013F35 KeyArn: Description: ARN of the data key Value: Fn::GetAtt: - DataKey17AFBB84 - Arn Conditions: CDKMetadataAvailable: Fn::Or: - Fn::Or: - Fn::Equals: - Ref: AWS::Region - af-south-1 - Fn::Equals: - Ref: AWS::Region - ap-east-1 - Fn::Equals: - Ref: AWS::Region - ap-northeast-1 - Fn::Equals: - Ref: AWS::Region - ap-northeast-2 - Fn::Equals: - Ref: AWS::Region - ap-northeast-3 - Fn::Equals: - Ref: AWS::Region - ap-south-1 - Fn::Equals: - Ref: AWS::Region - ap-south-2 - Fn::Equals: - Ref: AWS::Region - ap-southeast-1 - Fn::Equals: - Ref: AWS::Region - ap-southeast-2 - Fn::Equals: - Ref: AWS::Region - ap-southeast-3 - Fn::Or: - Fn::Equals: - Ref: AWS::Region - ap-southeast-4 - Fn::Equals: - Ref: AWS::Region - ca-central-1 - Fn::Equals: - Ref: AWS::Region - ca-west-1 - Fn::Equals: - Ref: AWS::Region - cn-north-1 - Fn::Equals: - Ref: AWS::Region - cn-northwest-1 - Fn::Equals: - Ref: AWS::Region - eu-central-1 - Fn::Equals: - Ref: AWS::Region - eu-central-2 - Fn::Equals: - Ref: AWS::Region - eu-north-1 - Fn::Equals: - Ref: AWS::Region - eu-south-1 - Fn::Equals: - Ref: AWS::Region - eu-south-2 - Fn::Or: - Fn::Equals: - Ref: AWS::Region - eu-west-1 - Fn::Equals: - Ref: AWS::Region - eu-west-2 - Fn::Equals: - Ref: AWS::Region - eu-west-3 - Fn::Equals: - Ref: AWS::Region - il-central-1 - Fn::Equals: - Ref: AWS::Region - me-central-1 - Fn::Equals: - Ref: AWS::Region - me-south-1 - Fn::Equals: - Ref: AWS::Region - sa-east-1 - Fn::Equals: - Ref: AWS::Region - us-east-1 - Fn::Equals: - Ref: AWS::Region - us-east-2 - Fn::Equals: - Ref: AWS::Region - us-west-1 - Fn::Equals: - Ref: AWS::Region - us-west-2 Parameters: BootstrapVersion: Type: AWS::SSM::Parameter::Value Default: /cdk-bootstrap/hnb659fds/version Description: Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip] --- compare_templates.py (run with the chapter 4 environment's python: C:/lwaws/c4/.venv/Scripts/python.exe compare_templates.py) --- """compare_templates.py - the CloudFormation templates `cdk synth` generated, against the hand-written template from chapter 4. Run with the chapter 4 virtual environment (it has cfn-lint and the chapter 4 modules): C:/lwaws/c4/.venv/Scripts/python.exe compare_templates.py Everything here is read from real files: the hand-written app_stack.yaml, and the templates cdk synth wrote (out/Reports-.template.json). """ import collections, json, pathlib, subprocess, sys sys.path.insert(0, "C:/lwaws/c4") from cfn_resolve import Template, parse_template from changeset_model import changes, show from stack_graph import capabilities, limits B = pathlib.Path("C:/lwaws/c5") hand_text = pathlib.Path("C:/lwaws/c4/app_stack.yaml").read_text(encoding="utf-8") ADMIN = "arn:aws:iam::111122223333:role/KeyAdmin" hand = {e: Template(parse_template(hand_text), {"Environment": e, "KeyAdminRoleArn": ADMIN}).render() for e in ("dev", "prod")} cdk = {e: json.loads((B / "out" / f"Reports-{e}.template.json").read_text(encoding="utf-8")) for e in ("dev", "prod")} def types(res): return collections.Counter(r["Type"] for r in res.values()) def actions(res): out = set() for r in res.values(): if r["Type"] in ("AWS::IAM::Policy", "AWS::IAM::Role"): props = r["Properties"] docs = [props["PolicyDocument"]] if "PolicyDocument" in props else [p["PolicyDocument"] for p in props.get("Policies", [])] for d in docs: for s in d["Statement"]: if s["Effect"] == "Allow": out |= set([s["Action"]] if isinstance(s["Action"], str) else s["Action"]) return out print("1. resource types: hand-written template (rendered by the chapter 4 resolver) against what cdk synth generated") for e in ("dev", "prod"): h, c = types(hand[e]["Resources"]), types(cdk[e]["Resources"]) print(f" {e}: hand-written {sum(h.values())} resources, CDK {sum(c.values())} resources") for t in sorted(set(h) | set(c)): mark = "" if h[t] == c[t] else " <-- differs" print(f" {t:<28} hand-written {h[t]} CDK {c[t]}{mark}") print("\n2. sections of the generated template: CDK made these decisions while the app ran, so there is no Parameters/Mappings/Conditions of its own") for e in ("dev", "prod"): j = cdk[e] print(f" {e}: sections {sorted(j)}; Parameters {sorted(j.get('Parameters', {}))}; Conditions {sorted(j.get('Conditions', {}))}; Mappings {sorted(j.get('Mappings', {}))}") print(f" hand-written template sections: {sorted(parse_template(hand_text))}") print("\n3. the worker role's allowed actions: hand-written inline policy against the policy CDK's grant methods wrote") ha, ca = actions(hand["dev"]["Resources"]), actions(cdk["dev"]["Resources"]) print(f" hand-written ({len(ha)}): {sorted(ha)}") print(f" CDK ({len(ca)}): {sorted(ca)}") print(f" only in CDK's policy: {sorted(ca - ha)}") print(f" only in the hand-written one: {sorted(ha - ca)}") print("\n4. DeletionPolicy / UpdateReplacePolicy per resource") for name, r in cdk["dev"]["Resources"].items(): print(f" CDK {r['Type']:<24} DeletionPolicy {r.get('DeletionPolicy', '-'):<8} UpdateReplacePolicy {r.get('UpdateReplacePolicy', '-')}") print("\n5. dev -> prod, as the chapter 4 change set approximation sees the two CDK templates") res = {e: {k: {"Type": v["Type"], "Properties": v.get("Properties", {})} for k, v in cdk[e]["Resources"].items() if v["Type"] != "AWS::CDK::Metadata"} for e in cdk} show("CDK Reports-dev -> Reports-prod (the stack name also changes, so names derived from it change too)", changes(res["dev"], res["prod"])) print("\n6. capabilities and quotas, with the chapter 4 checkers") for e in ("dev", "prod"): txt = (B / "out" / f"Reports-{e}.template.json").read_text(encoding="utf-8") print(f" {e}: capability {capabilities(cdk[e])[0]}; over a quota: {[f'{n} {v} > {m}' for n, v, m, bad in limits(txt, cdk[e]) if bad] or 'none'}; template {len(txt.encode()):,} bytes") print("\n7. cfn-lint (chapter 4 environment) on the generated templates") lint = "C:/lwaws/c4/.venv/Scripts/cfn-lint.exe" for e in ("dev", "prod"): r = subprocess.run([lint, f"out/Reports-{e}.template.json"], cwd=B, capture_output=True, text=True, encoding="utf-8") print(f" Reports-{e}.template.json: exit {r.returncode} {r.stdout.strip()[:600] or '(no findings)'}") --- python compare_templates.py (saved output of the real run) --- 1. resource types: hand-written template (rendered by the chapter 4 resolver) against what cdk synth generated dev: hand-written 7 resources, CDK 9 resources AWS::CDK::Metadata hand-written 0 CDK 1 <-- differs AWS::IAM::Policy hand-written 0 CDK 1 <-- differs AWS::IAM::Role hand-written 1 CDK 1 AWS::KMS::Alias hand-written 1 CDK 1 AWS::KMS::Key hand-written 1 CDK 1 AWS::S3::Bucket hand-written 1 CDK 1 AWS::S3::BucketPolicy hand-written 1 CDK 1 AWS::SQS::Queue hand-written 2 CDK 2 prod: hand-written 8 resources, CDK 10 resources AWS::CDK::Metadata hand-written 0 CDK 1 <-- differs AWS::CloudWatch::Alarm hand-written 1 CDK 1 AWS::IAM::Policy hand-written 0 CDK 1 <-- differs AWS::IAM::Role hand-written 1 CDK 1 AWS::KMS::Alias hand-written 1 CDK 1 AWS::KMS::Key hand-written 1 CDK 1 AWS::S3::Bucket hand-written 1 CDK 1 AWS::S3::BucketPolicy hand-written 1 CDK 1 AWS::SQS::Queue hand-written 2 CDK 2 2. sections of the generated template: CDK made these decisions while the app ran, so there is no Parameters/Mappings/Conditions of its own dev: sections ['Conditions', 'Outputs', 'Parameters', 'Resources', 'Rules']; Parameters ['BootstrapVersion']; Conditions ['CDKMetadataAvailable']; Mappings [] hand-written template sections: ['AWSTemplateFormatVersion', 'Conditions', 'Description', 'Mappings', 'Metadata', 'Outputs', 'Parameters', 'Resources'] prod: sections ['Conditions', 'Outputs', 'Parameters', 'Resources', 'Rules']; Parameters ['BootstrapVersion']; Conditions ['CDKMetadataAvailable']; Mappings [] hand-written template sections: ['AWSTemplateFormatVersion', 'Conditions', 'Description', 'Mappings', 'Metadata', 'Outputs', 'Parameters', 'Resources'] 3. the worker role's allowed actions: hand-written inline policy against the policy CDK's grant methods wrote hand-written (7): ['kms:Decrypt', 'kms:GenerateDataKey', 's3:GetObject', 's3:PutObject', 'sqs:DeleteMessage', 'sqs:GetQueueAttributes', 'sqs:ReceiveMessage'] CDK (19): ['kms:Decrypt', 'kms:DescribeKey', 'kms:Encrypt', 'kms:GenerateDataKey*', 'kms:ReEncrypt*', 's3:Abort*', 's3:GetBucket*', 's3:GetObject*', 's3:List*', 's3:PutObject', 's3:PutObjectLegalHold', 's3:PutObjectRetention', 's3:PutObjectTagging', 's3:PutObjectVersionTagging', 'sqs:ChangeMessageVisibility', 'sqs:DeleteMessage', 'sqs:GetQueueAttributes', 'sqs:GetQueueUrl', 'sqs:ReceiveMessage'] only in CDK's policy: ['kms:DescribeKey', 'kms:Encrypt', 'kms:GenerateDataKey*', 'kms:ReEncrypt*', 's3:Abort*', 's3:GetBucket*', 's3:GetObject*', 's3:List*', 's3:PutObjectLegalHold', 's3:PutObjectRetention', 's3:PutObjectTagging', 's3:PutObjectVersionTagging', 'sqs:ChangeMessageVisibility', 'sqs:GetQueueUrl'] only in the hand-written one: ['kms:GenerateDataKey', 's3:GetObject'] 4. DeletionPolicy / UpdateReplacePolicy per resource CDK AWS::KMS::Key DeletionPolicy Retain UpdateReplacePolicy Retain CDK AWS::KMS::Alias DeletionPolicy - UpdateReplacePolicy - CDK AWS::S3::Bucket DeletionPolicy Retain UpdateReplacePolicy Retain CDK AWS::S3::BucketPolicy DeletionPolicy - UpdateReplacePolicy - CDK AWS::SQS::Queue DeletionPolicy Delete UpdateReplacePolicy Delete CDK AWS::SQS::Queue DeletionPolicy Delete UpdateReplacePolicy Delete CDK AWS::IAM::Role DeletionPolicy - UpdateReplacePolicy - CDK AWS::IAM::Policy DeletionPolicy - UpdateReplacePolicy - CDK AWS::CDK::Metadata DeletionPolicy - UpdateReplacePolicy - 5. dev -> prod, as the chapter 4 change set approximation sees the two CDK templates -- CDK Reports-dev -> Reports-prod (the stack name also changes, so names derived from it change too) Add BacklogAlarm1E83516A AWS::CloudWatch::Alarm Replacement: - Modify DataKey17AFBB84 AWS::KMS::Key Replacement: False Modify /Properties/Description "Data key for Reports-dev (dev)" -> "Data key for Reports-prod (prod)" Modify DataKeyAliasF989EB84 AWS::KMS::Alias Replacement: True Modify /Properties/AliasName "alias/Reports-dev-data" -> "alias/Reports-prod-data" Modify WorkQueue94013F35 AWS::SQS::Queue Replacement: False Modify /Properties/RedrivePolicy/maxReceiveCount 3 -> 5 Modify /Properties/VisibilityTimeout 30 -> 120 6. capabilities and quotas, with the chapter 4 checkers dev: capability CAPABILITY_IAM; over a quota: none; template 13,372 bytes prod: capability CAPABILITY_IAM; over a quota: none; template 14,298 bytes 7. cfn-lint (chapter 4 environment) on the generated templates Reports-dev.template.json: exit 0 (no findings) Reports-prod.template.json: exit 0 (no findings) WHY THIS WORKS AS AN ANSWER --------------------------- The stack keeps the design of the chapter 4 template (a key with a delegating root statement and a separate administrator statement, an encrypted versioned bucket that refuses plain HTTP, a dead-letter queue, a worker role, a production-only alarm) and expresses the chapter 4 features as code: the Mappings table is an object, the Condition is an if, the parameter constraint is a check that throws. Synthesis ran for real and produced a template that cfn-lint accepts (exit status 0) for both environments. The comparison turns 'it works' into specifics: the same resource types plus an AWS::IAM::Policy and an AWS::CDK::Metadata resource (9 against 7 in dev, 10 against 8 in prod), no Mappings or Parameters of its own, a BootstrapVersion parameter and rule, 19 distinct allowed actions on the worker role against 7 hand-written, and Delete policies on the queues that nobody wrote. Not run: cdk init, cdk deploy or anything that needs an account; other languages.