AWS CDK: Cloud Development Kit for Infrastructure
Define AWS infrastructure using TypeScript, Python, or other programming languages with the AWS Cloud Development Kit, compiling to CloudFormation templates.
AWS CDK: Cloud Development Kit for Infrastructure
The AWS Cloud Development Kit (CDK) brings infrastructure as code into familiar programming languages. You write TypeScript, Python, Java, or C# code, and CDK turns it into CloudFormation templates that AWS then deploys. This gives you object-oriented programming — inheritance, composition, testability — while still leaning on CloudFormation’s deployment engine.
CDK sits on top of CloudFormation, so you get all the benefits of CloudFormation’s rollback behavior, change sets, and drift detection. The difference is that you write application code instead of JSON or YAML templates.
Introduction
When CDK makes sense
CDK is the right choice for AWS-heavy teams that want infrastructure code with the full expressiveness of a programming language. If your team writes TypeScript or Python for applications, CDK lets them use the same language and tooling for infrastructure without switching contexts.
Use CDK when your infrastructure benefits from object-oriented design. Shared patterns across dozens of services — a standard VPC setup, a consistent ECS service definition, a common API Gateway configuration — are where CDK’s inheritance and composition model shines. You define a base construct once and specialize it across teams.
CDK also shines when you want aggressive testing of infrastructure. The Template.fromStack() assertion API lets you write property checks that run against synthesized templates in milliseconds. If your team already writes Jest or Pytest tests, CDK testing fits naturally.
CDK Pipelines, which defines the CI/CD pipeline itself as CDK code, is particularly well-integrated. The pipeline updates itself when infrastructure code changes, which is harder to pull off with external CI tools.
When to stick with Terraform or Pulumi
If your infrastructure spans multiple clouds, CDK locks you into AWS. Terraform’s provider model handles AWS, Azure, GCP, and dozens of other platforms from the same configuration language. For multi-cloud strategies, CDK is not the answer.
If your team is ops-focused rather than software-engineering-focused, raw CloudFormation or Terraform HCL may be faster to learn than TypeScript. CDK adds a layer on top of CloudFormation — the additional expressiveness only pays off if you need it.
CDK apps can also be slower to synthesize for very large infrastructure graphs. Thousands of resources mean the TypeScript-to-CloudFormation compilation step can become noticeable. Terraform’s plan phase is often faster for massive state files.
CDK Overview and Installation
CDK applications are organized into stacks, which map to CloudFormation stacks. Within each stack, you define constructs — the building blocks of your infrastructure. Constructs represent AWS resources like an S3 bucket or an ECS service, but they can also represent patterns, like a load-balanced web service that combines an autoscaling group, a load balancer, and a security group.
# Install the CDK CLI
npm install -g aws-cdk
# Initialize a new CDK project
cdk init --language typescript
# Install AWS modules
npm install @aws-cdk/aws-ec2 @aws-cdk/aws-ecs @aws-cdk/aws-ecs-patterns
# List stacks in the app
cdk list
# Synthesize to CloudFormation
cdk synth
The cdk synth command converts your TypeScript code into a CloudFormation template. You can review the generated JSON or YAML before deploying anything. This step also validates that your constructs are internally consistent and that you have permission to create the resources you are requesting.
Construct Model and L3 Constructs
CDK uses a three-level construct model. L1 constructs are direct mappings to CloudFormation resources — use them when you need precise control over every CloudFormation property. L2 constructs have sensible defaults and convenience methods. L3 constructs, also called patterns, are opinionated solutions for common use cases.
import * as cdk from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as ecs from "aws-cdk-lib/aws-ecs";
import * as ecsPatterns from "aws-cdk-lib/aws-ecs-patterns";
class WebServiceStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Create a VPC
const vpc = new ec2.Vpc(this, "WebServiceVPC", {
maxAzs: 2,
natGateways: 1,
subnetConfiguration: [
{ cidrMask: 24, name: "Public", subnetType: ec2.SubnetType.PUBLIC },
{
cidrMask: 24,
name: "Private",
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
],
});
// Create an ECS cluster
const cluster = new ecs.Cluster(this, "WebServiceCluster", {
vpc,
clusterName: "web-service-cluster",
});
// Use L3 pattern for a load-balanced Fargate service
const loadBalancedService =
new ecsPatterns.ApplicationLoadBalancedFargateService(
this,
"WebService",
{
cluster,
memoryLimitMiB: 1024,
cpu: 512,
desiredCount: 2,
taskImageOptions: {
image: ecs.ContainerImage.fromRegistry("nginx:latest"),
environment: {
NODE_ENV: "production",
},
},
},
);
// Output the load balancer URL
new cdk.CfnOutput(this, "LoadBalancerURL", {
value: loadBalancedService.loadBalancer.loadBalancerDnsName,
});
}
}
const app = new cdk.App();
new WebServiceStack(app, "WebServiceStack");
The ApplicationLoadBalancedFargateService pattern creates everything needed for a production-ready service: a Fargate task definition, an ECS service, an Application Load Balancer, a target group, a security group, and IAM roles. One line of configuration replaces dozens of resource definitions.
Synthesizing to CloudFormation
When you run cdk synth, CDK reads your TypeScript files, resolves any runtime values, and generates CloudFormation templates in the cdk.out directory. These templates are what CloudFormation actually deploys.
# Synthesize to CloudFormation template
cdk synth MyStack
# Compare current state with deployed state
cdk diff MyStack
# Deploy the stack
cdk deploy MyStack
# Destroy the stack
cdk destroy MyStack
The cdk diff command is particularly useful. It compares your synthesized template against the currently deployed stack and shows you exactly what will change. This is a safe way to review changes before applying them, especially in production environments.
CDK also supports context values and runtime values that cannot be known at synthesis time, like the current AWS account ID or the latest AMI ID. These get resolved during deployment rather than synthesis.
CDK Pipelines for CI/CD
CDK pipelines let you define your deployment pipeline as code. A pipeline stack contains the pipeline itself, along with stages that represent deployment environments like staging and production.
import * as codepipeline from "aws-cdk-lib/aws-codepipeline";
import * as codepipelineActions from "aws-cdk-lib/aws-codepipeline-actions";
import * as codebuild from "aws-cdk-lib/aws-codebuild";
class PipelineStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const sourceArtifact = new codepipeline.Artifact();
const cloudAssemblyArtifact = new codepipeline.Artifact();
const pipeline = new codepipeline.Pipeline(this, "DeploymentPipeline", {
pipelineName: "my-app-pipeline",
synth: new codepipelineActions.CodeBuildAction({
actionName: "Synth",
input: sourceArtifact,
outputs: [cloudAssemblyArtifact],
buildSpec: codebuild.BuildSpec.fromObject({
version: "0.2",
phases: {
install: { commands: ["npm ci"] },
build: { commands: ["npx cdk synth"] },
},
artifacts: {
"discard-paths": "yes",
"base-directory": "cdk.out",
files: ["**/*"],
},
}),
}),
});
// Add source stage
pipeline.addStage({
stageName: "Source",
actions: [
new codepipelineActions.GitHubSourceAction({
actionName: "GitHub",
owner: "my-org",
repo: "my-app",
branch: "main",
output: sourceArtifact,
oauthToken: cdk.SecretValue.secretsManager("github-token"),
}),
],
});
// Add stage for deploying to production
pipeline.addStage({
stageName: "Prod",
actions: [
new codepipelineActions.CloudFormationCreateUpdateStackAction({
actionName: "Deploy",
stackName: "MyAppStack",
template: cloudAssemblyArtifact,
adminPermissions: true,
}),
],
});
}
}
The pipeline itself is deployed via CloudFormation. When your code changes, the pipeline automatically detects the change, synthesizes the template, and deploys to each stage in sequence.
Testing CDK Applications
CDK integrates with standard testing frameworks. You can write snapshot tests that verify your synthesized templates match expected output, or you can write assertion tests that validate specific resource properties.
import { App } from "aws-cdk-lib";
import { Template } from "aws-cdk-lib/assertions";
import { MyStack } from "../lib/my-stack";
test("VPC is created with correct CIDR", () => {
const app = new App();
const stack = new MyStack(app, "TestStack");
const template = Template.fromStack(stack);
template.hasResourceProperties("AWS::EC2::VPC", {
CidrBlock: "10.0.0.0/16",
EnableDnsHostnames: true,
EnableDnsSupport: true,
});
});
test("S3 bucket has versioning enabled", () => {
const app = new App();
const stack = new MyStack(app, "TestStack");
const template = Template.fromStack(stack);
template.hasResourceProperties("AWS::S3::Bucket", {
VersioningConfiguration: {
Status: "Enabled",
},
});
});
The Template.fromStack() method parses a synthesized CloudFormation template and provides methods to assert the presence and properties of resources. These tests run quickly because they work against the synthesized template without actually deploying anything.
CDK vs Terraform vs Pulumi Comparison
| Aspect | CDK | Terraform | Pulumi |
|---|---|---|---|
| Language | TypeScript, Python, Java, C# | HCL | TypeScript, Python, Go, C# |
| Ecosystem | AWS-focused | Multi-cloud, massive | Multi-cloud, growing |
| State | CloudFormation handles it | Self-managed backends | Managed or self-managed |
| Testing | Jest/Pytest assertions | Plan validation | Unit tests with language frameworks |
| Learning curve | Moderate (requires programming) | Gentle for ops | Moderate (requires programming) |
| Synthesize step | Generates JSON/YAML templates | Native plan | Native plan |
Trade-off Analysis
CDK vs Alternatives
| Consideration | CDK | Terraform | Pulumi |
|---|---|---|---|
| Multi-cloud support | AWS-only | Full multi-cloud | Full multi-cloud |
| Language expressiveness | Full OOP (inheritance, composition) | HCL limited to declarative | General-purpose languages |
| State management | CloudFormation handles automatically | Self-managed or remote backends | Self-managed or cloud backends |
| Ecosystem size | Growing, AWS-focused | Massive, mature provider network | Growing, multi-cloud |
| Learning curve | Moderate (requires TypeScript/Python) | Gentle for ops engineers | Moderate (requires programming) |
| Synthesis speed | Slower for large graphs (thousands of resources) | Faster plan phase for massive state | Similar to Terraform |
| IDE/Type support | First-class TypeScript, auto-complete | Limited HCL support | Full language tooling |
| Testing | Jest/Pytest assertions against synthesized JSON | Plan validation, external tools | Native unit testing with language frameworks |
| Pipeline integration | CDK Pipelines (self-updating) | External CI/CD | External CI/CD |
| Breakage risk | Construct library updates can change behavior | Provider version changes | Same as Terraform |
When CDK Wins
CDK excels when you need object-oriented abstraction for infrastructure patterns that repeat across teams. If you have fifty services that all need the same VPC setup, you write one base construct and inherit it fifty times. That kind of abstraction is genuinely hard to achieve in Terraform HCL.
TypeScript or Python also means your infrastructure code can use your existing tooling—linters, formatters, test frameworks, CI systems. Engineers who already write application code stay in their familiar environment.
When CDK Loses
If you manage infrastructure across AWS, GCP, and Azure, CDK is not an option. The moment you need Terraform or Pulumi, you lose the investment in CDK code.
For very large infrastructure graphs, CDK synthesis time becomes noticeable. If you are managing thousands of resources, Terraform’s plan phase is often faster because it does not need to compile TypeScript to JSON.
Teams that are ops-focused rather than software-engineering-focused may find CDK adds unnecessary abstraction. Raw CloudFormation or Terraform HCL are closer to the metal and require less ramp-up time.
Production Failure Scenarios
Common CDK Failures
| Failure | Impact | Mitigation |
|---|---|---|
| Missing synthesizer permissions | Stack creation fails mid-deploy | Bootstrap AWS account with CDK toolkit first |
| Circular construct dependencies | Synth hangs or crashes | Audit dependency graph before deploying |
| Context values missing in CI | Different synthesis in CI vs local | Ensure consistent AWS account/region context |
| Deep inheritance chains | CDK app slow to synthesize | Use composition over inheritance for constructs |
| Unpinned ConstructLib versions | Different resource versions across environments | Pin all ConstructLib versions in package.json |
Bootstrap Failure Recovery
flowchart TD
A[cdk deploy fails] --> B{Bootstrap needed?}
B -->|Yes| C[Run cdk bootstrap]
B -->|No| D[Check IAM permissions]
C --> E[Creates S3 bucket, IAM roles]
E --> F[Retry deploy]
D --> G[Add required IAM permissions]
G --> F
Best Practices for Team Workflows
Organize CDK projects around the concept of team ownership. Each team maintains their own constructs and stacks, publishing them as libraries that other teams consume. This creates clear boundaries and prevents merge conflicts in monorepo setups.
Use the CDK Bootcamp pattern for constructing cloud-native applications. Stacks should represent deployment environments, constructs should represent components, and the assembly of constructs into stacks should reflect how the application is actually deployed.
Always run cdk synth in your CI pipeline before deploying. This catches syntax errors, type errors, and logical errors before they reach any environment.
Observability Hooks
What to monitor:
- CDK app synthesis duration — slow synthesis usually means deep dependency chains or overly complex constructs
- CloudFormation stack drift between deployments
- Deploy frequency per stack — deploying too often suggests missing abstraction
- Failed CDK deployments and error types
- CloudFormation template size — approach the 51,200-byte limit and synthesis starts failing
# List all stacks and their status
cdk list | while read stack; do
echo "=== $stack ==="
aws cloudformation describe-stacks --stack-name "$stack" \
--query 'Stacks[0].[StackStatus,LastUpdatedTimestamp]'
done
# Check for drift between synthesized and deployed state
cdk diff --no-color
# Monitor CloudFormation stack events during deployment
aws cloudformation describe-stack-events \
--stack-name MyAppStack \
--query 'StackEvents[?ResourceStatus==`CREATE_FAILED`]'
# CDK metadata and version info
cdk --version
npm list aws-cdk-lib
CDK Architecture Flow
flowchart TD
A[CDK Code .ts/.py] --> B[cdk synth]
B --> C[CloudFormation Template]
C --> D[CloudFormation Stack]
D --> E[AWS Resources]
E --> F[cdk diff]
F -->|Drift detected| G[Update via CDK]
F -->|No drift| H[No action needed]
Common Pitfalls / Anti-Patterns
Using L1 constructs everywhere
L1 constructs map directly to CloudFormation resources. Using them exclusively gives you precise control but loses the productivity benefits of L2 and L3 constructs. Reserve L1 for properties that higher-level constructs do not expose.
Ignoring the CDK context cache
CDK caches things like availability zones and AMI IDs in .cdk.json. This cache can become stale after region outages or when you add a new region. Clear it with cdk context --clear when you suspect stale data.
Deploying from local machines instead of CI
When individual engineers deploy from their machines, you lose audit trails and introduce inconsistency. CDK Pipelines or CI-based deployments ensure every deployment is version-controlled and logged.
Modifying synthesized templates directly
CDK always regenerates templates from source during cdk synth. If you hand-edit a synthesized template in cdk.out, your changes vanish on the next synthesis. All modifications belong in the source code.
Using cdk deploy --require-approval never in production
Skipping manual approval checks in production defeats the purpose of reviewable change sets. Use cdk diff to review changes before deploying, even when not requiring approval at the CLI level.
Interview Questions
Expected answer points:
- CDK synthesizes application code into CloudFormation templates
- CloudFormation handles deployment, rollback, and state management
- CDK adds object-oriented abstraction on top of CloudFormation's JSON/YAML
- All CloudFormation features (change sets, drift detection, rollback) work with CDK
Expected answer points:
- L1: direct CloudFormation resource mappings, full control, verbose
- L2: sensible defaults and convenience methods, preferred for most use
- L3: opinionated patterns (like ApplicationLoadBalancedFargateService), replace dozens of resources
Expected answer points:
- Context values (AZs, AMI IDs) resolved at synthesis time
- CI environments may lack account/region context that local has
- Ensure consistent context via CDK bootstrapping and environment variables
- Use `cdk context --clear` when stale data causes issues
Expected answer points:
- Stacks represent deployment environments (dev, staging, prod)
- Constructs represent components (VPC, ECS, Database)
- Assembly of constructs into stacks reflects actual deployment topology
- Creates clear ownership boundaries between teams
Expected answer points:
- Snapshot tests verify synthesized templates match expected output
- Assertion tests validate specific resource properties via `Template.fromStack()`
- Tests run against synthesized JSON without actual deployment
- Integration with Jest/Pytest for teams already using those frameworks
Expected answer points:
- Catches syntax errors, type errors, and logical errors before any environment
- Validates construct internal consistency
- Checks IAM permissions for requested resources
- Generates CloudFormation template for review via `cdk diff`
Expected answer points:
- Pipeline definition is CDK code that updates itself when infrastructure changes
- No external CI configuration required—the pipeline IS infrastructure
- Pipeline deployment via CloudFormation ensures self-consistency
- External CI tools require separate pipeline configuration that can drift
Expected answer points:
- Circular dependencies cause synth to hang or crash
- Avoid by using composition over inheritance for construct relationships
- Audit dependency graph before deploying large infrastructure
- Use `cdk doctor` to detect dependency issues early
Expected answer points:
- When higher-level constructs do not expose required CloudFormation properties
- When you need precise control over every resource attribute
- For new or niche AWS services not yet covered by L2/L3
- Reserve for the specific property—don't use L1 exclusively
Expected answer points:
- Bootstrap creates S3 bucket and IAM roles needed for CDK toolkit
- Required before `cdk deploy` can upload CloudFormation templates
- Runs once per account/region: `cdk bootstrap aws://123456789012/us-east-1`
- Without bootstrap, stack creation fails mid-deploy with permissions error
Expected answer points:
- Never hardcode secrets in CDK code — use environment variables or AWS Secrets Manager
- `cdk.SecretValue.secretsManager()` retrieves secrets at deployment time
- For CI/CD, use `cdk.SecretValue.unsafePlainText()` only in non-production with caution
- CloudFormation does not expose secret values in console after creation
- Use AWS Systems Manager Parameter Store with encryption for configuration secrets
- Enable CloudTrail to audit secret access patterns
Expected answer points:
- CDK v2 uses stable module structure (`aws-cdk-lib` instead of `@aws-cdk/*`)
- v2 removes experimental modules from stable API surface — fewer breaking changes
- Better stability guarantees: experimental constructs now clearly marked
- Simplified versioning: all `aws-cdk-lib` modules at same version
- v1 no longer receives bug fixes or new features — security patches only
- Migration: update imports and refactor any removed experimental APIs
Expected answer points:
- Use context values for environment-specific configuration: `app.node.tryGetContext('env')`
- Define separate stack instances per environment: `new MyStack(app, 'MyStack-Dev')`
- Pass environment-specific props to control replicas, instance sizes, feature flags
- Use CDK Pipelines with separate stages for each environment
- Cross-environment references require VPC peering or shared services in separate stack
- Use environment variables or JSON config files to parameterize at synthesis time
Expected answer points:
- Aspects visit all constructs in a tree and apply modifications or validations
- Use cases: add tags to all resources, check compliance (all S3 buckets encrypted), add IAM policies
- Built-in aspects: `TagManager` for AWS tags, `AwsSolutionsCheck` for security compliance
- Custom aspect: implement `IAspect` interface with `visit()` method
- Aspects run after synthesis but before deployment — good for enforcement
- Example: ensure every resource has environment tag before deployment
Expected answer points:
- Deep inheritance chains slow synthesis — use composition over inheritance
- Avoid circular construct dependencies — audit with `cdk doctor`
- Use L3 patterns instead of composing dozens of L1 constructs manually
- Split large apps into multiple CDK apps with separate deployment pipelines
- Cache context values (AZs, AMI IDs) — avoid `cdk context --clear` in CI
- For very large infrastructure (thousands of resources), consider Terraform instead
Expected answer points:
- CDK abstracts CloudFormation, but not all CloudFormation features are exposed
- Escape hatch: access underlying `CfnResource` via `.node.defaultChild` to add/modify properties
- Example: `const cfn = myBucket.node.defaultChild as s3.CfnBucket`
- Use sparingly — escape hatch bypasses CDK validation and may break on future updates
- Better: file GitHub issue requesting the missing feature, use escape hatch as workaround
- Test thoroughly when using escape hatch — CloudFormation synthesis may behave unexpectedly
Expected answer points:
- Cross-stack references: `stack2.VpcId.fromStackName(stack1, 'SharedVpc')`
- CDK automatically creates CloudFormation cross-stack references (outputs + imports)
- Avoid circular dependencies between stacks — one stack must be deployed first
- For complex dependencies: use AWS SSM Parameter Store or S3 as intermediate store
- Stack dependencies affect deployment order — CDK synthesizes correct order
- Separate stacks for separately deployed units; avoid monolith stack for large apps
Expected answer points:
- CDK synthesized templates are visible in CloudFormation console — no secrets in template
- Deployment principal (who runs `cdk deploy`) needs IAM permissions for all resources created
- Use `cdk bootstrap` with least-privilege bootstrap roles, not default admin
- Enable `cdk diff` in CI to review changes before deployment
- Use stack termination protection for production stacks to prevent accidental deletion
- Pin ConstructLib versions to prevent unexpected resource changes on upgrade
Expected answer points:
- Use `Template.fromStack()` assertion API for property checks
- Test resource count: `template.resourceCountByType('AWS::EC2::VPC')`
- Test specific properties: `template.hasResourceProperties('AWS::S3::Bucket', {VersioningEnabled: true})`
- Snapshot tests capture full synthesized template — catch unexpected changes
- Test edge cases: empty props, maximum values, invalid combinations
- Integration tests deploy to isolated environment for full stack validation
Expected answer points:
- Construct Hub (constructs.dev) aggregates community-published CDK constructs
- Use when AWS does not have native L2/L3 construct for your use case
- Evaluate community constructs: check maintenance status, GitHub stars, AWS validation
- Add community construct: `npm install @author/construct-name`
- Community constructs may have different stability guarantees than official AWS ones
- For production: fork and maintain community constructs to prevent supply chain issues
Further Reading
- CDK Documentation - Official AWS CDK guide
- CDK API Reference - Full construct library reference
- CDK Patterns - Curated architecture patterns using L3 constructs
- aws-cdk-lib repository - Report issues and see construct source code
- CDK Workshop - Interactive tutorial for hands-on learning
- Construct Hub - Community-published construct library
Conclusion
CDK brings programming language expressiveness to AWS infrastructure while leveraging CloudFormation’s battle-tested deployment engine. The construct model provides sensible defaults while allowing full customization when needed. CDK pipelines bring CI/CD best practices to infrastructure deployments, and integrated testing ensures your infrastructure code does what you intend.
For securing your AWS infrastructure, see Cloud Security for IAM policies, encryption, and VPC configuration. For monitoring CDK-deployed resources and pipeline health, see Observability Engineering.
If your team lives primarily in the AWS ecosystem and already writes TypeScript or Python, CDK is worth serious consideration. The productivity gains from IDE support, type checking, and software engineering practices like testing and composition are significant compared to raw CloudFormation templates.
For more on AWS services, see our post on Cost Optimization which covers AWS cost management strategies.
Category
Tags
Related Posts
AWS Core Services for DevOps: EC2, ECS, EKS, S3, Lambda
Navigate essential AWS services for DevOps workloads—compute (EC2, ECS, EKS), storage (S3), serverless (Lambda), and foundational networking.
IaC Module Design: Reusable and Composable Infrastructure
Design Terraform modules that are reusable, composable, and maintainable—versioning, documentation, and publish patterns for infrastructure building blocks.
IaC State Management: Remote Backends and Team Collaboration
Manage Terraform/OpenTofu state securely with remote backends, state locking, and strategies for team collaboration without state conflicts.