AWS CDK has quietly become the default way experienced AWS teams define infrastructure, and the numbers back it up: “aws cdk” pulls roughly 4,400 monthly US searches, more than most individual AWS service tutorials, because it sits at the intersection of two audiences that rarely overlap â developers who want infrastructure defined in a real programming language, and platform teams tired of maintaining sprawling YAML. The project’s GitHub repository had shipped 687 releases by June 16, 2026, with 2.260.0 marked as the latest tag, and AWS’s own CDK v2 Developer Guide confirms v2 remains the current, actively developed line as of August 2026 â v1 support ended back on June 1, 2023, so there’s no ambiguity about which line to build on. Node.js 22.x is now the required runtime baseline, and the construct library has matured enough that most common AWS architectures ship as ready-made L2 or L3 patterns instead of hand-wired CloudFormation.
This tutorial walks through setting up AWS CDK from a clean machine to a deployed, tested, multi-stack application: installation, bootstrapping, writing your first stack, construct levels, context configuration, testing with the assertions module, and wiring CDK into a CI/CD pipeline. Budget about 80 minutes end to end, most of which is reading and typing rather than waiting on AWS.
Don't miss new tech stories on Google
Add Tech Insider once in the Google app and our stories appear in your news suggestions.
What is AWS CDK and why teams are adopting it in 2026
The AWS Cloud Development Kit (CDK) is an open-source framework for defining cloud infrastructure using real programming languages â TypeScript, Python, Java, C#, and Go all have first-class support â instead of declarative YAML or JSON templates. Under the hood, CDK code compiles (“synthesizes”) down to standard AWS CloudFormation templates, which means you get CloudFormation’s reliability and rollback behavior with a programming language’s abstractions: loops, conditionals, functions, and reusable classes.
The pitch that’s driving 2026 adoption specifically is the construct library’s maturity. Where a raw CloudFormation template might need 80 lines of YAML to stand up an S3 bucket with the right encryption, lifecycle policy, and bucket policy, a CDK L2 construct does it in three or four lines with secure defaults already applied. That coverage keeps expanding, too â AWS shipped a full L2 construct for Amazon Data Firehose on February 17, 2025, letting teams define streaming ingestion infrastructure with the same secure-defaults ergonomics instead of dropping to raw L1 resources. Multiply that across a real application’s dozens of resources and the difference in code volume â and in how many mistakes are possible â is substantial.
There’s also a practical reason CDK keeps showing up in job postings and internal platform-team roadmaps this year: it lets application engineers who already know TypeScript or Python contribute infrastructure changes directly, in code review, using the same tools and linters as the rest of the codebase, rather than routing every change through a separate team that owns a YAML repository. AWS leaned further into that direction by taking the CDK Toolkit Library generally available on May 30, 2025 â a programmatic Node.js library, available in all regions, that lets tooling and platform teams drive synth and deploy operations directly from application code instead of shelling out to the CLI. That doesn’t eliminate the need for infrastructure expertise, but it does lower the barrier for a backend engineer to safely add a queue or a scheduled Lambda without waiting on a ticket.
AWS CDK itself is free and open source. You only pay for the AWS resources your stacks actually create â the framework and CLI carry no licensing cost, and the bootstrap resources it needs (a small S3 bucket, an ECR repository) cost a few cents a month at most for a typical account.
Prerequisites and versions you’ll need
- Node.js 22.x or later â required even if you’re writing CDK in Python or Java, since the CDK Toolkit itself runs on Node.js under the hood via JSII
- AWS CLI v2, configured with credentials that have permission to create CloudFormation stacks, S3 buckets, IAM roles, and an ECR repository
- An AWS account with billing enabled â CDK needs somewhere to deploy to
- Your language of choice installed: TypeScript/JavaScript needs nothing extra beyond Node, Python needs 3.8+ and pip, Java needs a JDK and Maven, C# needs .NET, Go needs a recent Go toolchain
- Roughly 80 minutes â most steps are fast; the slowest part is the one-time bootstrap and your first stack deployment
# Install the CDK CLI globally via npm (works regardless of which
# language you'll write your stacks in)
npm install -g aws-cdk
# Confirm versions
node --version # should report v22.x or later
cdk --version # e.g. 2.1120.0 (build abcdef1)
aws --version # aws-cli/2.x
Step 1: Configure AWS credentials
CDK uses the same credential chain as the AWS CLI and SDKs â named profiles, environment variables, or an assumed role all work. Confirm you’re authenticated as the right identity before doing anything else.
aws configure
# AWS Access Key ID: AKIA...
# AWS Secret Access Key: ********
# Default region name: us-east-1
# Default output format: json
aws sts get-caller-identity
# Confirms the account ID and IAM ARN CDK will deploy as
Step 2: Initialize a new CDK project
cdk init scaffolds a working project in your chosen language, complete with a sample stack, test setup, and the cdk.json configuration file. Worth knowing going in: AWS decoupled CDK CLI versioning from the aws-cdk-lib construct library starting February 2025, with the CLI jumping to its own 2.1000.0+ numbering while the library kept its separate 2.174.0+ line â so don’t be alarmed if cdk --version and your package.json‘s aws-cdk-lib entry show very different numbers, that’s expected now. This tutorial uses TypeScript, the most common CDK language and the one with the deepest documentation coverage, but the same commands and concepts apply with --language python, --language java, --language csharp, or --language go.
mkdir ti-cdk-demo && cd ti-cdk-demo
cdk init app --language typescript
# Project structure created:
# bin/ti-cdk-demo.ts - app entry point
# lib/ti-cdk-demo-stack.ts - your stack definition
# test/ti-cdk-demo.test.ts - Jest test scaffold
# cdk.json - CDK configuration and context
# package.json, tsconfig.json
npm install
Step 3: Understand cdk.json and context
cdk.json tells the CDK CLI how to run your app and stores context values â configuration and feature flags that influence how constructs synthesize. Every new CDK version ships with a set of context feature flags in this file that opt your app into updated (and sometimes breaking) default behaviors, so it’s worth actually reading this file rather than treating it as boilerplate. AWS’s documentation history also added explicit guidance on August 29, 2025 for opting out of CDK CLI telemetry ahead of that collection feature’s rollout â worth checking if your organization has policies about anonymous usage data leaving developer machines.
{
"app": "npx ts-node --prefer-ts-exts bin/ti-cdk-demo.ts",
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/aws-iam:minimizePolicies": true,
"environment": "dev"
}
}
Context is also how CDK caches the results of environment lookups â asking AWS “what availability zones exist in this account/region” or “what’s the ID of this VPC” during synthesis, then caching the answer so repeat synths don’t hit the AWS API every time. If a lookup result seems stale after infrastructure changes outside CDK, clear it with cdk context --clear.
Step 4: Bootstrap your AWS environment
Before deploying anything, every AWS account/region combination you target needs to be “bootstrapped” once. This deploys a small CloudFormation stack (named CDKToolkit by default) containing the resources CDK needs to stage and deploy your actual application stacks.
cdk bootstrap aws://123456789012/us-east-1
# âģ Bootstrapping environment aws://123456789012/us-east-1...
# CDKToolkit: creating CloudFormation changeset...
# â
Environment aws://123456789012/us-east-1 bootstrapped.
The bootstrap stack creates three things worth knowing about: an S3 bucket that stores synthesized CloudFormation templates and file assets (Lambda code bundles, static files), an ECR repository for Docker image assets used by container-based resources â AWS expanded its documentation on building those container image assets with Docker on March 21, 2025, worth a read if your stacks package Lambda or ECS workloads as images â and a set of IAM roles â a deployment role, a lookup role for context queries, and asset-publishing roles that let CDK upload to that S3 bucket and ECR repo without your personal credentials needing broad permissions. If you ever see an error like “this CDK CLI version requires bootstrap stack version X but found Y,” it means your CLI has been updated since you last bootstrapped â bootstrap template versions v26 and v27, documented in AWS’s guide on March 27, 2025, are common targets on recently updated CLIs â re-run cdk bootstrap to bring the stack current.
Step 5: Write your first stack
Replace the generated stack in lib/ti-cdk-demo-stack.ts with something real â an S3 bucket, a Lambda function, and an API Gateway endpoint in front of it, a common serverless starting point.
// lib/ti-cdk-demo-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigw from 'aws-cdk-lib/aws-apigateway';
export class TiCdkDemoStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const uploadBucket = new s3.Bucket(this, 'UploadBucket', {
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
const handler = new lambda.Function(this, 'ApiHandler', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: lambda.Code.fromInline(
'exports.handler = async () => ({ statusCode: 200, body: "OK" });'
),
environment: {
BUCKET_NAME: uploadBucket.bucketName,
},
});
uploadBucket.grantReadWrite(handler);
const api = new apigw.LambdaRestApi(this, 'DemoApi', {
handler,
proxy: true,
});
new cdk.CfnOutput(this, 'ApiUrl', { value: api.url });
new cdk.CfnOutput(this, 'BucketName', { value: uploadBucket.bucketName });
}
}
Notice what’s absent: no bucket policy JSON, no IAM policy documents written by hand, no manually wiring the Lambda’s execution role to read/write the bucket. grantReadWrite() generates a least-privilege IAM policy scoped to that specific bucket automatically â this is the L2 construct abstraction doing its job.
Step 6: Understand construct levels â L1, L2, and L3
CDK’s construct library is organized into three levels, and knowing when to reach for each one matters more than memorizing every API.
| Level | What it is | Example | When to use |
|---|---|---|---|
| L1 (CFN Resources) | Direct 1:1 mapping to a CloudFormation resource type, prefixed Cfn | s3.CfnBucket | You need a property CloudFormation supports but the L2 construct hasn’t exposed yet |
| L2 (Curated constructs) | Opinionated wrapper with secure defaults and helper methods | s3.Bucket, lambda.Function | The default choice for the large majority of resources you’ll create |
| L3 (Patterns) | Multi-resource architecture patterns bundled as one construct | aws-cdk-lib/aws-ecs-patterns load-balanced Fargate service | You’re implementing a well-known architecture and don’t want to wire five resources by hand |
You can freely mix levels within one stack â start with an L3 pattern for the bulk of an architecture, then drop to the L1 Cfn* class for one specific resource where you need a property the L2 wrapper hasn’t caught up to yet. That escape hatch is one of CDK’s more underrated features compared to tools that lock you into whatever abstraction they’ve chosen. It’s also why teams rarely get stuck waiting on AWS to add L2 support for a brand-new service feature â the L1 layer is generated directly from the CloudFormation resource specification, so it’s available the same day CloudFormation itself supports a property, even before a human-friendly L2 wrapper catches up.
Step 7: Synthesize, diff, and deploy
Three commands cover the entire CDK deployment workflow, and understanding what each one actually does prevents a lot of “why didn’t my change apply” confusion.
# Synthesize: compile your CDK code into a CloudFormation template
# without touching AWS. Useful for inspecting exactly what will deploy.
cdk synth
# Diff: compare your local code against what's currently deployed
cdk diff
# Deploy: synthesize, then create/update the CloudFormation stack
cdk deploy
# Deploy a specific stack in a multi-stack app
cdk deploy TiCdkDemoStack --require-approval never
Expected cdk diff output on a first run against an empty environment looks like this:
Stack TiCdkDemoStack
Resources
[+] AWS::S3::Bucket UploadBucket UploadBucket4A2F1E86
[+] AWS::Lambda::Function ApiHandler ApiHandler7D4C3B21
[+] AWS::ApiGateway::RestApi DemoApi DemoApiA1B2C3D4
[+] AWS::IAM::Policy ApiHandler/ServiceRole/DefaultPolicy ...
Outputs
[+] Output ApiUrl: {"Value": {"Fn::Join": ...}}
[+] Output BucketName: {"Value": {"Ref": "UploadBucket4A2F1E86"}}
Running cdk diff in a pull request check, before cdk deploy ever runs against production, is one of the highest-value habits you can build into a CDK workflow â it turns “what does this PR actually change in AWS” from a guess into a printed, reviewable list.
Step 8: Structure a multi-stack application
Real applications rarely fit in one stack. Splitting by concern â network, data, application, pipeline â keeps blast radius contained and makes each stack’s CloudFormation deployment faster and easier to reason about.
ti-cdk-demo/
âââ bin/
â âââ ti-cdk-demo.ts # App entry point, instantiates all stacks
âââ lib/
â âââ network-stack.ts # VPC, subnets, security groups
â âââ data-stack.ts # RDS/DynamoDB, S3 buckets
â âââ app-stack.ts # Lambda, API Gateway, ECS services
â âââ pipeline-stack.ts # CDK Pipelines CI/CD definition
âââ test/
â âââ *.test.ts # Jest assertion and snapshot tests
âââ cdk.json
âââ package.json
// bin/ti-cdk-demo.ts
import * as cdk from 'aws-cdk-lib';
import { NetworkStack } from '../lib/network-stack';
import { DataStack } from '../lib/data-stack';
import { AppStack } from '../lib/app-stack';
const app = new cdk.App();
const env = { account: process.env.CDK_DEFAULT_ACCOUNT, region: 'us-east-1' };
const network = new NetworkStack(app, 'TiNetworkStack', { env });
const data = new DataStack(app, 'TiDataStack', { env, vpc: network.vpc });
new AppStack(app, 'TiAppStack', { env, vpc: network.vpc, table: data.table });
Passing resources between stacks (like network.vpc above) creates a CloudFormation cross-stack reference automatically â CDK handles the export/import wiring so you don’t have to write Fn::ImportValue by hand.
Step 9: Test your infrastructure code
CDK’s assertions module lets you write real unit tests against synthesized CloudFormation output â checking that a bucket has encryption enabled, that a Lambda has the right timeout, or that a specific IAM policy exists, all without deploying anything to AWS.
// test/ti-cdk-demo.test.ts
import { App } from 'aws-cdk-lib';
import { Template, Match } from 'aws-cdk-lib/assertions';
import { TiCdkDemoStack } from '../lib/ti-cdk-demo-stack';
test('bucket is encrypted and blocks public access', () => {
const app = new App();
const stack = new TiCdkDemoStack(app, 'TestStack');
const template = Template.fromStack(stack);
template.hasResourceProperties('AWS::S3::Bucket', {
BucketEncryption: Match.objectLike({
ServerSideEncryptionConfiguration: Match.anyValue(),
}),
});
template.resourceCountIs('AWS::Lambda::Function', 1);
});
test('matches the stored snapshot', () => {
const app = new App();
const stack = new TiCdkDemoStack(app, 'TestStack');
const template = Template.fromStack(stack);
expect(template.toJSON()).toMatchSnapshot();
});
npm test
# PASS test/ti-cdk-demo.test.ts
# â bucket is encrypted and blocks public access (48 ms)
# â matches the stored snapshot (12 ms)
Snapshot tests catch unintended infrastructure drift â if a dependency bump silently changes a construct’s generated CloudFormation, the snapshot diff surfaces it in code review instead of in a surprise production change.
Step 10: Wire CDK into a CI/CD pipeline
Two common patterns cover most teams: running the CDK CLI directly from an external CI system like GitHub Actions, or defining the pipeline itself as CDK code using CDK Pipelines (an L3 construct built on CodePipeline and CodeBuild).
# .github/workflows/deploy.yml
name: Deploy CDK Stack
on:
push:
branches: [main]
pull_request:
jobs:
cdk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: npm ci
- run: npm test
- run: npx cdk synth
- name: Configure AWS credentials
if: github.ref == 'refs/heads/main'
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsDeployRole
aws-region: us-east-1
- name: Deploy
if: github.ref == 'refs/heads/main'
run: npx cdk deploy --all --require-approval never
Run cdk synth and tests on every pull request regardless of branch, and gate the actual cdk deploy step behind a branch check so infrastructure changes only apply from reviewed, merged code â the same discipline you’d apply to application code deployments.
How AWS CDK compares to Terraform, Pulumi, and raw CloudFormation
| Tool | Language | State management | Cloud scope | Best fit |
|---|---|---|---|---|
| AWS CDK | TypeScript, Python, Java, C#, Go | Delegated to CloudFormation | AWS-only (community layers exist for others) | AWS-first teams wanting real-language abstractions with CloudFormation’s reliability |
| Terraform | HCL (or TypeScript/Python via CDK for Terraform) | Self-managed state file/backend | Multi-cloud, largest provider ecosystem | Multi-cloud shops, teams standardized on HCL already |
| Pulumi | TypeScript, Python, Go, C#, Java | Pulumi-managed state (or self-hosted) | Multi-cloud | Teams wanting real-language IaC without being AWS-only |
| CloudFormation | YAML/JSON | Native, no separate state file | AWS-only | Simple stacks, teams that prefer declarative templates over code |
The practical decision usually comes down to two questions: are you AWS-only or multi-cloud, and does your team want infrastructure defined in a general-purpose programming language or a declarative DSL. CDK answers “AWS-only, real language” specifically â if either half of that changes, Terraform or Pulumi become the stronger fit.
5 common CDK setup pitfalls
- Deploying without bootstrapping first. A brand-new account/region combination has no
CDKToolkitstack yet âcdk deploywill fail immediately with a clear error, but it’s the single most common “why won’t this work” moment for first-time users. - Letting the bootstrap stack drift out of date. Updating the CDK CLI without re-running
cdk bootstrapeventually produces a version mismatch error. Re-bootstrap after any CLI upgrade that touches a shared environment. - Treating context values as permanent. Cached lookup values (VPC IDs, AZ lists) can go stale after manual changes outside CDK. Run
cdk context --clearwhen the deployed reality and your cached context disagree. - Circular resource dependencies. Two resources referencing each other (a role that needs a policy that needs to reference the role) synthesize fine but fail at CloudFormation deploy time. Restructure the reference chain rather than trying to force it through.
- Using
RemovalPolicy.DESTROYandautoDeleteObjectsin production. Both are convenient for demo/dev stacks (as used in Step 5) but will silently delete data-bearing resources on stack teardown â remove them before anything touches real data.
Troubleshooting: 8 common CDK errors and fixes
| Error / symptom | Likely cause | Fix |
|---|---|---|
| “This stack uses assets, so the toolkit stack must be deployed” | Environment was never bootstrapped | Run cdk bootstrap aws://ACCOUNT/REGION |
| “This CDK CLI is not compatible with the CDK library used by your application” | CLI and aws-cdk-lib package versions have drifted apart | Update both to matching major/minor versions; check package.json against cdk --version |
| Bootstrap stack version mismatch | CLI upgraded since the account was last bootstrapped | Re-run cdk bootstrap with the current CLI |
| Asset publishing failure (“Unable to upload asset”) | Deployment role lacks S3/ECR permissions, or the bootstrap bucket was deleted manually | Verify the bootstrap stack still exists (CDKToolkit in CloudFormation); re-bootstrap if missing |
AccessDenied during cdk deploy | The IAM identity running CDK lacks CloudFormation or resource-creation permissions | Check the specific denied action in the CloudFormation Events tab and grant it |
| Circular dependency between resources | Two constructs reference each other, forming a cycle CloudFormation can’t resolve | Break the cycle â often by using a separate low-level reference instead of a direct construct property |
| Stack stuck in UPDATE_ROLLBACK_FAILED | A failed deployment left CloudFormation in an inconsistent state | Use aws cloudformation continue-update-rollback, or resolve the specific stuck resource manually in the console |
| Context lookup returns outdated values | Cached context from a prior synth no longer matches deployed AWS state | Run cdk context --clear then re-synth |
Advanced tips for running CDK in production
- Pin your CDK CLI and library versions together. Add
aws-cdk-libandaws-cdk(the CLI) as exact-pinned dependencies inpackage.jsonrather than ranges, so a teammate’snpm installdoesn’t silently pull a newer minor version with different default behavior. - Use
Stageconstructs for multi-environment deployments. Wrapping a group of stacks in aStagelets CDK Pipelines promote the exact same synthesized template through dev, staging, and production â eliminating “it worked in staging” drift caused by re-synthesizing per environment. - Run
cdk diffas a required PR check. Treat unreviewed infrastructure diffs the same as unreviewed code diffs â a merge should never be the first time a human sees what’s about to deploy. - Tag stacks and resources consistently. Apply
cdk.Tags.of(this).add()at the app or stack level so cost allocation and ownership tracking don’t depend on every construct author remembering to tag manually. - Keep custom L1-level overrides documented inline. When you drop to a
Cfn*escape hatch because an L2 construct doesn’t expose a property yet, leave a one-line comment explaining why â L2 coverage improves over time and someone should eventually remove the workaround.
Complete working project: a serverless API with CDK Pipelines
Putting the pieces together, here’s a pipeline stack that deploys the app stack from Step 5 automatically whenever the main branch changes, using CDK Pipelines instead of an external CI system.
// lib/pipeline-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as pipelines from 'aws-cdk-lib/pipelines';
import { TiCdkDemoStack } from './ti-cdk-demo-stack';
class AppStage extends cdk.Stage {
constructor(scope: Construct, id: string, props?: cdk.StageProps) {
super(scope, id, props);
new TiCdkDemoStack(this, 'TiCdkDemoStack');
}
}
export class PipelineStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const pipeline = new pipelines.CodePipeline(this, 'Pipeline', {
pipelineName: 'TiCdkDemoPipeline',
synth: new pipelines.ShellStep('Synth', {
input: pipelines.CodePipelineSource.gitHub('your-org/ti-cdk-demo', 'main'),
commands: ['npm ci', 'npm test', 'npx cdk synth'],
}),
});
pipeline.addStage(new AppStage(this, 'Production', {
env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: 'us-east-1' },
}));
}
}
cdk deploy PipelineStack
# One-time deploy creates a self-mutating CodePipeline.
# From here on, pushes to main automatically trigger synth, test, and deploy.
That’s a “self-mutating” pipeline â because the pipeline definition itself is CDK code deployed through the pipeline, changes to the pipeline’s own structure (adding a new stage, changing the source repo) roll out the same way application changes do, without a separate manual pipeline-update step.
Enforcing organization-wide rules with CDK Aspects
Once more than one team is writing CDK stacks, you’ll eventually want to enforce rules across all of them without relying on every author remembering â things like “every S3 bucket must be encrypted” or “no security group may allow unrestricted inbound SSH.” CDK’s Aspects API visits every construct in a stack tree at synthesis time and lets you inspect or reject resources that violate a policy, before anything ever reaches CloudFormation.
// lib/aspects/require-encryption.ts
import { IAspect, Annotations } from 'aws-cdk-lib';
import { IConstruct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
export class RequireBucketEncryption implements IAspect {
visit(node: IConstruct): void {
if (node instanceof s3.CfnBucket) {
if (!node.bucketEncryption) {
Annotations.of(node).addError(
'S3 buckets must have encryption enabled'
);
}
}
}
}
// bin/ti-cdk-demo.ts
import { Aspects } from 'aws-cdk-lib';
import { RequireBucketEncryption } from '../lib/aspects/require-encryption';
Aspects.of(app).add(new RequireBucketEncryption());
// cdk synth now fails with a clear error if any stack in the app
// creates an unencrypted bucket, instead of that mistake reaching production
For teams that don’t want to author these checks from scratch, the community-maintained cdk-nag package ships pre-built Aspect rule packs mapped to standards like AWS Well-Architected and CIS Benchmarks, and it’s become close to a default addition for production CDK apps that need an audit trail showing security rules were checked at build time, not discovered after deployment.
Migrating existing infrastructure to CDK
Very few teams start a CDK project against a completely empty AWS account. More often, you’re introducing CDK into an account that already has resources created manually, through CloudFormation, or through Terraform â and the migration path differs depending on which. AWS added dedicated documentation on September 8, 2025 covering how to preserve already-deployed resources when refactoring stacks â a common gap in earlier guidance, since restructuring construct code can otherwise trigger CloudFormation to replace resources it thinks changed identity rather than actually update them.
From existing CloudFormation stacks: CDK can adopt resources from a stack you didn’t originally create with CDK using cdk import, which maps existing physical resources (an S3 bucket, a DynamoDB table) into new CDK construct definitions without recreating them. This is the lowest-risk migration path since it never touches the actual resource, only how it’s tracked going forward.
From manually created resources: Most L2 constructs expose a fromXxxAttributes() or fromXxxArn() static method that wraps an existing resource as a read-only reference inside your CDK app, letting new resources reference it (for example, a Lambda that needs to read from a bucket someone created by hand years ago) without CDK taking ownership of that resource’s lifecycle.
From Terraform: There’s no automated conversion path â Terraform state and CloudFormation stacks are fundamentally different tracking mechanisms. The practical approach most teams take is running both in parallel during a transition period, using CDK for all new resources and gradually recreating Terraform-managed resources under CDK ownership only when they’d need significant changes anyway, rather than attempting a big-bang migration of a large existing Terraform codebase.
Whichever path applies, treat the first CDK stack in an existing account as a chance to validate your bootstrap, IAM permissions, and CI/CD wiring on something low-stakes before pointing CDK at anything that would be painful to get wrong.
Building your own reusable constructs
Once a pattern repeats across two or three stacks in your organization â a standard logging setup, a specific API Gateway + Lambda + DynamoDB shape your team always uses â it’s worth packaging it as a custom construct rather than copy-pasting the same 40 lines into every new stack file. A construct is just a class extending Construct that composes lower-level resources behind a clean, typed interface.
// lib/constructs/logged-lambda.ts
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as logs from 'aws-cdk-lib/aws-logs';
import { Duration } from 'aws-cdk-lib';
export interface LoggedLambdaProps {
handler: string;
code: lambda.Code;
timeoutSeconds?: number;
}
export class LoggedLambda extends Construct {
public readonly function: lambda.Function;
constructor(scope: Construct, id: string, props: LoggedLambdaProps) {
super(scope, id);
this.function = new lambda.Function(this, 'Function', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: props.handler,
code: props.code,
timeout: Duration.seconds(props.timeoutSeconds ?? 10),
logRetention: logs.RetentionDays.TWO_WEEKS,
});
}
}
Every team member who uses LoggedLambda instead of raw lambda.Function automatically gets the organization’s standard log retention policy applied â one place to change it, instead of auditing every stack file when the retention policy changes. This is the same pattern the official construct library uses internally at a larger scale: L2 constructs are, structurally, exactly this kind of composition over L1 resources.
For constructs that need to be shared across multiple repositories rather than just multiple stacks in one app, publish them as a private npm package (or the equivalent for your language â a PyPI package, a Maven artifact). AWS also maintains Construct Hub, a public registry of open-source CDK constructs across all supported languages, worth checking before writing a construct from scratch â there’s a reasonable chance someone has already built and published a well-tested version of whatever pattern you need.
Cost visibility for CDK-managed infrastructure
CDK makes it fast to create infrastructure, which cuts both ways â it’s also fast to accidentally create more of it than intended, especially with L3 patterns that provision several resources behind one construct call. A few habits keep cost visibility from lagging behind deployment speed.
Tag everything at the app level rather than per-resource. Applying tags once, high up in the construct tree, means every resource created underneath inherits them automatically â no relying on individual engineers to remember tagging on each new bucket or function.
// bin/ti-cdk-demo.ts
import { Tags } from 'aws-cdk-lib';
Tags.of(app).add('team', 'platform');
Tags.of(app).add('cost-center', 'eng-infra');
Tags.of(app).add('managed-by', 'cdk');
Before merging a stack that introduces new always-on infrastructure (an RDS instance, a NAT gateway, a provisioned-capacity DynamoDB table), run cdk diff against the target environment and manually cross-check the new resource types against AWS’s pricing pages â CDK will tell you exactly what’s being created, but it won’t tell you what it costs. For teams running many stacks, AWS Cost Explorer filtered by the managed-by: cdk tag from the snippet above gives a clean view of CDK-attributable spend separate from manually created resources.
It’s also worth periodically running cdk diff against production even when you don’t intend to deploy â a nonzero diff on a stack nobody’s touched recently usually means either drift (someone modified a resource manually in the console) or an unnoticed dependency update that changed a construct’s defaults. Catching that during a routine check is far less disruptive than discovering it during an actual incident.
Cleaning up to avoid ongoing charges
# Destroy a specific stack and everything it created
cdk destroy TiCdkDemoStack
# Destroy every stack in the app
cdk destroy --all
# The bootstrap CDKToolkit stack can stay â it costs pennies a month
# and re-bootstrapping later means a fresh ECR/S3 setup
If a stack used RemovalPolicy.RETAIN anywhere (the CDK default for anything data-bearing unless you explicitly override it, as this tutorial did for demo purposes), cdk destroy will leave those specific resources behind intentionally â check the CloudFormation console afterward if you want a fully clean account.
Frequently asked questions
Is AWS CDK free?
Yes. CDK is an open-source framework and CLI with no licensing cost. You only pay for the AWS resources your stacks create, plus the negligible S3/ECR storage costs of the bootstrap stack.
Do I need to know CloudFormation to use CDK?
Not to get started, but understanding CloudFormation concepts â stacks, changesets, rollback behavior â helps significantly once you’re debugging a deployment failure, since CDK ultimately deploys through CloudFormation and its error messages surface at that layer.
Which language should I pick for CDK?
TypeScript has the deepest documentation and example coverage since it’s the language CDK itself is authored in. Python is a close second and often preferred by teams already using Python for other infrastructure or data tooling. All five supported languages produce identical CloudFormation output.
What’s the difference between cdk synth and cdk deploy?cdk synth compiles your code into a CloudFormation template with no AWS interaction. cdk deploy does that same synthesis step, then actually applies the resulting template to your AWS account.
Can I use CDK alongside existing CloudFormation or Terraform resources?
Yes. CDK can import existing resources into a stack via fromXxxAttributes() methods, and separate CDK and Terraform-managed stacks can coexist in the same account as long as they’re not both trying to manage the same resource.
Why does CDK require Node.js even for Python or Java projects?
The CDK Toolkit and the JSII layer that lets multiple languages share one construct library both run on Node.js under the hood. Your application code can be pure Python or Java, but the CLI orchestrating synth/deploy needs Node.js installed regardless.
How do I roll back a bad CDK deployment?
CloudFormation automatically rolls back a failed deployment to the last known-good state. For a deployment that succeeded but introduced a bug, redeploy the previous version of your CDK code â CDK doesn’t have its own separate rollback mechanism beyond what CloudFormation provides.
Should I commit cdk.out to version control?
No. cdk.out is the synthesized output directory, regenerated on every synth â treat it like a build artifact and add it to .gitignore, which cdk init does by default.
Can CDK manage resources outside AWS?
Not directly â CDK’s construct library is AWS-specific, since it synthesizes to CloudFormation, which is itself AWS-only. Multi-cloud teams typically pair CDK for their AWS footprint with a separate tool (Terraform or Pulumi) for other providers rather than expecting one tool to cover both.
How is CDK different from the Serverless Framework or SAM?
AWS SAM (Serverless Application Model) is a CloudFormation extension focused specifically on serverless resources (Lambda, API Gateway, DynamoDB) using a simplified YAML syntax. CDK covers the full breadth of AWS services, not just serverless, and lets you use real programming-language constructs instead of a YAML shorthand. Some teams use both â CDK for broader infrastructure, SAM’s local testing tools for fast Lambda iteration.
For deeper background, AWS maintains the authoritative CDK v2 Developer Guide, the CDK GitHub repository for release notes and issue tracking, the bootstrapping reference, the testing guide for the assertions module, the AWS CDK product page, and the official Node.js download page for current LTS releases.





