Based on the DOP-C02 exam guideReport an error
When CloudFormation templates exceed 3000 lines, developers repeatedly ask the same question. "Is this really code?" YAML has no functions, no variable scope, weak autocomplete, and applying the same pattern to 50 microservices leaves only copy-paste. 2018's AWS CDK (Cloud Development Kit) addresses exactly this limit. Define infrastructure in generic languages like TypeScript/Python/Java/Go/.NET and cdk synth synthesizes ordinary CloudFormation templates. SAM applied macros atop CloudFormation; CDK climbs one more level — treating language itself as an abstraction tool.
Today we explore why Construct has L1/L2/L3 layers, what trust chain cdk bootstrap creates and its security meaning, why CDK Pipelines' self-mutation is a game-changer, operational patterns when Terraform and CDK coexist, CDKTF's position, and how CDK Aspects handle cross-cutting concerns. CDK direct exam questions are moderate but CDK Pipelines + Multi-Account scenarios frequently appear.
Initially CDK seems identical to Pulumi — both define infrastructure with generic languages. A decisive difference exists: CDK executes code to generate CloudFormation templates (JSON/YAML), then passes that template to the CFN engine for deployment. Pulumi interprets its own code graph directly, calling AWS APIs itself.
This difference determines entire operational model. CDK inherits all CFN governance assets (StackSets, Drift Detection, ChangeSet, Service Catalog, Config compliance) automatically. Pulumi uses its own separate governance tools. So large AWS-only organizations prefer CDK, while multi-cloud environments favor Pulumi or Terraform.
CDK code (TypeScript)
│ cdk synth
▼
CloudFormation template (YAML/JSON)
│ cdk deploy
▼
CloudFormation engine (AWS managed)
│ Drift / ChangeSet / StackSet / Service Catalog available
▼
Real AWS resources
Due to this structure, CDK appears as single source of truth but actually has two abstraction stages. First, from CDK code to Construct tree (in-memory object graph). Second, from Construct tree to CFN template (synth). So cdk diff previews require understanding both stages — some changes are code-level intent, others are post-synth CFN changes.
💡 Related Theory: CDK's synthesis model mirrors LLVM compiler's IR (Intermediate Representation) pattern. High-level languages (C++/Rust) compile to IR (LLVM bitcode), IR translates to each target (x86, ARM). CDK compiles TypeScript to IR (Construct tree), IR transforms to CFN template. This two-stage approach enables cdktf (Construct tree → Terraform HCL), cdk8s (Construct tree → Kubernetes manifest) synthesis to different backends. One abstraction scales to multiple backends.
CDK's most powerful design decision separates Construct levels. Same S3 bucket expressible at three abstraction tiers.
// L1 — 1:1 CloudFormation mapping (Cfn prefix)
new s3.CfnBucket(this, 'Raw', {
bucketName: 'my-bucket',
bucketEncryption: {
serverSideEncryptionConfiguration: [{
serverSideEncryptionByDefault: { sseAlgorithm: 'AES256' }
}]
}
});
// L2 — Sensible defaults + methods (most common)
const bucket = new s3.Bucket(this, 'Data', {
encryption: s3.BucketEncryption.S3_MANAGED,
versioned: true,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
lifecycleRules: [{
transitions: [{
storageClass: s3.StorageClass.INTELLIGENT_TIERING,
transitionAfter: cdk.Duration.days(30),
}],
}],
});
bucket.grantRead(myLambda); // IAM policy auto-created
bucket.addEventNotification(...); // S3 event + Lambda permission auto-handled
// L3 — Pattern (multiple resources bundled)
new ecs_patterns.ApplicationLoadBalancedFargateService(this, 'Web', {
taskImageOptions: { image: ecs.ContainerImage.fromRegistry('nginx') },
publicLoadBalancer: true,
desiredCount: 3,
});
// Auto-creates: VPC + Subnets + IGW + NAT + ECS Cluster + Fargate Service +
// Task Definition + ALB + Target Group + Security Groups + IAM Roles| Level | Abstraction | Code Volume | Learning Curve | Control |
|---|---|---|---|---|
| L1 (Cfn*) | CFN 1:1 | High | Low (CFN-level) | Maximum |
| L2 | Sensible defaults + helper | Medium | Medium | High |
| L3 (patterns) | Multiple resources bundled | Low | Low (simple API) | Low (black box) |
In practice use L2 by default, deploy L1 for security-critical parts (IAM, KMS), extract reusable patterns as custom L3. External L3 like ApplicationLoadBalancedFargateService accelerates PoC but prod needs control of internals (security group ranges, ALB logs, WAF attachment) usually requiring custom Constructs.
🎯 Scenario: "Team deploys 50 microservices with same pattern (Lambda + DynamoDB + API Gateway + CloudWatch Alarm + X-Ray). Security policy change should auto-propagate to all." — Answer is custom L3 Construct (
StandardLambdaApiPattern). All services use it, security policy changes in one place → all 50 services auto-update on next deploy. CFN requires module patterns or Nested Stacks with worse code reuse.
cdk bootstrap runs once per new account·region before using CDK. Superficially "create S3 bucket, ECR repo," actually establishes all trust relationships for CDK operations, a security infrastructure.
cdk bootstrap aws://111111111111/ap-northeast-2This single line creates:
cdk-hnb659fds-assets-111-ap-northeast-2): Lambda zip, CFN templates, Docker metadata storagecdk-hnb659fds-container-assets-111-ap-northeast-2): Docker imagescdk-hnb659fds-deploy-role-*: Calls deploy commandscdk-hnb659fds-cfn-exec-role-*: CFN uses for executioncdk-hnb659fds-file-publishing-role-*: S3 file uploadscdk-hnb659fds-image-publishing-role-*: ECR image pushcdk-hnb659fds-lookup-role-*: Query AWS (VPC IDs)CDKToolkit: Defines above resourcesCore design is permission separation. CFN execution role (cfn-exec-role) has strongest permissions (AdministratorAccess default), but only CloudFormation service assumes it. Deploy role that people/pipelines assume has narrower permissions — can't create resources directly, only via CloudFormation.
# Cross-account: Tooling trusts Dev/Prod to bootstrap
cdk bootstrap aws://DEV-ACCT/ap-northeast-2 \
--trust TOOLING-ACCT \
--trust-for-lookup TOOLING-ACCT \
--cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess--trust TOOLING-ACCT sets trust policy so Dev account's deploy role can assume Tooling account's ID. This enables CDK Pipelines from one Tooling account deploying to multiple environment accounts.
🔍 Deeper: bootstrap has versions —
CdkBootstrapSSM Parameter stores version number,cdk deploychecks for compatibility. Old bootstrap can't use new features (e.g., CFN Bucket KMS encryption enhancements). AWS regularly updates bootstrap versions, so operational standards include "quarterlycdk bootstrapre-run."
⚠️ Pitfall: bootstrap's cfn-exec-role defaulting to AdministratorAccess frequently criticized from security perspective. Prod should use
--cloudformation-execution-policiesspecifying narrower policies (company standard PowerUser + explicit deny). Otherwise any CDK code change deploys to prod with full permissions, defeating change control.
2020 CDK Pipelines launch's biggest differentiator was self-mutation. Ordinary CodePipeline separates definition (pipeline structure) from execution; changing the pipeline itself requires separate tools (CloudFormation, Terraform). CDK Pipelines' pipeline includes itself as first Stage, auto-reflecting code changes on next run.
const pipeline = new CodePipeline(this, 'Pipeline', {
pipelineName: 'MyApp',
crossAccountKeys: true,
synth: new ShellStep('Synth', {
input: CodePipelineSource.connection('my-org/app', 'main', {
connectionArn: 'arn:aws:codestar-connections:...',
}),
commands: ['npm ci', 'npm test', 'npx cdk synth'],
}),
selfMutation: true, // default
});
pipeline.addStage(new AppStage(this, 'Dev', {
env: { account: 'DEV-ACCT', region: 'ap-northeast-2' },
}));
pipeline.addStage(new AppStage(this, 'Prod', {
env: { account: 'PRD-ACCT', region: 'ap-northeast-2' },
}), {
pre: [new ManualApprovalStep('ApproveProd')],
post: [new ShellStep('SmokeTest', {
commands: ['curl -fSs https://api.example.com/health'],
})],
});Pipeline's actual Stage order:
1. Source (GitHub/CodeCommit pull)
2. Build (cdk synth → CFN template + Asset)
3. UpdatePipeline ← self-mutating step
(Pipeline def change → pipeline self-updates)
4. PublishAssets (Lambda zip → S3, Docker → ECR)
5. Dev (CFN deploy to DEV-ACCT)
6. ManualApproval
7. Prod (CFN deploy to PRD-ACCT)
8. SmokeTest
Stage 3 is critical. If developer adds new Stage (e.g., Staging), next push's UpdatePipeline auto-adds Staging Stage to pipeline, and subsequent runs execute new Stage. Pipeline definition changes need no separate tools.
💡 Related Theory: self-mutation follows unix
makebootstrap or Lisp meta-circular evaluator (self-modifying code). System defines and evolves itself. Tekton Trigger Templates, ArgoCD App-of-Apps, Spinnaker Managed Pipeline Templates are variations of same pattern. CDK Pipelines most cleanly implements GitOps's core (pipelines as code) as AWS native tool.
🎯 Scenario: "Company adds 5 new customer sandbox environments weekly, operations manually updates pipeline." — Answer is CDK Pipelines + CDK code dynamically generating environments (for loop or external config file). Git commit adding environments → self-mutation auto-adds to pipeline. General CodePipeline + CFN need separate tools or manual steps.
CDK Pipelines multi-account deployments require crossAccountKeys: true. Unknown what this automates causes permission setup accidents.
CodePipeline stores artifacts (build results, source) in S3, multi-account scenarios mean that S3 object needs reading by other account's CFN execution role. KMS-encrypted object requires KMS Key Policy allowing other account decryption. crossAccountKeys: true auto-creates:
crossAccountKeys: false (default) uses AWS-managed KMS key, which can't share across accounts, causing multi-account deploy failure. This pitfall's "yesterday prod deployment worked, today adding Prod account causes KMS error" scenario frequently tests.
⚠️ Pitfall:
crossAccountKeys: truecreates customer-managed CMK with extra cost ($1/month/key). Toggling on/off means existing artifacts become unreadable, requiring careful decision. If multi-account is possible, start with enabled as standard.
CDK Aspects are IaC's Aspect-Oriented Programming (AOP). Tree-wide traversal applies cross-cutting processing to all nodes.
import { Aspects, IAspect, IConstruct, CfnResource } from 'aws-cdk-lib';
class EnforceTags implements IAspect {
visit(node: IConstruct) {
if (node instanceof CfnResource) {
node.tags.setTag('Environment', this.env);
node.tags.setTag('CostCenter', this.costCenter);
node.tags.setTag('Owner', 'platform-team');
}
}
constructor(private env: string, private costCenter: string) {}
}
class EnforceEncryption implements IAspect {
visit(node: IConstruct) {
if (node instanceof s3.CfnBucket) {
if (!node.bucketEncryption) {
Annotations.of(node).addError('S3 buckets must have encryption');
}
}
if (node instanceof rds.CfnDBInstance) {
if (!node.storageEncrypted) {
Annotations.of(node).addError('RDS must have storage encryption');
}
}
}
}
Aspects.of(app).add(new EnforceTags('prod', 'cc-001'));
Aspects.of(app).add(new EnforceEncryption());Annotations.addError fails synth, blocking build. Where Hooks block pre-deployment, Aspects block pre-synth, earlier in CI pipeline so feedback is faster.
| Validation Tool | Timing | Does |
|---|---|---|
| CDK Aspects | cdk synth time | CDK code stage policy |
| cdk-nag | cdk synth time | AWS Solutions/HIPAA/NIST ruleset bundles |
| cfn-lint | Post-synth CFN time | Template syntax/best practice |
| CFN Hook | Deploy time | Pre-provision blocking (proactive) |
| Config Rules | Post-deploy | Evaluation/alert/auto-fix (reactive) |
📚 Case Study: 2022 SaaS applied cdk-nag full ruleset (NIST 800-53 Rev5) wholesale, build failed with 1200 violations. Instead of all-at-once enforcement: (1) warn-only ruleset initial → baseline, (2) top 5 impact rules enforce → 6-month gradual fix, (3) stage-wise enforce. Graceful governance adoption is standard.
Real large organizations don't use one tool only. AWS workloads use CDK, multi-cloud or SaaS providers (GitHub, Datadog, Snowflake) use Terraform. Handling both:
// CDKTF — Generate Terraform from CDK syntax
import { App, TerraformStack, S3Backend } from 'cdktf';
import { AwsProvider, s3, datadog } from '@cdktf/provider-aws';
class HybridStack extends TerraformStack {
constructor(scope: Construct, id: string) {
super(scope, id);
new AwsProvider(this, 'aws', { region: 'ap-northeast-2' });
new DatadogProvider(this, 'dd', { apiKey: '...' });
const bucket = new s3.S3Bucket(this, 'b', { bucket: 'cdktf-example' });
new datadog.Monitor(this, 'm', {
name: `s3-${bucket.bucket}-errors`,
query: 'sum:s3.errors{bucket:cdktf-example} > 10',
});
new S3Backend(this, { bucket: 'tfstate', key: 'app/state.tfstate', region: 'ap-northeast-2' });
}
}CDKTF advantage: combine CDK code reusability with Terraform Provider breadth. Disadvantage: CDK camp prefers CFN-native speed, Terraform camp sees HCL as standard; CDKTF awkwardly middle. Rare on tests but frequent in practice.
| Tool | AWS Depth | Multi-cloud | State Management | Test Weight |
|---|---|---|---|---|
| CloudFormation | Maximum | ❌ | AWS managed | Very high |
| CDK | Maximum (CFN synth) | ❌ | AWS managed | Medium |
| SAM | Serverless focused | ❌ | AWS managed (CFN) | High |
| Terraform | High | ✅ | tfstate (user responsibility) | Low |
| CDKTF | High | ✅ | tfstate | Very low |
| Pulumi | High | ✅ | Pulumi Cloud or self | Almost none |
🔍 Deeper: Terraform tfstate's user responsibility is operations' biggest difference. tfstate records all actual resource state; corruption means Terraform sees "never saw this" → duplicate create or delete attempts. Operations standard: (1) S3 + DynamoDB lock for remote storage, (2) S3 versioning + object lock, (3) per-environment workspace or backend. CDK/CFN delegate this burden to AWS.
Today's picture has five parts. First, CDK synthesizes (generates) templates, inheriting all CFN governance (StackSets, Drift, ChangeSet) — LLVM IR pattern at two abstraction stages. Second, Construct L1/L2/L3 are three stairs where L2 is standard + custom L3 extraction. Third, bootstrap establishes trust infrastructure; cfn-exec-role strength is prod security core. Fourth, CDK Pipelines self-mutation
is GitOps's "pipelines as code," multi-account requires crossAccountKeys: true. Fifth, Aspects + cdk-nag (synth-time) + Hooks (deploy-time) + Config Rules (post-deploy) form three-layer validation.
Next we see CDK expand to entire AWS ecosystem in Week 9 summary, then Week 10 dives into monitoring.
Click a choice to reveal the answer and explanation.
Question 1
What is the key difference between CDK and Pulumi?
Question 2
What is the most recommended practical pattern for CDK construct levels?
Question 3
What is the security significance of the IAM role structure that cdk bootstrap creates?
Question 4
What does self-mutation mean in CDK Pipelines?
Question 5
Why is crossAccountKeys: true required for multi-account deployments with CDK Pipelines?
Question 6
What is the most accurate use case for CDK Aspects?
Question 7
What is the recommended strategy when introducing CDK Aspects or cdk-nag rule sets to prod?
The five key takeaways from today are as follows. First, CDK uses a code → CFN template synthesis model and automatically reuses CFN governance assets (the LLVM IR pattern). Second, construct L1/L2/L3 are three steps of abstraction, and in practice the approach is L2 by default plus extracting your own L3. Third, the five IAM roles of cdk bootstrap follow the principle of separation of privilege, and cfn-exec needs to be narrowed in prod. Fourth, CDK Pipelines self-mutation is the AWS implementation of the GitOps principle "the pipeline is code too," and multi-account setups require crossAccountKeys: true. Fifth, the three-tier verification layer of Aspects + cdk-nag (synth time) + Hooks (deployment time) + Config rules (after deployment), with phased rollout, is the standard for introducing governance.