AWS CDK Deep Dive
AWS CDK is an infrastructure-as-code framework that lets you define AWS resources using TypeScript, Python, Go, Java, or C#. The synthesized output is CloudFormation—CDK is a CloudFormation generator, not a replacement—but the authoring experience is fundamentally different. Instead of writing hundreds of lines of YAML, you instantiate constructs in code, use loops and conditions natively, extract shared patterns into reusable components, and write tests against the synthesized output.
Whether CDK is the right choice depends heavily on your team, your existing tooling, and what you are building. This post works through the full CDK programming model, from the three construct levels through CDK Pipelines and testing, and ends with a direct comparison against Terraform that does not pull punches.
The Construct Tree
Every CDK application is a tree of constructs. At the root is the App. Below the App are one or more Stack objects. Below each Stack are the constructs that represent AWS resources. The tree structure determines resource IDs (used for CloudFormation logical IDs), the scope for grants and permissions, and how outputs and cross-references resolve.
App
├── NetworkStack (Stack)
│ ├── VPC (ec2.Vpc)
│ │ ├── PublicSubnet1 (ec2.PublicSubnet)
│ │ └── PublicSubnet2 (ec2.PublicSubnet)
│ └── SecurityGroup (ec2.SecurityGroup)
└── AppStack (Stack)
├── WebhookHandler (Custom Construct)
│ ├── Bucket (s3.Bucket)
│ └── Function (lambda.Function)
└── Api (apigateway.RestApi)
Each node in the tree has an id (local within its parent) and a path (the full path from root). The path is used to generate stable CloudFormation logical IDs. If you rename or move a construct, the logical ID changes, and CloudFormation treats it as delete-and-recreate. This is the primary footgun for CDK refactoring.
Construct Levels
CDK constructs come in three levels of abstraction. Understanding which to use and when is the foundation of working effectively with CDK.
L1: CloudFormation Wrappers
L1 constructs map one-to-one to CloudFormation resource types. They are prefixed with Cfn (for CloudFormation). CfnBucket, CfnFunction, CfnTable. L1 constructs expose every CloudFormation property but provide no defaults or higher-level semantics.
|
|
You rarely write L1 constructs directly. Their main use is as an escape hatch when an L2 construct does not yet expose a CloudFormation property you need.
L2: Curated Constructs
L2 constructs are the everyday CDK interface. They wrap one CloudFormation resource with an opinionated, intent-based API that embeds AWS best practices as defaults.
|
|
The grantRead call does more than add an IAM statement—it creates a well-scoped policy with the correct S3 actions (s3:GetObject, s3:ListBucket) on the specific bucket ARN, and attaches it to the Lambda execution role. This is representative of L2’s value: the permission grant is one line, correct by construction, and requires no IAM knowledge from the caller.
L2 constructs also expose strongly typed references. bucket.bucketName returns a CDK token that resolves at synthesis time to the actual bucket name. Pass it between constructs without hardcoding names or using SSM lookups.
L3: Patterns
L3 constructs (also called pattern constructs) combine multiple L2 resources into a working architectural pattern. apigateway.LambdaRestApi creates an API Gateway REST API wired to a Lambda function in three lines; ecs_patterns.ApplicationLoadBalancedFargateService creates an ALB, ECS cluster, Fargate task definition, and all supporting IAM roles in one construct.
|
|
L3 constructs are excellent for getting started quickly. Their limitation is inflexibility—if the pattern does not match your exact needs, you may find yourself fighting the abstraction. In that case, compose L2 constructs directly.
Stacks and Environments
A Stack is a CDK construct that represents a single CloudFormation stack. Each Stack synthesizes to one CloudFormation template. Stacks are the unit of deployment—you deploy, update, and destroy stacks independently.
Environment Pinning
An environment in CDK terms is an account and region pair. Stacks can be environment-specific or environment-agnostic.
Environment-specific stacks know their target account and region:
|
|
Knowing the environment enables context lookups at synthesis time—VPC IDs, availability zones, SSM parameters. L2 constructs like ec2.Vpc.fromLookup require a known environment.
Environment-agnostic stacks omit the env property. They can deploy to any account/region but cannot use context lookups. Useful for generic constructs published as libraries, or for stacks whose target environment is resolved at deployment time:
|
|
Cross-Stack References
When one stack needs a value from another (a VPC ID, a bucket name, a Lambda ARN), you have two options.
CloudFormation exports (CfnOutput with exportName) create a hard dependency between stacks. If the producing stack changes or deletes the export, CloudFormation blocks the operation until the consuming stack is updated. In practice, this creates deployment ordering problems and makes stacks brittle.
SSM Parameter Store is the better pattern for cross-stack sharing:
|
|
SSM lookups during synthesis make a live AWS API call. The result is cached in cdk.context.json—commit this file so CI builds are deterministic and don’t require AWS credentials to synthesize.
Custom Constructs
Custom constructs are how CDK pays off at scale. Rather than repeating the same S3 + Lambda + IAM pattern across twenty stacks, you write it once as a construct, publish it as an npm package, and consume it everywhere.
A construct is any TypeScript class that extends Construct:
|
|
Expose the internal resources (bucket, function, dlq) as public properties so callers can further configure them:
|
|
The construct ID (ImageProcessor) namespaces all child construct IDs. The S3 bucket gets logical ID ImageProcessorBucketABC123, the Lambda gets ImageProcessorFunctionXYZ456. This means you can use the same construct class multiple times in the same stack without ID collisions—just give each instance a unique ID.
Escape Hatches
CDK’s L2 constructs do not expose every CloudFormation property. When you need a property that L2 has not surfaced, the escape hatch is the .node.defaultChild accessor, which returns the underlying L1 CfnResource.
|
|
For resources where CDK has no L2 at all, use CfnResource directly:
|
|
Use escape hatches sparingly. They produce correct CloudFormation but bypass CDK’s type checking and grant mechanics. When you find yourself frequently escaping the same L2 construct, the construct probably has a gap worth opening a GitHub issue for.
CDK Pipelines
CDK Pipelines is a construct library for building deployment pipelines that deploy CDK applications. The distinctive feature is self-mutation: the first deployment creates the CodePipeline, and every subsequent commit to the source repo can update the pipeline definition without manual intervention.
|
|
Bootstrap for Cross-Account Deployments
Cross-account deployments require each target account to be bootstrapped with a trust relationship to the pipeline account:
|
|
The bootstrap creates IAM roles (cdk-QUALIFIER-deploy-role-ACCOUNT-REGION, cdk-QUALIFIER-file-publishing-role-ACCOUNT-REGION) with trust policies allowing the pipeline account’s CodeBuild role to assume them. This is the mechanism by which a CodeBuild job in the pipeline account can deploy to a different account without storing cross-account credentials.
Scope the --cloudformation-execution-policies to the minimum permissions your stacks require. AdministratorAccess is convenient but means a compromised pipeline has full account access.
Testing CDK Applications
The aws-cdk-lib/assertions module provides unit test utilities against the synthesized CloudFormation template. This is CDK’s answer to “how do I know my infrastructure code does what I think?”
|
|
Match.arrayWith asserts that the array contains the specified elements (not that it equals them). Match.objectLike asserts that the object contains the specified properties. Match.exact asserts equality. This gives you fine-grained control over assertion specificity.
Snapshot testing captures the entire synthesized template on first run and compares against it on subsequent runs. It catches unintended changes but produces noisy diffs when legitimate changes occur:
|
|
Use snapshot tests for constructs that are stable and expensive to reason about manually. Prefer fine-grained assertions for anything you care about specifically—they fail with clear messages and are not broken by unrelated changes.
Integration tests with @aws-cdk/integ-tests-alpha deploy actual stacks to a real AWS account and run assertions against live resources. They are expensive (time and money) but necessary for constructs that depend on runtime behavior:
|
|
CDK vs Terraform
This comparison has a clear answer for some teams and is genuinely ambiguous for others.
What CDK Actually Is Under the Hood
CDK synthesizes to CloudFormation. cdk deploy runs cdk synth to generate CloudFormation templates, uploads them to S3, and calls cloudformation:CreateChangeSet followed by cloudformation:ExecuteChangeSet. The deployment speed and behavior is CloudFormation’s—CDK adds synthesis time on top of CloudFormation’s deployment time. A stack that takes 10 minutes to deploy with vanilla CloudFormation takes 10 minutes with CDK.
Drift detection is CloudFormation drift detection—limited to supported resource types, not continuous, requires an explicit detection request. Terraform’s terraform plan detects drift every time you run it, against all resources.
The Honest Table
| Concern | CDK | Terraform |
|---|---|---|
| Authoring language | TypeScript, Python, Go, Java, C# | HCL (domain-specific) |
| Loops and conditions | Native (for, if, map) | for_each, count, ternary |
| Abstraction and reuse | Constructs as packages on npm/PyPI | Modules on registry.terraform.io |
| Type safety | Strong (TypeScript) to moderate (Python) | Weak (HCL has types but limited checking) |
| State management | CloudFormation stacks (AWS-managed) | State files (S3 + DynamoDB locking, or Terraform Cloud) |
| Drift detection | CloudFormation drift (limited, on-demand) | terraform plan (comprehensive, on every apply) |
| Deployment speed | Slow (CloudFormation intermediary) | Fast (direct AWS API calls) |
| Multi-cloud | AWS only | AWS, Azure, GCP, everything with a provider |
| Non-AWS resources | Limited (GitHub, Datadog via custom resources) | Comprehensive (hundreds of providers) |
| Bootstrap requirement | Yes (CDKToolkit stack) | No |
| Escape to raw CloudFormation | Easy (escape hatches) | N/A |
| Testing | Template assertions, snapshot, integ-runner | terratest, terraform-compliance |
| Mature ecosystem | Growing | Large and mature |
When CDK Wins
CDK is the right choice when your team writes TypeScript or Python daily and finds HCL’s lack of abstraction frustrating. If you are building an internal developer platform where teams define their infrastructure as CDK constructs that platform engineers maintain, the ability to publish constructs as npm packages with versioning and type signatures is genuinely valuable. A new PostgresDatabase(this, 'DB', { size: 'small' }) construct that encapsulates RDS Multi-AZ, automated backups, SSM parameter output, and security group rules is impossible to express with equivalent usability in Terraform modules.
CDK also wins when the application code and infrastructure code live together and change together. A Lambda function defined next to its infrastructure code, bundled automatically by CDK’s asset system, deployed with cdk deploy—the feedback loop is tight.
When Terraform Wins
Terraform wins when your infrastructure spans multiple clouds. When you manage Datadog monitors, GitHub repositories, PagerDuty schedules, and Okta groups alongside AWS resources. CDK has no answer for non-AWS resources.
Terraform wins for teams that are not already writing TypeScript or Python at work. HCL has a shallow learning curve; getting productive in CDK requires understanding the programming language, the CDK construct model, CloudFormation concepts, and TypeScript’s type system simultaneously.
Terraform wins when operational simplicity matters more than authoring power. The terraform plan output is universally understood; the CDK synth output is a CloudFormation template that requires CloudFormation knowledge to interpret. When something goes wrong in production, Terraform’s debugging story—read the plan, read the state, look at the resource—is simpler than CDK’s—read the synthesized template, read the CloudFormation events, correlate back to the CDK source.
The Hybrid
Many organizations use both. Terraform for foundational infrastructure: VPCs, EKS clusters, IAM roles, shared databases, DNS. CDK for application infrastructure: Lambda functions, API Gateways, event buses, DynamoDB tables—resources that are closely coupled to application code and change with it. Terraform’s stability and drift detection are valuable for slow-moving shared infrastructure. CDK’s language integration and asset bundling are valuable for fast-moving application infrastructure.
Practical Configuration Patterns
Per-Environment Configuration
Avoid CfnParameter for environment configuration—it makes synthesis non-deterministic and complicates CDK Pipelines. Use typed Stack properties instead:
|
|
Consistent Tagging
Apply tags at the App level to cover all stacks:
|
|
Tags applied to a parent propagate to all children that support tagging. Override at the resource level when a specific resource needs different values.
Asset Bundling
CDK bundles Lambda code automatically. For Node.js, CDK can use esbuild to bundle and minify before uploading:
|
|
NodejsFunction uses Docker by default for reproducible builds. Pass bundling: { forceDockerBundling: false } to use the local Node.js/esbuild for faster iteration. Use Docker in CI to ensure consistent artifacts.
The --hotswap flag speeds up development iteration by bypassing CloudFormation for code-only changes:
|
|
Hotswap only works for specific change types (Lambda code, Docker images, environment variables). Any change that requires CloudFormation resources to be created or deleted falls back to a full deployment automatically.
Comments