4 min read
Five CDK Mistakes I Made on My First Real Project
Stateful resources destroyed on deploy, context caching bugs, and the IAM grant pattern I should have used from day one.
The CDK is pleasant enough that you can get quite far without understanding what it's doing underneath. Then you learn, all at once, at the worst possible time. Here are five things that cost me real hours.
1. Leaving removalPolicy at the default on a data store
By default, CDK sets RemovalPolicy.RETAIN on some stateful resources and DESTROY on others, and the specific behavior varies by construct. I assumed one rule applied everywhere. It doesn't.
Be explicit on anything holding data you care about:
const table = new dynamodb.Table(this, 'AppTable', {
partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'sk', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
removalPolicy: cdk.RemovalPolicy.RETAIN,
pointInTimeRecoverySpecification: { pointInTimeRecoveryEnabled: true },
});
RETAIN means a cdk destroy leaves the table sitting in your account, orphaned from the stack. That's mildly annoying. The alternative is discovering that DESTROY did exactly what it says.
Run cdk diff before every deploy and actually read the "Resources" section. Any line starting with a destroy marker on a table or bucket deserves a full stop.
2. Changing a construct ID and triggering a replace
CDK derives CloudFormation logical IDs from the construct path. Rename new dynamodb.Table(this, 'AppTable', ...) to 'MainTable' and CloudFormation doesn't see a rename — it sees one resource to delete and a different one to create.
For a Lambda, that's a non-event. For a table, it's your data.
Rule I now follow: construct IDs on stateful resources are permanent. Change the variable name in TypeScript all you like; leave the string alone.
3. Writing IAM policies by hand
My first version:
fn.addToRolePolicy(new iam.PolicyStatement({
actions: ['dynamodb:GetItem', 'dynamodb:PutItem', 'dynamodb:Query'],
resources: [table.tableArn],
}));
This works until you add a global secondary index and start querying it. GSI ARNs are children of the table ARN, table.tableArn doesn't cover them, and you get an access-denied error that reads like a bug in your query code.
The grant methods handle this:
table.grantReadWriteData(fn);
They scope permissions correctly, include index ARNs, and add the KMS grants if the table is encrypted with a customer-managed key. They're also self-documenting in a way a raw policy statement isn't.
4. Not committing cdk.context.json
Context lookups — Vpc.fromLookup, HostedZone.fromLookup — hit live AWS APIs during synth and cache results in cdk.context.json.
I gitignored it, reasoning that generated files don't belong in version control. Then CI, running with a different role, did a fresh lookup, failed, and fell back to a dummy value. The synth succeeded. The deploy succeeded. The DNS record pointed at nothing.
Commit the file. When you genuinely need to refresh it, cdk context --clear is the deliberate way to do that.
5. Putting everything in one stack
One stack for the database, the API, the Lambdas, and the frontend distribution felt tidy. It stops feeling tidy when a botched change to a Lambda rolls back and takes the whole deploy with it — including changes to resources that were fine.
Splitting along lifecycle boundaries works better:
const data = new DataStack(app, 'DataStack', { env });
const api = new ApiStack(app, 'ApiStack', { env, table: data.table });
Passing the construct directly creates an implicit dependency, so CDK deploys them in the right order and wires up the cross-stack export for you. Now a bad API change can't threaten the data layer, and stacks that rarely change stop being redeployed constantly.
The pattern underneath
Four of these five come from the same root cause: treating the CDK as if it were the whole system, when it's a program that emits CloudFormation templates. Logical IDs, replacement behavior, rollback scope, cross-stack references — those are all CloudFormation concepts, and they'll surface no matter how nice the TypeScript looks.
cdk synth prints the template it generates. It's worth reading a few times early on.