The Problem with CloudFormation
If you’ve spent any time managing AWS infrastructure, you know the pain. A simple API Gateway + Lambda + DynamoDB setup can balloon into 500+ lines of YAML. You copy-paste the same VPC config across ten stacks, then realize you missed updating a CIDR block in three places. Refactoring becomes archaeology.
CloudFormation works — but it treats your infrastructure like a document, not code. No loops. No functions. No type checking. When something breaks, the error messages are cryptic. Rollback on a moderately complex stack? Budget 20 minutes, minimum.
The issue isn’t CloudFormation itself — it’s the paradigm. Declarative YAML was designed for human-readable config, not for 15 microservices running across 3 environments. Beyond a certain scale, you’re no longer writing infrastructure. You’re maintaining YAML full-time.
Core Concepts: What AWS CDK Actually Does
AWS CDK (Cloud Development Kit) flips the model. You write TypeScript code that generates the CloudFormation template for you — automatically, at deploy time.
That shift carries real weight. You get IDE autocomplete and compile-time type checking. Real loops and conditionals. The ability to share infrastructure as versioned npm packages. And critically, errors that surface before deployment — not halfway through a 20-minute rollout.
CDK organizes resources around three levels of constructs:
- L1 (Cfn* classes): Direct wrappers around CloudFormation resources — full control, but verbose. Maps 1:1 to every property in the CloudFormation spec.
- L2: Higher-level abstractions with sensible defaults and helper methods. This is what you’ll use for 90% of your work.
- L3 (Patterns): Opinionated, pre-built solutions for common multi-resource architectures like an ECS service behind an ALB.
Run cdk deploy and CDK synthesizes your TypeScript into a CloudFormation template, then deploys it. The YAML still exists under the hood — you just never have to touch it.
After switching from raw templates to CDK, we went from maintaining roughly 8,000 lines of YAML across 12 stacks to a TypeScript codebase any developer could read in a single afternoon. That shift changed how the whole team thinks about infrastructure.
Hands-on Practice
Prerequisites and Installation
You’ll need Node.js 18+, the AWS CLI configured with credentials, and an active AWS account.
npm install -g aws-cdk
cdk --version
Bootstrap your account once per account/region pair — this creates the S3 bucket and IAM roles CDK needs to operate:
aws configure # Set your access key, secret, and default region
cdk bootstrap aws://YOUR_ACCOUNT_ID/ap-northeast-1
Create Your First CDK Project
mkdir my-infra && cd my-infra
cdk init app --language typescript
Two files do all the real work:
my-infra/
├── bin/
│ └── my-infra.ts # App entry point — instantiates your stacks
├── lib/
│ └── my-infra-stack.ts # Stack definition — where your resources live
├── cdk.json
└── package.json
Build a Real Stack: S3 + Lambda + API Gateway
Open lib/my-infra-stack.ts and replace the default content with a real-world pattern:
import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import { Construct } from 'constructs';
export class MyInfraStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// S3 bucket with versioning and server-side encryption
const bucket = new s3.Bucket(this, 'StorageBucket', {
versioned: true,
encryption: s3.BucketEncryption.S3_MANAGED,
removalPolicy: cdk.RemovalPolicy.DESTROY, // Change to RETAIN for production
});
// Lambda handler
const handler = new lambda.Function(this, 'ApiHandler', {
runtime: lambda.Runtime.NODEJS_20_X,
code: lambda.Code.fromAsset('src/lambda'),
handler: 'index.handler',
environment: {
BUCKET_NAME: bucket.bucketName,
},
});
// One line grants the correct IAM policy automatically
bucket.grantReadWrite(handler);
// API Gateway wired to Lambda
const api = new apigateway.RestApi(this, 'MyApi', {
restApiName: 'My Service API',
});
api.root.addMethod('GET', new apigateway.LambdaIntegration(handler));
new cdk.CfnOutput(this, 'ApiUrl', { value: api.url });
}
}
That bucket.grantReadWrite(handler) call is doing real work. It constructs the correct IAM policy and attaches it to the Lambda execution role automatically. The equivalent in raw CloudFormation is 15–20 lines of YAML — and you’d need to update them manually whenever the bucket name or role ARN changed.
Create the Lambda handler:
mkdir -p src/lambda
// src/lambda/index.ts
export const handler = async (event: any) => {
return {
statusCode: 200,
body: JSON.stringify({
message: 'Hello from CDK!',
bucket: process.env.BUCKET_NAME,
}),
};
};
Preview Before You Deploy
Don’t deploy blind. Check the diff first:
cdk diff # Shows resource changes — like git diff for infrastructure
cdk synth # Generates the raw CloudFormation template so you can inspect it
When the diff looks right, deploy:
cdk deploy
CDK prompts you to confirm any IAM permission changes — a deliberate safety net. After that, it streams deployment progress directly to your terminal.
Multi-Environment Support Without Duplication
Here’s where CDK pulls clearly ahead of hand-written templates. Parameterize your stack at the app level:
// bin/my-infra.ts
import * as cdk from 'aws-cdk-lib';
import { MyInfraStack } from '../lib/my-infra-stack';
const app = new cdk.App();
const env = app.node.tryGetContext('env') || 'dev';
new MyInfraStack(app, `MyInfra-${env}`, {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
},
});
One flag deploys to any environment:
cdk deploy -c env=staging
cdk deploy -c env=prod
Reusable Constructs: Where CDK Gets Serious
Start building multiple stacks and you’ll notice the same patterns appearing everywhere. CDK lets you extract them into composable constructs that bake in your team’s standards:
// lib/constructs/monitored-lambda.ts
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import { Construct } from 'constructs';
interface MonitoredLambdaProps extends lambda.FunctionProps {}
export class MonitoredLambda extends Construct {
public readonly function: lambda.Function;
constructor(scope: Construct, id: string, props: MonitoredLambdaProps) {
super(scope, id);
this.function = new lambda.Function(this, 'Function', {
...props,
tracing: lambda.Tracing.ACTIVE, // X-Ray always enabled
});
// Every Lambda automatically gets an error rate alarm
new cloudwatch.Alarm(this, 'ErrorAlarm', {
metric: this.function.metricErrors(),
threshold: 5,
evaluationPeriods: 1,
});
}
}
From now on, every Lambda your team spins up gets X-Ray tracing and a CloudWatch error alarm by default. Nobody forgets. No configuration drift between stacks.
Tearing Down Resources
cdk destroy
CDK removes everything it provisioned. Resources marked with RemovalPolicy.RETAIN — production databases, for instance — are skipped with a warning instead of deleted.
When CDK Makes Sense, and When It Doesn’t
CDK earns its keep when infrastructure complexity justifies abstraction. Repeated patterns across environments, a team that already knows TypeScript or Python, a need to distribute infrastructure as versioned packages — any one of these tips the balance. All three? Easy call.
For a handful of static resources that barely change, raw CloudFormation or the AWS console is genuinely simpler. No ceremony required. If your team is deep in Terraform, its HCL syntax and massive module registry may also be a better fit — especially for multi-cloud setups, since CDK is AWS-only.
For teams shipping on AWS at real scale, though, CDK has one compounding advantage: your infrastructure becomes a real codebase. Write unit tests with aws-cdk-lib/assertions. Review infrastructure changes in pull requests. Catch regressions before they hit production.
The teams that get the most out of CDK treat lib/ the same way they treat application code — composable constructs, test coverage, meaningful commit history. That discipline pays off fast when you’re managing 30+ stacks across dev, staging, and prod, and a new engineer can map the entire architecture in a single session.

