AWS CDK skill for Codex
Infrastructure code that never replaces your database. Codex structures CDK apps with stateful and stateless stacks apart, explicit environments and typed per-stage config. It treats logical ID changes as a data-loss risk, reads every cdk diff for replacements and IAM widening before a deploy, and writes assertion tests plus cdk-nag checks for the properties that matter instead of snapshot tests nobody reads.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- aws-cdk Installs to ~/.agents/skills/aws-cdk/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install aws-cdk The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project aws-cdk Installs to .agents/skills/aws-cdk/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/aws-cdk.zipunzip -o aws-cdk.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) aws-cdk PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $aws-cdk, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds and maintains AWS CDK v2 apps in TypeScript or Python - stack and construct design, environment-aware config, cdk synth/diff/deploy, bootstrap, cdk-nag checks and assertion tests with Template.fromStack - without causing accidental resource replacement. Use when writing or refactoring CDK stacks, adding an L2/L3 construct, reviewing a cdk diff, fixing a failed deploy or bootstrap error, splitting stacks, or adding unit tests for infrastructure code.
Use it when
- Writing or refactoring AWS CDK stacks and constructs in TypeScript or Python
- Reviewing a cdk diff before a production deploy
- Adding unit tests or cdk-nag policy checks to infrastructure code
- Debugging bootstrap, asset bundling or cross-stack export errors
Use something else when
- The project is a SAM template for a serverless app: use aws-sam
- The infrastructure is written in Terraform modules: use terraform-modules
How to check the result
Tests and cdk synth pass offline, and cdk diff shows no replacement of stateful resources and no unexpected IAM or security group changes.
The mistake it prevents
Renaming or moving a construct changes its logical ID, so CloudFormation deletes and recreates the resource, including databases and buckets.
Done well means: cdk synth is deterministic and offline, every deploy is preceded by a
cdk diff a human could read, stateful resources can never be replaced or deleted by a
refactor, and the infrastructure has tests that fail when a security property regresses.
Before changing anything
- Read
AGENTS.md,cdk.json,package.json/pyproject.tomland the lockfile. Note theappcommand incdk.json, theaws-cdk-libversion and the CLI version (npx cdk --version). Since 2025 the CLI is versioned separately (2.1000+) fromaws-cdk-lib; a CLI older than the library fails with a cloud assembly schema error. - Find the entry point (
bin/*.tsorapp.py) and list stacks:npx cdk ls. - Check whether
cdk.context.jsonis committed. It should be: it pins lookups (Vpc.fromLookup, AMIs, AZs) so synth is reproducible and works without network. - Run
npx cdk synth --quietbefore editing so you know the baseline is green.
Synth needs no AWS credentials unless a lookup is missing from context. diff, deploy
and bootstrap need network and credentials; in a Codex sandbox with network off, stop
after synth and tests and hand the deploy step to the user.
Project layout default
bin/app.ts # wires stacks to environments, nothing else
lib/stacks/network-stack.ts
lib/stacks/data-stack.ts # stateful: databases, buckets, queues
lib/stacks/service-stack.ts
lib/constructs/ # reusable L3 constructs, no Stack subclasses
test/ # assertion tests per stack
- Separate stateful from stateless stacks. Stateless stacks can be destroyed and
recreated; stateful ones get
terminationProtection: true. - Pass resources between stacks as constructor props (typed references), not by
Fn.importValuestrings. - Put config in a typed object per environment, not in scattered
this.node.tryGetContextcalls.
// bin/app.ts
const app = new cdk.App();
const envs = {
staging: { account: '111111111111', region: 'eu-west-1', minTasks: 1 },
prod: { account: '222222222222', region: 'eu-west-1', minTasks: 3 },
} as const;
const stage = (app.node.tryGetContext('stage') ?? 'staging') as keyof typeof envs;
const cfg = envs[stage];
const data = new DataStack(app, `Data-${stage}`, { env: cfg, terminationProtection: stage === 'prod' });
new ServiceStack(app, `Service-${stage}`, { env: cfg, table: data.table, minTasks: cfg.minTasks });
Always set env explicitly. Environment-agnostic stacks cannot do lookups and silently
get two AZs.
Construct level decision
| Need | Use |
|---|---|
| Standard resource with sane defaults | L2 (s3.Bucket, lambda.Function, ecs.FargateService) |
| A property the L2 does not expose | L2 plus escape hatch: (bucket.node.defaultChild as s3.CfnBucket).addPropertyOverride(...) |
| Brand-new service with no L2 yet | L1 Cfn* construct, wrapped in your own L3 |
| A pattern repeated across stacks | Your own L3 construct extending Construct |
| AWS Solutions / patterns libraries | Only after reading what they create; they hide IAM and networking choices |
Prefer grant* methods (table.grantReadWriteData(fn), bucket.grantRead(role)) over
hand-written PolicyStatements: they scope to the resource ARN and include KMS key grants.
Logical IDs and replacement - the main trap
A resource’s CloudFormation logical ID is derived from its construct path. Renaming a construct ID, moving it into another construct, or moving it to another stack produces a new logical ID, and CloudFormation deletes the old resource and creates a new one. For a database or bucket that is data loss.
Hard rules:
- Every stateful resource gets
removalPolicy: RemovalPolicy.RETAIN(orSNAPSHOTfor RDS) in production. TheDESTROY+autoDeleteObjectscombination is for ephemeral dev stacks only. - Before renaming or moving a construct, run
cdk diffand look for[-]/[+]pairs orrequires replacementon stateful types. If it appears, keep the old ID or pin it:
(table.node.defaultChild as dynamodb.CfnTable).overrideLogicalId('OrdersTable8A3F2C1B');
- Do not hardcode physical names (
bucketName,tableName,functionName) unless an external system requires them: a named resource cannot be replaced without a manual rename, so property changes that need replacement fail. - Cross-stack references create CloudFormation exports. An export in use cannot be
removed or changed; deleting the consumer reference first, deploying, then removing the
producer output is a two-deploy dance. Use
this.exportValue(...)in the producer to keep an export alive during that transition.
Deploy workflow
-
npm test(orpytest) passes -
npx cdk synth --quietpasses with no warnings you have not read -
npx cdk diff <Stack>reviewed: IAM changes, security group changes, replacements - User confirmed the diff for any non-dev environment
-
npx cdk deploy <Stack> --require-approval broadening - Post-deploy check (stack status, smoke test)
npx cdk bootstrap aws://111111111111/eu-west-1 # once per account/region
npx cdk diff Service-prod --context stage=prod
npx cdk deploy Service-prod --context stage=prod --require-approval broadening
npx cdk deploy --all --concurrency 3 --context stage=staging
Never deploy to production, run cdk destroy, or pass --require-approval never against
a shared account without the user’s explicit go-ahead after they have seen the diff.
cdk watch and --hotswap bypass CloudFormation and leave drift: dev stacks only.
Testing
Use fine-grained assertions for properties that matter (encryption, public access, IAM scope). Snapshot tests catch everything and therefore get blindly updated; keep at most one per stack.
import { App } from 'aws-cdk-lib';
import { Template, Match } from 'aws-cdk-lib/assertions';
import { DataStack } from '../lib/stacks/data-stack';
test('bucket blocks public access and enforces TLS', () => {
const app = new App();
const stack = new DataStack(app, 'T', { env: { account: '1', region: 'eu-west-1' } });
const t = Template.fromStack(stack);
t.hasResourceProperties('AWS::S3::Bucket', {
PublicAccessBlockConfiguration: { BlockPublicAcls: true, RestrictPublicBuckets: true },
});
t.hasResourceProperties('AWS::S3::BucketPolicy', {
PolicyDocument: { Statement: Match.arrayWith([
Match.objectLike({ Effect: 'Deny', Condition: { Bool: { 'aws:SecureTransport': 'false' } } }),
]) },
});
t.hasResource('AWS::DynamoDB::Table', { DeletionPolicy: 'Retain' });
});
Python equivalent: from aws_cdk.assertions import Template, Match,
Template.from_stack(stack).has_resource_properties(...).
Add policy checks with cdk-nag as an Aspect so violations fail synth:
import { Aspects } from 'aws-cdk-lib';
import { AwsSolutionsChecks, NagSuppressions } from 'cdk-nag';
Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true }));
NagSuppressions.addStackSuppressions(stack, [
{ id: 'AwsSolutions-IAM4', reason: 'AWSLambdaBasicExecutionRole is acceptable for logs' },
]);
Every suppression needs a reason a reviewer would accept. Installing cdk-nag needs
network; ask before adding dependencies.
Common failures
| Symptom | Cause | Fix |
|---|---|---|
SSM parameter /cdk-bootstrap/hnb659fds/version not found | Account/region not bootstrapped | cdk bootstrap aws://ACCOUNT/REGION (user confirms) |
Cloud assembly schema version mismatch | CLI older than aws-cdk-lib | Upgrade the aws-cdk CLI package |
Export ... cannot be deleted as it is in use | Removed a cross-stack reference in one step | Keep the export with exportValue, deploy, then remove |
... already exists on create | Hardcoded physical name or retained old resource | Drop the explicit name or import the resource with cdk import |
Stack stuck UPDATE_ROLLBACK_FAILED | Resource could not roll back | aws cloudformation continue-update-rollback --stack-name X (optionally --resources-to-skip), only with user approval |
| Lookup works locally, fails in CI | cdk.context.json not committed | Commit it; CI should not need lookup permissions |
| Asset bundling fails | No Docker and no local esbuild for NodejsFunction | Add esbuild as a dev dependency |
Gotchas
Vpcdefault creates one NAT gateway per AZ. SetnatGatewaysdeliberately; for dev consider 0 with VPC endpoints.lambda.Functioncreates a log group with infinite retention unless you pass alogGroupwithretention.- Feature flags in
cdk.jsonchange synthesized output. Do not delete them during upgrades; new projects get the current recommended set fromcdk init. - Tokens are unresolved at synth time:
bucket.bucketNameis${Token[...]}in TypeScript, so never branch on its string value. cdk diffagainst a stack deployed outside CDK, or edited in the console, will not show the drift. Run CloudFormation drift detection when in doubt.
Output
When reporting a change, give: stacks affected, the cdk diff summary (resources added,
modified, replaced, IAM/security group changes), tests added or updated, and the exact
deploy command left for the user to run.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "AWS CDK"
short_description: "Infrastructure code that never replaces your database"
default_prompt: "$aws-cdk Split this CDK stack so the database lives in its own stack without recreating it" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.