AWS Builder Center
Infrastructure as Code in Practice: A Hands-On Guide to AWS CDK

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.

student

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

ConceptWhat it isMental model
ConstructA reusable building block with a scope, an id, and propsA React component for infrastructure
StackA construct that becomes one CloudFormation templateA deployable unit (keep it under ~500 resources)
AppThe root that holds your stackscdk.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 typescript
cdk 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-1

A 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.json caches lookups (VPC ids, AZs). Commit it, or your build breaks when a lookup fails in CI.
  • Escaping hatch overuse. If you reach for addPropertyOverride everywhere, you have outgrown L2 — write your own L3 construct instead.
  • Skipping the kill switch. Set "@aws-cdk/core:enableStackNameDuplicates": true style feature flags deliberately, and pin your aws-cdk-lib version; 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.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article