AWS CDK Patterns That Keep Infrastructure Code Maintainable
The AWS CDK gives you real code and real abstractions — and real ways to make a mess. Here are the constructs, stack, and testing patterns that scale.
- #iac
- #aws-cdk
- #aws
- #typescript
- #python
- #cloud
The AWS CDK lets you define cloud infrastructure in TypeScript, Python, Java, or Go, then synthesizes it down to CloudFormation. The pitch is compelling: real programming-language abstractions on top of AWS’s battle-tested deployment engine. But “real code” cuts both ways — the CDK gives you the tools to build clean, reusable infrastructure and equally good tools to build an unmaintainable tangle.
I’ve inherited CDK codebases at both extremes. The difference is almost always a handful of patterns. Here are the ones worth adopting from day one.
Understand the three construct levels
The CDK’s core abstraction is the construct, and they come in three levels you should be able to name:
- L1 (Cfn*) — raw CloudFormation resources, a 1:1 mapping.
CfnBucket. Verbose, no defaults, but complete coverage of every AWS feature. - L2 — curated, opinionated resources with sane defaults and helper methods.
s3.Bucket. This is where you live 90% of the time. - L3 (patterns) — multi-resource solutions for a whole use case.
ApplicationLoadBalancedFargateServicestands up an ECS service, ALB, target groups, and security groups in one construct.
Reach for the highest level that fits, and drop down only when you need control the higher level doesn’t expose. The classic mistake is writing everything in L1 because that’s what the CloudFormation docs show — you throw away the entire point of the CDK.
// L2 — defaults handle encryption, blocks public access, etc.
const bucket = new s3.Bucket(this, 'Assets', {
versioned: true,
encryption: s3.BucketEncryption.S3_MANAGED,
removalPolicy: cdk.RemovalPolicy.RETAIN,
});
Build your own L3 constructs for org standards
The highest-leverage CDK pattern: wrap your organization’s conventions into a custom construct. Instead of every team re-deriving “a Lambda with our standard logging, tracing, and alarms,” you write it once:
export class StandardFunction extends Construct {
public readonly fn: lambda.Function;
constructor(scope: Construct, id: string, props: StandardFunctionProps) {
super(scope, id);
this.fn = new lambda.Function(this, 'Fn', {
runtime: lambda.Runtime.NODEJS_20_X,
tracing: lambda.Tracing.ACTIVE, // X-Ray on by default
logRetention: logs.RetentionDays.ONE_MONTH,
...props,
});
// Org-standard error-rate alarm, baked in
new cloudwatch.Alarm(this, 'Errors', {
metric: this.fn.metricErrors(),
threshold: 1,
evaluationPeriods: 1,
});
}
}
Now every Lambda in the company gets tracing, log retention, and an error alarm automatically. This is how a platform team encodes standards into something developers want to use because it’s less work than rolling their own. It’s the same self-service idea behind platform engineering, expressed in the CDK’s native unit.
Separate stateful and stateless stacks
A hard-won lesson: put stateful resources (databases, S3 buckets, anything you can’t afford to lose) in a different stack from stateless ones (Lambdas, API Gateways, ECS services). Stateless stacks get deployed constantly and occasionally need to be torn down and recreated; you never want a routine app deploy to risk your database.
const data = new DataStack(app, 'Data'); // RDS, S3 — rarely changes
const compute = new ComputeStack(app, 'Compute', { // deployed constantly
database: data.database,
});
Pass references across stacks explicitly. The CDK wires up the cross-stack exports for you, and your blast radius for a bad deploy stays contained to the stateless layer.
Use environments and context, not copy-pasted stacks
Don’t duplicate a stack per environment. Parametrize one stack class and instantiate it with different props:
new AppStack(app, 'AppDev', { instanceCount: 1, env: devEnv });
new AppStack(app, 'AppProd', { instanceCount: 6, env: prodEnv });
Pull environment-specific values from cdk.json context or SSM Parameter Store rather than hardcoding. One stack definition, many environments, no drift between a dev stack and a prod stack that were supposed to be identical but slowly diverged.
Test with assertions and snapshots
Because it’s real code, you can unit-test the synthesized template. Two flavors:
import { Template } from 'aws-cdk-lib/assertions';
test('bucket is encrypted', () => {
const stack = new MyStack(app, 'Test');
const template = Template.fromStack(stack);
template.hasResourceProperties('AWS::S3::Bucket', {
BucketEncryption: Match.objectLike({ /* ... */ }),
});
});
Fine-grained assertions check specific properties — great for enforcing “every bucket is encrypted” as a test that fails the PR. Snapshot tests catch unintended changes by diffing the synthesized template against a saved baseline; when the snapshot changes unexpectedly, your reviewer sees exactly what infra is about to change. Run both in CI.
Always review the diff before deploy
The CDK synthesizes to CloudFormation, and cdk diff shows you precisely what will change — including, critically, any resource replacements that destroy and recreate. A change that swaps a logical ID or alters an immutable property can silently replace your database.
cdk diff
Make reading cdk diff a non-negotiable step in your deploy pipeline, and treat any “X resources to be destroyed” line as a stop-and-review event. This is the CDK equivalent of terraform plan, and the same discipline applies.
Where AI helps
The CDK’s surface area is huge, and AI is genuinely strong here because TypeScript and Python CDK have enormous training representation.
- Scaffolding a stack from a description: “a CDK TypeScript stack with a Fargate service behind an ALB, an RDS Postgres instance, in separate stateful/stateless stacks.” A strong first draft in seconds.
- Writing the custom L3 construct that wraps your standards — describe the defaults you want baked in.
- Generating assertion tests for a stack, which are tedious to write by hand.
The model can reference renamed or deprecated CDK APIs (the library moves fast), so always cdk synth and cdk diff to verify, and pin it to aws-cdk-lib v2. We keep a set of IaC prompts for CDK scaffolding and testing.
The bottom line
The CDK rewards a little upfront discipline enormously. Use the right construct level, wrap your org standards into custom constructs, split stateful from stateless, parametrize across environments, test the synthesized template, and read every cdk diff. Do that and the CDK gives you genuinely maintainable infrastructure that new engineers can read and extend. Skip it, and you get a pile of bespoke L1 resources nobody wants to touch — real code’s downside, with none of its upside.
AI-generated CDK code is assistive, not authoritative. Always run cdk synth and cdk diff, and review resource replacements before deploying to production.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.