
Infrastructure as Code in Practice: A Hands-On Guide to AWS CDK
A practical walkthrough of the AWS Cloud Development Kit: how constructs, stacks and synths work, when to choose TypeScript or Python, and a working example that deploys an API with a DynamoDB table behind Amazon CloudFront.
Why I moved from YAML to CDK
I spent two years maintaining CloudFormation templates that nobody on the team wanted to touch. Nested stacks, string substitution, and no way to write a unit test. The AWS Cloud Development Kit (CDK) fixed the part that actually hurt: I could finally express infrastructure in a programming language I already knew.
The core idea is simple. You define constructs in TypeScript, Python, Java, C#, Go or .NET, and the CDK synthesizes them into a CloudFormation template at deploy time. You still get CloudFormation's change sets, drift detection, and rollback semantics — you just stop hand-writing YAML.
The three objects you must understand
| Concept | What it is | Mental model |
|---|---|---|
| Construct | A reusable building block with a scope, an id, and props | A React component for infrastructure |
| Stack | A construct that becomes one CloudFormation template | A deployable unit (keep it under ~500 resources) |
| App | The root that holds your stacks | cdk.json + your entry point |
Constructs come in three layers: L1 (auto-generated, 1:1 with CloudFormation resources), L2 (curated with sensible defaults, e.g.
aws-s3-bucket.Bucket), and L3 / patterns (opinionated bundles such as aws-apigateway.LambdaRestApi). Reach for the highest level that still gives you control, then drop down a layer only where you need it.Getting started
1
2
3
npm install -g aws-cdk
mkdir cdk-demo && cd cdk-demo
cdk init app --language typescriptcdk init scaffolds bin/app.ts, lib/*-stack.ts, cdk.json and a Jest test harness. Bootstrap once per account/region so the toolkit has somewhere to put assets:1
cdk bootstrap aws://123456789012/us-east-1A working example: API + table + CDN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigw from 'aws-cdk-lib/aws-apigateway';
import * as ddb from 'aws-cdk-lib/aws-dynamodb';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
export class EdgeStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const table = new ddb.Table(this, 'Items', {
partitionKey: { name: 'pk', type: ddb.AttributeType.STRING },
billingMode: ddb.BillingMode.PAY_PER_REQUEST,
removalPolicy: cdk.RemovalPolicy.RETAIN,
});
const handler = new lambda.Function(this, 'Handler', {
runtime: lambda.Runtime.NODEJS_22_X,
architecture: lambda.Architecture.ARM_64,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda'),
environment: { TABLE_NAME: table.tableName },
});
table.grantReadWriteData(handler);
const api = new apigw.RestApi(this, 'Api', {
deployOptions: { stageName: 'prod', tracingEnabled: true },
});
const items = api.root.addResource('items');
items.addMethod('GET', new apigw.LambdaIntegration(handler));
new cloudfront.Distribution(this, 'Cdn', {
defaultBehavior: { origin: new origins.RestApiOrigin(api) },
});
}
}Two details worth calling out.
grantReadWriteData writes least-privilege IAM for you instead of a wildcard policy, and RemovalPolicy.RETAIN stops cdk destroy from deleting production data.Test before you deploy
Synth-time assertions are the feature people underestimate:
1
2
3
4
5
6
7
test('table uses pay-per-request', () => {
const app = new cdk.App();
const stack = new EdgeStack(app, 'Test');
Template.fromStack(stack).hasResourceProperties('AWS::DynamoDB::Table', {
BillingMode: 'PAY_PER_REQUEST',
});
});Run
npx jest, then cdk diff against the deployed stack to see exactly what will change before you run cdk deploy.Pitfalls I keep hitting
- One giant stack. Split by lifecycle or blast radius; cross-stack references resolve automatically but create dependencies you should review in
cdk diff. - Context drift.
cdk.context.jsoncaches lookups (VPC ids, AZs). Commit it, or your build breaks when a lookup fails in CI. - Escaping hatch overuse. If you reach for
addPropertyOverrideeverywhere, you have outgrown L2 — write your own L3 construct instead. - Skipping the kill switch. Set
"@aws-cdk/core:enableStackNameDuplicates": truestyle feature flags deliberately, and pin youraws-cdk-libversion; behavior changes ship behind flags.
When CDK is the wrong tool
If your team reviews raw templates in pull requests, or you need a UI-driven path for non-developers, CloudFormation templates or Service Catalog may fit better. CDK pays off when infrastructure needs loops, conditionals, validation, and shared abstractions across many services.
Next steps
Start with one low-risk stack, add a
cdk diff gate in CI, then extract your first custom construct once you copy-paste the same block twice. That is usually the moment IaC stops feeling like configuration and starts feeling like software.Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article