AWS CDK โ
Cloud Development Kit
Define cloud infrastructure using real programming languages โ Python, Java, TypeScript, Go. CDK synthesizes your code into CloudFormation templates and deploys them.
โก CDK in 30 Seconds
- Write infrastructure in Python, Java, TypeScript, C#, or Go โ real code with loops, conditions, abstractions
- CDK synthesizes your code into CloudFormation templates (JSON/YAML)
- Deploys via CloudFormation under the hood โ same reliability, rollback, and drift detection
- Constructs are the building blocks โ from low-level (L1: raw CFN) to high-level (L2/L3: opinionated defaults)
- Free to use โ you pay only for the AWS resources deployed
What is CDK
The AWS Cloud Development Kit (CDK) is an open-source framework that lets you define AWS infrastructure using familiar programming languages. Instead of writing YAML/JSON templates, you write Python classes, Java objects, or TypeScript code โ and CDK converts it into CloudFormation.
๐ Think of CDK as: CloudFormation written in real code โ with variables, loops, functions, and IDE support
CDK doesn't replace CloudFormation โ it generates CloudFormation. Your CDK code is compiled into a CloudFormation template (called "synthesis"), and then CloudFormation handles the actual deployment. You get the power of a programming language with the reliability of CloudFormation.
CloudFormation Pain Points
- YAML/JSON at scale becomes verbose (thousands of lines)
- No loops or conditionals (limited
!If) - Copy-paste patterns across templates
- No IDE autocomplete or type checking
- Hard to abstract reusable components
- No unit testing for infrastructure
CDK Solves
- Real programming โ loops, conditions, inheritance
- IDE support โ autocomplete, type checking, refactoring
- Constructs โ reusable, shareable, composable abstractions
- L2 constructs have smart defaults (less boilerplate)
- Unit testing with standard test frameworks (pytest, JUnit)
- One language for app code AND infrastructure
| Feature | CloudFormation | CDK | Terraform |
|---|---|---|---|
| Language | YAML / JSON | Python, Java, TypeScript, Go, C# | HCL |
| Abstraction | Low (raw resources) | High (L2/L3 constructs with defaults) | Medium (modules) |
| IDE support | Limited (YAML lint) | Full โ autocomplete, types, refactoring | Good (HCL LSP) |
| Testing | None built-in | Unit tests with pytest / JUnit / Jest | terraform plan + Terratest |
| Loops / Logic | Very limited | Full language power | Limited (count, for_each) |
| State | Managed by AWS | Managed by AWS (via CFN) | Self-managed state file |
| Multi-cloud | AWS only | AWS only (CDK for Terraform exists) | Multi-cloud |
| Deploy engine | CloudFormation | CloudFormation | Terraform CLI |
| Best for | Simple stacks, AWS-native shops | Complex infra, dev teams, reusable patterns | Multi-cloud, platform teams |
Python
- Most popular CDK language
- Great for data/ML teams
- pip install aws-cdk-lib
- Fast prototyping
Java
- Enterprise teams
- Strong typing + Maven/Gradle
- Same language as backend
- Best IDE support (IntelliJ)
TypeScript
- CDK is written in TypeScript
- Best documentation coverage
- Full-stack JS/TS teams
- npm install aws-cdk-lib
Here's what a simple S3 bucket looks like in CDK vs CloudFormation:
CloudFormation (YAML)
Resources:
MyBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: my-app-bucket
VersioningConfiguration:
Status: Enabled
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true CDK Python (4 lines)
from aws_cdk import aws_s3 as s3
s3.Bucket(self, "MyBucket",
bucket_name="my-app-bucket",
versioned=True,
encryption=s3.BucketEncryption.S3_MANAGED,
block_public_access=s3.BlockPublicAccess.BLOCK_ALL
) L2 construct โ encryption + public access block are smart defaults (even simpler with no props = secure by default)
๐ The CDK L2 Bucket construct is secure by default โ if you write just s3.Bucket(self, "MyBucket") with no properties, it automatically enables encryption and blocks public access. CloudFormation requires you to explicitly set every property.
| Command | Purpose | Equivalent |
|---|---|---|
cdk init | Scaffold a new CDK project | โ |
cdk synth | Synthesize CDK code โ CloudFormation template | Generate YAML |
cdk diff | Show what will change (like a Change Set preview) | CFN Change Set |
cdk deploy | Deploy stack to AWS (synth + create/update CFN stack) | aws cloudformation deploy |
cdk destroy | Delete the stack and all resources | aws cloudformation delete-stack |
cdk bootstrap | One-time setup โ creates CDK staging S3 bucket + IAM roles | โ |
cdk watch | Hot-deploy on file change (dev mode) | โ |
CDK lets you write infrastructure in real programming languages (Python, Java, TypeScript) and synthesizes it to CloudFormation. You get IDE autocomplete, type safety, loops, abstractions, and unit testing โ while CloudFormation handles the actual deployment with its proven reliability.
Core Concepts โ Apps, Stacks & Constructs
CDK organises infrastructure into a tree hierarchy: App โ Stack(s) โ Construct(s).
| Concept | What It Is | Maps To |
|---|---|---|
| App | The root of the CDK tree. Contains one or more stacks. Entry point for synthesis. | The overall CDK project |
| Stack | A deployable unit = one CloudFormation stack. Contains constructs. | CloudFormation stack |
| Construct | A cloud component (one resource or a composition of many). The fundamental building block. | One or more CloudFormation resources |
CDK organises constructs into three levels of abstraction:
L1 โ CFN Resources
- Direct 1:1 mapping to CloudFormation
- Prefix:
Cfn*(e.g.,CfnBucket) - No smart defaults
- All properties required
- Use when L2 doesn't exist
L2 โ Curated Constructs
- Higher-level, opinionated wrappers
- E.g.,
s3.Bucket,ec2.Vpc - Smart defaults (secure, best practices)
- Helper methods (
.grantRead()) - Most commonly used level
L3 โ Patterns
- Multi-resource architectures in one construct
- E.g.,
LambdaRestApi= Lambda + API GW + IAM - Entire application patterns
- AWS Solutions Constructs library
- Highest abstraction, least flexibility
๐ Python โ CDK App Structure
# app.py โ Entry point
import aws_cdk as cdk
from my_app.network_stack import NetworkStack
from my_app.app_stack import AppStack
app = cdk.App()
# Deploy to specific account/region
env = cdk.Environment(account="123456789012", region="us-east-1")
network = NetworkStack(app, "NetworkStack", env=env)
AppStack(app, "AppStack", vpc=network.vpc, env=env)
app.synth() ๐ Python โ Stack Definition
# my_app/network_stack.py
from aws_cdk import Stack
from aws_cdk import aws_ec2 as ec2
from constructs import Construct
class NetworkStack(Stack):
def __init__(self, scope: Construct, id: str, **kwargs):
super().__init__(scope, id, **kwargs)
# L2 Construct: VPC with best-practice defaults
self.vpc = ec2.Vpc(self, "MainVpc",
max_azs=2,
nat_gateways=1,
subnet_configuration=[
ec2.SubnetConfiguration(
name="Public",
subnet_type=ec2.SubnetType.PUBLIC,
cidr_mask=24
),
ec2.SubnetConfiguration(
name="Private",
subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS,
cidr_mask=24
),
]
)
# One line โ creates VPC + 4 subnets + IGW + NAT + route tables โ Java โ CDK App Structure
// src/main/tech/com/myapp/CdkApp.java
package com.myapp;
import software.amazon.awscdk.App;
import software.amazon.awscdk.Environment;
import software.amazon.awscdk.StackProps;
public class CdkApp {
public static void main(String[] args) {
App app = new App();
Environment env = Environment.builder()
.account("123456789012")
.region("us-east-1")
.build();
StackProps props = StackProps.builder().env(env).build();
NetworkStack network = new NetworkStack(app, "NetworkStack", props);
new AppStack(app, "AppStack", props, network.getVpc());
app.synth();
}
} โ Java โ Stack Definition
// src/main/tech/com/myapp/NetworkStack.java
package com.myapp;
import software.amazon.awscdk.Stack;
import software.amazon.awscdk.StackProps;
import software.amazon.awscdk.services.ec2.*;
import software.constructs.Construct;
import java.util.List;
public class NetworkStack extends Stack {
private final Vpc vpc;
public NetworkStack(Construct scope, String id, StackProps props) {
super(scope, id, props);
// L2 Construct: VPC with best-practice defaults
this.vpc = Vpc.Builder.create(this, "MainVpc")
.maxAzs(2)
.natGateways(1)
.subnetConfiguration(List.of(
SubnetConfiguration.builder()
.name("Public")
.subnetType(SubnetType.PUBLIC)
.cidrMask(24)
.build(),
SubnetConfiguration.builder()
.name("Private")
.subnetType(SubnetType.PRIVATE_WITH_EGRESS)
.cidrMask(24)
.build()
))
.build();
// One call โ VPC + 4 subnets + IGW + NAT + route tables
}
public Vpc getVpc() { return vpc; }
} The power of CDK is composition โ you build complex infrastructure by nesting constructs. A single L2 construct like Vpc generates 15+ CloudFormation resources automatically. You can also create your own reusable constructs.
Python โ Custom Construct
class SecureApi(Construct):
"""Reusable: API GW + Lambda + WAF"""
def __init__(self, scope, id, *,
handler_path: str,
stage: str):
super().__init__(scope, id)
fn = _lambda.Function(self, "Handler",
runtime=_lambda.Runtime.PYTHON_3_12,
code=_lambda.Code.from_asset(handler_path),
handler="index.handler",
)
self.api = apigateway.LambdaRestApi(
self, "Endpoint",
handler=fn,
deploy_options={"stage_name": stage}
)
Java โ Custom Construct
public class SecureApi extends Construct {
private final LambdaRestApi api;
public SecureApi(Construct scope, String id,
String handlerPath, String stage) {
super(scope, id);
Function fn = Function.Builder.create(this, "Handler")
.runtime(Runtime.PYTHON_3_12)
.code(Code.fromAsset(handlerPath))
.handler("index.handler")
.build();
this.api = LambdaRestApi.Builder
.create(this, "Endpoint")
.handler(fn)
.deployOptions(StageOptions.builder()
.stageName(stage).build())
.build();
}
public LambdaRestApi getApi() { return api; }
}
Now use your custom construct in any stack:
๐ Python Usage
# In any stack:
api = SecureApi(self, "OrdersApi",
handler_path="lambda/orders",
stage="prod"
) โ Java Usage
// In any stack:
SecureApi api = new SecureApi(
this, "OrdersApi",
"lambda/orders", "prod"
); One of CDK's killer features: L2 constructs expose .grant*() methods that automatically create the correct IAM policies โ no manual policy writing.
Python
# Lambda needs to read from S3 bucket
bucket = s3.Bucket(self, "DataBucket")
fn = _lambda.Function(self, "Processor", ...)
# ONE LINE โ creates IAM policy automatically
bucket.grant_read(fn)
# Other grant methods:
# bucket.grant_read_write(fn)
# table.grant_read_data(fn)
# queue.grant_send_messages(fn)
# topic.grant_publish(fn) Java
// Lambda needs to read from S3 bucket
Bucket bucket = Bucket.Builder.create(this, "DataBucket")
.build();
Function fn = Function.Builder.create(this, "Processor")
...
.build();
// ONE LINE โ creates IAM policy automatically
bucket.grantRead(fn);
// Other grant methods:
// bucket.grantReadWrite(fn);
// table.grantReadData(fn);
// queue.grantSendMessages(fn);
// topic.grantPublish(fn); ๐ Without CDK, you'd write a 15-line IAM policy document with the correct S3 actions (s3:GetObject, s3:ListBucket), the bucket ARN, and the objects ARN (arn:aws:s3:::bucket/*). With CDK: bucket.grant_read(fn) does all of this automatically with least-privilege.
cdk bootstrap is a one-time setup command that prepares your AWS account for CDK deployments. It creates a CloudFormation stack called CDKToolkit containing:
What Bootstrap Creates
- S3 bucket โ stores synthesized templates and assets (Lambda zips, Docker images)
- ECR repository โ stores Docker images for container constructs
- IAM roles โ deployment, file publishing, lookup roles
- SSM parameter โ bootstrap version tracking
When to Run
- Once per account + region combination
- Before the first
cdk deploy - Re-run when CDK updates the bootstrap template
- Cross-account deploys need bootstrap in target account
Python Project
my-cdk-app/
โโโ app.py # Entry point
โโโ cdk.json # CDK config
โโโ requirements.txt # Dependencies
โโโ my_app/
โ โโโ __init__.py
โ โโโ network_stack.py
โ โโโ app_stack.py
โโโ tests/
โโโ test_app_stack.py Java Project
my-cdk-app/
โโโ cdk.json # CDK config
โโโ pom.xml # Maven deps
โโโ src/main/tech/com/myapp/
โ โโโ CdkApp.java # Entry point
โ โโโ NetworkStack.java
โ โโโ AppStack.java
โโโ src/test/tech/com/myapp/
โโโ AppStackTest.java CDK organises infra as App โ Stacks โ Constructs. L1 = raw CFN, L2 = smart defaults + grant methods, L3 = full patterns. Composition lets you build reusable custom constructs. Grant methods eliminate manual IAM policy writing. Bootstrap once per account/region before first deploy.
Building Stacks
A common serverless pattern: API Gateway โ Lambda โ DynamoDB. In CDK, this is just a few constructs wired together with grant methods handling all IAM.
Python
from aws_cdk import Stack, RemovalPolicy
from aws_cdk import aws_lambda as _lambda
from aws_cdk import aws_apigateway as apigw
from aws_cdk import aws_dynamodb as dynamodb
from constructs import Construct
class ServerlessApiStack(Stack):
def __init__(self, scope: Construct, id: str, **kwargs):
super().__init__(scope, id, **kwargs)
# DynamoDB table
table = dynamodb.Table(self, "Orders",
partition_key=dynamodb.Attribute(
name="orderId",
type=dynamodb.AttributeType.STRING
),
billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST,
removal_policy=RemovalPolicy.DESTROY,
)
# Lambda function
handler = _lambda.Function(self, "OrderHandler",
runtime=_lambda.Runtime.PYTHON_3_12,
code=_lambda.Code.from_asset("lambda"),
handler="orders.handler",
environment={
"TABLE_NAME": table.table_name,
},
)
# Grant Lambda read/write access to DynamoDB
table.grant_read_write_data(handler)
# API Gateway
api = apigw.RestApi(self, "OrdersApi",
rest_api_name="Orders Service",
)
orders = api.root.add_resource("orders")
orders.add_method("GET", apigw.LambdaIntegration(handler))
orders.add_method("POST", apigw.LambdaIntegration(handler))
Java
import software.amazon.awscdk.*;
import software.amazon.awscdk.services.lambda.*;
import software.amazon.awscdk.services.apigateway.*;
import software.amazon.awscdk.services.dynamodb.*;
import software.constructs.Construct;
import java.util.Map;
public class ServerlessApiStack extends Stack {
public ServerlessApiStack(Construct scope, String id,
StackProps props) {
super(scope, id, props);
// DynamoDB table
Table table = Table.Builder.create(this, "Orders")
.partitionKey(Attribute.builder()
.name("orderId")
.type(AttributeType.STRING).build())
.billingMode(BillingMode.PAY_PER_REQUEST)
.removalPolicy(RemovalPolicy.DESTROY)
.build();
// Lambda function
Function handler = Function.Builder.create(this, "OrderHandler")
.runtime(Runtime.PYTHON_3_12)
.code(Code.fromAsset("lambda"))
.handler("orders.handler")
.environment(Map.of(
"TABLE_NAME", table.getTableName()
))
.build();
// Grant Lambda read/write to DynamoDB
table.grantReadWriteData(handler);
// API Gateway
RestApi api = RestApi.Builder.create(this, "OrdersApi")
.restApiName("Orders Service")
.build();
Resource orders = api.getRoot().addResource("orders");
orders.addMethod("GET", new LambdaIntegration(handler));
orders.addMethod("POST", new LambdaIntegration(handler));
}
}
๐ What CDK generated behind the scenes: ~25 CloudFormation resources โ Lambda function + execution role + policy, API Gateway + deployment + stage + methods, DynamoDB table, Lambda permission for API GW. You wrote ~40 lines of code.
Python โ ECS Fargate Service
from aws_cdk import Stack
from aws_cdk import aws_ec2 as ec2
from aws_cdk import aws_ecs as ecs
from aws_cdk import aws_ecs_patterns as patterns
from constructs import Construct
class WebAppStack(Stack):
def __init__(self, scope: Construct, id: str, **kwargs):
super().__init__(scope, id, **kwargs)
vpc = ec2.Vpc(self, "AppVpc", max_azs=2)
cluster = ecs.Cluster(self, "Cluster", vpc=vpc)
# L3 Pattern: ALB + Fargate in ONE construct
service = patterns.ApplicationLoadBalancedFargateService(
self, "WebService",
cluster=cluster,
cpu=256,
memory_limit_mib=512,
desired_count=2,
task_image_options=patterns.ApplicationLoadBalancedTaskImageOptions(
image=ecs.ContainerImage.from_asset("./docker"),
container_port=8080,
),
public_load_balancer=True,
)
# Auto-scaling
scaling = service.service.auto_scale_task_count(
min_capacity=2, max_capacity=10
)
scaling.scale_on_cpu_utilization("CpuScale",
target_utilization_percent=70
)
Java โ ECS Fargate Service
import software.amazon.awscdk.*;
import software.amazon.awscdk.services.ec2.*;
import software.amazon.awscdk.services.ecs.*;
import software.amazon.awscdk.services.ecs.patterns.*;
import software.constructs.Construct;
public class WebAppStack extends Stack {
public WebAppStack(Construct scope, String id, StackProps props) {
super(scope, id, props);
Vpc vpc = Vpc.Builder.create(this, "AppVpc")
.maxAzs(2).build();
Cluster cluster = Cluster.Builder.create(this, "Cluster")
.vpc(vpc).build();
// L3 Pattern: ALB + Fargate in ONE construct
ApplicationLoadBalancedFargateService service =
ApplicationLoadBalancedFargateService.Builder
.create(this, "WebService")
.cluster(cluster)
.cpu(256)
.memoryLimitMiB(512)
.desiredCount(2)
.taskImageOptions(
ApplicationLoadBalancedTaskImageOptions.builder()
.image(ContainerImage.fromAsset("./docker"))
.containerPort(8080)
.build())
.publicLoadBalancer(true)
.build();
// Auto-scaling
ScalableTaskCount scaling = service.getService()
.autoScaleTaskCount(EnableScalingProps.builder()
.minCapacity(2).maxCapacity(10).build());
scaling.scaleOnCpuUtilization("CpuScale",
CpuUtilizationScalingProps.builder()
.targetUtilizationPercent(70).build());
}
}
The L3 pattern ApplicationLoadBalancedFargateService creates: VPC, ALB, target group, ECS cluster, task definition, Fargate service, security groups, IAM roles, CloudWatch log group โ all wired together with correct permissions.
CDK uses context values and environment-specific configurations to customize deployments:
Python โ Environment Config
# app.py
app = cdk.App()
# Read context from cdk.json or CLI
env_name = app.node.try_get_context("env") or "dev"
config = {
"dev": {
"instance_type": "t3.micro",
"min_capacity": 1,
"max_capacity": 2,
},
"prod": {
"instance_type": "t3.large",
"min_capacity": 2,
"max_capacity": 20,
},
}
AppStack(app, f"AppStack-{env_name}",
config=config[env_name],
env=cdk.Environment(
account="123456789012",
region="us-east-1"
)
)
# Deploy: cdk deploy -c env=prod Java โ Environment Config
// CdkApp.java
App app = new App();
String envName = (String) app.getNode()
.tryGetContext("env");
if (envName == null) envName = "dev";
Map<String, Map<String, Object>> config = Map.of(
"dev", Map.of(
"instanceType", "t3.micro",
"minCapacity", 1,
"maxCapacity", 2
),
"prod", Map.of(
"instanceType", "t3.large",
"minCapacity", 2,
"maxCapacity", 20
)
);
new AppStack(app, "AppStack-" + envName,
StackProps.builder()
.env(Environment.builder()
.account("123456789012")
.region("us-east-1").build())
.build(),
config.get(envName)
);
// Deploy: cdk deploy -c env=prod CDK assets are local files (Lambda code, Docker images) that CDK automatically uploads to S3/ECR during deployment.
| Asset Type | What It Does | Example |
|---|---|---|
Code.from_asset("dir") | Zips a directory and uploads to S3 for Lambda | Lambda function code from local folder |
ContainerImage.from_asset("dir") | Builds Docker image locally, pushes to ECR | ECS/Fargate container from local Dockerfile |
BundlingOptions | Run a build step (pip install, mvn package) before upload | Install Python deps into Lambda zip |
CDK excels at full-stack deployments โ from serverless (Lambda + API GW + DynamoDB) to containerised (ECS Fargate + ALB). L3 patterns encapsulate entire architectures in one construct. Context values and environment maps enable multi-environment deployments from a single codebase. Assets handle code bundling and Docker builds automatically.
Patterns & Testing
One of CDK's biggest advantages: you can unit test your infrastructure using the same testing frameworks as your application code. CDK provides assertion libraries to verify the synthesized CloudFormation template.
Python โ pytest
# tests/test_app_stack.py
import aws_cdk as cdk
from aws_cdk.assertions import Template, Match
from my_app.serverless_api_stack import ServerlessApiStack
def test_dynamodb_table_created():
app = cdk.App()
stack = ServerlessApiStack(app, "TestStack")
template = Template.from_stack(stack)
# Assert DynamoDB table exists
template.has_resource_properties(
"AWS::DynamoDB::Table", {
"KeySchema": [{
"AttributeName": "orderId",
"KeyType": "HASH",
}],
"BillingMode": "PAY_PER_REQUEST",
}
)
def test_lambda_has_env_vars():
app = cdk.App()
stack = ServerlessApiStack(app, "TestStack")
template = Template.from_stack(stack)
template.has_resource_properties(
"AWS::Lambda::Function", {
"Environment": {
"Variables": {
"TABLE_NAME": Match.any_value(),
}
},
"Runtime": "python3.12",
}
)
def test_api_gateway_exists():
app = cdk.App()
stack = ServerlessApiStack(app, "TestStack")
template = Template.from_stack(stack)
template.resource_count_is("AWS::ApiGateway::RestApi", 1)
# Run: pytest tests/ Java โ JUnit 5
// src/test/tech/com/myapp/ServerlessApiStackTest.java
import org.junit.jupiter.api.Test;
import software.amazon.awscdk.App;
import software.amazon.awscdk.assertions.Template;
import software.amazon.awscdk.assertions.Match;
import java.util.Map;
class ServerlessApiStackTest {
@Test
void dynamoDbTableCreated() {
App app = new App();
ServerlessApiStack stack = new ServerlessApiStack(
app, "TestStack", null);
Template template = Template.fromStack(stack);
template.hasResourceProperties(
"AWS::DynamoDB::Table",
Map.of(
"KeySchema", java.util.List.of(
Map.of("AttributeName", "orderId",
"KeyType", "HASH")),
"BillingMode", "PAY_PER_REQUEST"
)
);
}
@Test
void lambdaHasEnvironmentVars() {
App app = new App();
ServerlessApiStack stack = new ServerlessApiStack(
app, "TestStack", null);
Template template = Template.fromStack(stack);
template.hasResourceProperties(
"AWS::Lambda::Function",
Map.of(
"Runtime", "python3.12",
"Environment", Match.objectLike(
Map.of("Variables",
Match.objectLike(Map.of(
"TABLE_NAME", Match.anyValue()
))
)
)
)
);
}
@Test
void apiGatewayExists() {
App app = new App();
ServerlessApiStack stack = new ServerlessApiStack(
app, "TestStack", null);
Template template = Template.fromStack(stack);
template.resourceCountIs(
"AWS::ApiGateway::RestApi", 1);
}
}
// Run: mvn test Snapshot tests capture the entire synthesized template and compare it against a stored baseline. Any change to the infrastructure produces a diff โ catching unintended modifications.
Python Snapshot
def test_snapshot(snapshot):
app = cdk.App()
stack = ServerlessApiStack(app, "TestStack")
template = Template.from_stack(stack)
# Compare against stored snapshot
assert template.to_json() == snapshot Java Snapshot
@Test
void snapshotTest() {
App app = new App();
ServerlessApiStack stack = new ServerlessApiStack(
app, "TestStack", null);
Template template = Template.fromStack(stack);
// Match against stored snapshot
template.templateMatches(
Match.objectEquals(expectedTemplate));
} CDK Pipelines is a construct that creates a self-mutating CI/CD pipeline for your CDK app. The pipeline automatically updates itself when you change the pipeline definition โ and deploys your infrastructure through stages.
Python โ CDK Pipeline
from aws_cdk import Stack
from aws_cdk.pipelines import CodePipeline, CodeBuildStep
from aws_cdk.pipelines import CodePipelineSource
from constructs import Construct
class PipelineStack(Stack):
def __init__(self, scope, id, **kwargs):
super().__init__(scope, id, **kwargs)
pipeline = CodePipeline(self, "Pipeline",
synth=CodeBuildStep("Synth",
input=CodePipelineSource.git_hub(
"my-org/my-repo", "main"),
commands=[
"pip install -r requirements.txt",
"npx cdk synth",
]
)
)
# Add stages
pipeline.add_stage(StagingStage(self, "Staging"))
pipeline.add_stage(ProdStage(self, "Prod"),
pre=[
pipelines.ManualApprovalStep("Approve")
]
) Java โ CDK Pipeline
import software.amazon.awscdk.*;
import software.amazon.awscdk.pipelines.*;
import software.constructs.Construct;
import java.util.List;
public class PipelineStack extends Stack {
public PipelineStack(Construct scope, String id,
StackProps props) {
super(scope, id, props);
CodePipeline pipeline = CodePipeline.Builder
.create(this, "Pipeline")
.synth(CodeBuildStep.Builder.create("Synth")
.input(CodePipelineSource.gitHub(
"my-org/my-repo", "main"))
.commands(List.of(
"mvn package",
"npx cdk synth"
))
.build())
.build();
// Add stages
pipeline.addStage(new StagingStage(this, "Staging"));
pipeline.addStage(new ProdStage(this, "Prod"),
AddStageOpts.builder()
.pre(List.of(
new ManualApprovalStep("Approve")))
.build());
}
} When an L2 construct doesn't expose a property you need, escape hatches let you access the underlying L1 (CFN) resource and modify it directly.
Python
# Access the underlying CFN resource
bucket = s3.Bucket(self, "MyBucket")
# Escape hatch: get the L1 CfnBucket
cfn_bucket = bucket.node.default_child
# Modify a property not exposed by L2
cfn_bucket.add_property_override(
"AnalyticsConfigurations", [{
"Id": "FullAnalytics",
"StorageClassAnalysis": {
"DataExport": {
"Destination": {...}
}
}
}]
) Java
// Access the underlying CFN resource
Bucket bucket = Bucket.Builder.create(this, "MyBucket")
.build();
// Escape hatch: get the L1 CfnBucket
CfnBucket cfnBucket = (CfnBucket) bucket.getNode()
.getDefaultChild();
// Modify a property not exposed by L2
cfnBucket.addPropertyOverride(
"AnalyticsConfigurations",
List.of(Map.of(
"Id", "FullAnalytics",
"StorageClassAnalysis", Map.of(...)
))
); CDK enables real unit testing of infrastructure (assertions + snapshots). CDK Pipelines create self-mutating CI/CD โ the pipeline updates itself before deploying your app. Escape hatches let you access L1 resources when L2 constructs don't expose what you need.
Best Practices
Do
- One stack per lifecycle โ network, data, and app stacks deploy independently
- Use L2 constructs โ they enforce best practices (encryption, IAM least privilege)
- Use grant methods โ never write IAM policies manually
- Tag everything โ
cdk.Tags.of(stack).add("Team", "orders") - Use
RemovalPolicy.RETAINon databases and S3 buckets in prod - Write tests โ at minimum, assertion tests for critical resources
- Use CDK Pipelines for production deployments
Avoid
- God stacks โ everything in one stack (blast radius, slow deploys)
- Hardcoded account/region โ use environment variables or context
- Ignoring
cdk diffโ always review before deploy - Custom IAM policies when grant methods exist
- L1 constructs when L2 exists (lose smart defaults)
- Skipping bootstrap โ leads to confusing deploy errors
- Mutable infrastructure โ don't SSH in and change things manually
Create reusable construct libraries that your team or organisation can share โ like internal npm/PyPI/Maven packages for infrastructure patterns.
| Language | Package Manager | Publish Command |
|---|---|---|
| Python | PyPI / CodeArtifact | twine upload dist/* |
| Java | Maven Central / CodeArtifact | mvn deploy |
| TypeScript | npm / CodeArtifact | npm publish |
๐ Projen is a tool that generates and maintains CDK construct library project configuration. Use it to standardise multi-language construct libraries with consistent builds, tests, and publishing.
| Scenario | Best Choice | Why |
|---|---|---|
| AWS-only, dev team owns infra | CDK | Same language as app, testing, IDE support |
| Multi-cloud (AWS + Azure + GCP) | Terraform | Single tool for all providers |
| Platform team serving many app teams | Terraform | HCL is consistent, state management mature |
| Complex logic (loops, conditionals, inheritance) | CDK | Real programming language vs HCL limits |
| Strict compliance / policy-as-code | Terraform + Sentinel | Mature policy engine |
| Rapid prototyping / serverless | CDK | L3 patterns deploy full architectures fast |
| Existing CloudFormation investment | CDK | Imports existing CFN, same engine underneath |
Approach 1: Import Existing Stack
- Use
CfnIncludeto import existing CFN template into CDK - Gradually replace L1 resources with L2 constructs
- No need to recreate resources โ same stack continues
- Allows incremental migration
Approach 2: Green-Field Rewrite
- Write new CDK stacks alongside existing CFN
- Migrate resources between stacks using import/export
- Delete old CFN stack once migration complete
- Best for major architecture refactoring
| Mistake | Impact | Fix |
|---|---|---|
| Changing construct ID | Resource replacement (data loss for DBs!) | Never rename construct IDs after first deploy |
Not running cdk diff | Surprise replacements in production | Always diff before deploy; use CDK Pipelines |
| Using L1 when L2 exists | Lose secure defaults, more code | Check if L2 construct is available first |
| Forgetting RemovalPolicy | Stack delete = data gone | removal_policy=RemovalPolicy.RETAIN for prod data |
| Too many stacks | Deployment ordering complexity | Balance: split by lifecycle, not too granular |
| No tests | Broken infra caught only at deploy time | Write assertion tests; run in CI before deploy |
| Ignoring bootstrap version | Deploy failures in new regions/accounts | Re-run cdk bootstrap when upgrading CDK versions |
AWS CDK โ Complete Summary
- CDK โ write infrastructure in Python/Java/TypeScript, synthesize to CloudFormation, deploy with proven CFN engine.
- Constructs โ L1 (raw CFN), L2 (smart defaults + grants), L3 (full patterns like ALB+Fargate).
- Architecture โ App โ Stacks โ Constructs. One stack = one CloudFormation stack.
- Grant methods โ
bucket.grant_read(fn)eliminates manual IAM policy writing. - Testing โ Unit test synthesized templates with assertions (pytest/JUnit). Snapshot testing for regression.
- CDK Pipelines โ self-mutating CI/CD. Source โ Build โ Self-Update โ Deploy staging โ Approve โ Deploy prod.
- Escape hatches โ access L1 resources via
node.default_childwhen L2 doesn't expose a property. - Best practices โ split stacks by lifecycle, use L2 constructs, grant methods for IAM, RemovalPolicy for data, test before deploy.
CDK is the most developer-friendly IaC tool for AWS โ same language as your app, full IDE support, unit testable infrastructure, and opinionated L2/L3 constructs that enforce best practices. Use CDK Pipelines for production, always run cdk diff, and never change construct IDs after first deployment.