What is AWS CDK and How is it Different from CloudFormation
If you've spent any time manually writing CloudFormation YAML, you've probably felt the friction — hundreds of lines of declarative markup to express what should be a straightforward pattern. AWS CDK lets you define the same infrastructure using Python classes, loops, and conditionals, then synthesizes it into CloudFormation under the hood. Understanding where CDK ends and CloudFormation begins is what separates engineers who use CDK effectively from those who fight it.
TL;DR: AWS CDK vs CloudFormation
| Dimension | AWS CDK | CloudFormation |
|---|---|---|
| Authoring language | Python, TypeScript, Java, Go, C# | YAML / JSON |
| Abstraction level | High-level constructs + raw escape hatches | Raw resource declarations |
| Reuse mechanism | Python classes, pip packages, Construct Hub | Nested stacks, macros |
| Deployment engine | CloudFormation (synthesized template) | CloudFormation directly |
| Diff / preview | cdk diff before deploy | Change sets |
| State management | Delegated to CloudFormation | CloudFormation stack state |
| Learning curve | Requires CDK concepts + CloudFormation awareness | Declarative, but verbose |
How AWS CDK Works Under the Hood
CDK is a synthesis framework. When you run cdk synth, your Python code executes and produces one or more CloudFormation templates — standard JSON/YAML that CloudFormation then deploys. CDK itself never talks to AWS resource APIs directly. It is a code-to-template compiler, and CloudFormation is the deployment engine.
(cdk.out/)"] C --> D["cdk deploy"] D --> E["S3 / ECR
(Asset Staging)"] D --> F["CloudFormation API"] F --> G["AWS Resources"] style A fill:#f0f4ff,stroke:#4a6cf7 style C fill:#fff8e1,stroke:#f9a825 style G fill:#e8f5e9,stroke:#43a047
- App — the root CDK construct. One app produces one or more stacks.
- Stack — maps 1:1 to a CloudFormation stack. Each stack is an independent deployment unit.
- Construct — the fundamental building block. Every resource, pattern, or logical group is a construct. Constructs compose into trees.
- cdk synth — executes your Python, walks the construct tree, and emits CloudFormation JSON into the
cdk.out/directory. - cdk deploy — uploads the synthesized template and any assets (Lambda zips, Docker images) to S3/ECR, then calls CloudFormation to create or update the stack.
The synthesized template is real CloudFormation — you can inspect it, commit it, or deploy it manually. CDK adds no runtime agent or sidecar. Once deployed, the stack is a standard CloudFormation stack and behaves exactly like one.
Construct Levels: L1, L2, L3
CDK organizes constructs into three abstraction levels. Knowing which level to use — and when to drop down — is the core skill.
e.g. ApplicationLoadBalancedFargateService"] --> L2 L2["L2 Curated Constructs
e.g. s3.Bucket, lambda.Function"] --> L1 L1["L1 Cfn* Classes
e.g. CfnBucket — 1:1 CloudFormation"] L1 --> CF["CloudFormation Resource"] style L3 fill:#e8f5e9,stroke:#43a047 style L2 fill:#e3f2fd,stroke:#1e88e5 style L1 fill:#fff8e1,stroke:#f9a825 style CF fill:#fce4ec,stroke:#e53935
- L1 (Cfn* classes) — Auto-generated from the CloudFormation resource spec.
aws_cdk.aws_s3.CfnBucketmaps directly toAWS::S3::Bucket. Every CloudFormation property is exposed. Use L1 when you need a property that L2 hasn't exposed yet. - L2 (curated constructs) — Hand-authored by the CDK team.
aws_cdk.aws_s3.Bucketadds sane defaults, grant methods (bucket.grant_read(lambda_fn)), and event integrations. This is where you spend most of your time. - L3 (patterns) — Opinionated multi-resource patterns.
aws_cdk.aws_ecs_patterns.ApplicationLoadBalancedFargateServicecreates a VPC, ECS cluster, ALB, task definition, and IAM roles in one construct. Useful for standard architectures; escape to L2 when you need control.
Think of L1 as raw CloudFormation in Python syntax, L2 as a well-reviewed Terraform module, and L3 as a pre-built reference architecture you can instantiate in three lines.
Writing Your First CDK Stack in Python
Install the CDK CLI and bootstrap your environment before writing any stack code. Bootstrapping provisions the S3 bucket and ECR repository CDK uses to stage assets — without it, cdk deploy fails for stacks with assets.
# Install CDK CLI (requires Node.js — the CLI is JS, your app code is Python)
npm install -g aws-cdk
# Create a new Python CDK project
mkdir my-infra && cd my-infra
cdk init app --language python
# Activate the virtualenv created by cdk init
source .venv/bin/activate
pip install -r requirements.txt
# Bootstrap the target account/region (one-time per account/region)
cdk bootstrap aws://123456789012/us-east-1
A minimal stack that creates an S3 bucket with versioning enabled and a Lambda function with read access to that bucket:
🔽 Click to expand — example CDK stack (Python)
import aws_cdk as cdk
from aws_cdk import (
aws_s3 as s3,
aws_lambda as lambda_,
Duration,
)
from constructs import Construct
class MyAppStack(cdk.Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
# L2 construct — sane defaults, versioning opt-in
data_bucket = s3.Bucket(
self, 'DataBucket',
versioned=True,
removal_policy=cdk.RemovalPolicy.RETAIN,
encryption=s3.BucketEncryption.S3_MANAGED,
)
# L2 Lambda construct
processor = lambda_.Function(
self, 'Processor',
runtime=lambda_.Runtime.PYTHON_3_12,
handler='handler.main',
code=lambda_.Code.from_asset('lambda'),
timeout=Duration.seconds(30),
environment={
'BUCKET_NAME': data_bucket.bucket_name,
},
)
# grant_read generates the least-privilege IAM policy automatically
data_bucket.grant_read(processor)
app = cdk.App()
MyAppStack(app, 'MyAppStack', env=cdk.Environment(
account='123456789012',
region='us-east-1',
))
app.synth()
The grant_read call is worth pausing on. It generates an IAM policy scoped to the specific bucket ARN and attaches it to the Lambda execution role — no manual policy authoring required. The synthesized policy looks like this:
{
"Effect": "Allow",
"Action": [
"s3:GetObject*",
"s3:GetBucket*",
"s3:List*"
],
"Resource": [
"arn:aws:s3:::my-app-stack-databucket-xxxx",
"arn:aws:s3:::my-app-stack-databucket-xxxx/*"
]
}
CDK resolves the bucket ARN at synthesis time using tokens — a lazy evaluation mechanism that defers value resolution until the full construct tree is assembled. You never hardcode ARNs.
CDK Tokens and Cross-Stack References
Tokens are CDK's answer to the CloudFormation !Ref / !GetAtt problem. When you write data_bucket.bucket_name in Python, you get back a string-like token — not the actual bucket name, which doesn't exist yet. CDK resolves tokens to CloudFormation intrinsic functions during synthesis.
# This prints a token, not a real bucket name — expected behavior
print(data_bucket.bucket_name)
# Output: ${Token[TOKEN.123]}
# After synth, this resolves to:
# {"Ref": "DataBucketXXXX"} in the CloudFormation template
Cross-stack references work the same way. If Stack B needs an output from Stack A, CDK automatically creates a CloudFormation Export in Stack A and an Fn::ImportValue in Stack B. The constraint this creates: once a cross-stack reference exists, you cannot delete the exporting stack until all consumers are removed first. This is a CloudFormation constraint, not a CDK one — but CDK makes it easy to create these dependencies without realizing it.
cdk diff, cdk deploy, and the CloudFormation Relationship
The daily CDK workflow maps directly onto CloudFormation operations. Understanding this mapping prevents surprises during deployment.
# Preview changes before deploying — generates a CloudFormation change set internally
cdk diff MyAppStack
# Deploy — synthesizes, uploads assets, then calls CloudFormation CreateChangeSet + ExecuteChangeSet
cdk deploy MyAppStack
# Synthesize only — inspect the CloudFormation template without deploying
cdk synth MyAppStack
# Destroy — calls CloudFormation DeleteStack
cdk destroy MyAppStack
cdk deploy calls CloudFormation's change set API under the hood — it does not bypass CloudFormation. This means all CloudFormation behaviors apply: rollback on failure, stack events in the console, and stack state management. If a deployment fails and leaves the stack in ROLLBACK_COMPLETE (from an initial creation failure) or UPDATE_ROLLBACK_FAILED, subsequent CDK deploys will fail at the CloudFormation layer, not the CDK layer. UPDATE_ROLLBACK_COMPLETE is a recoverable state and allows further updates.
# Check stack status when cdk deploy fails unexpectedly
aws cloudformation describe-stacks \
--stack-name MyAppStack \
--query 'Stacks[0].StackStatus'
# View stack events to find the root cause resource
aws cloudformation describe-stack-events \
--stack-name MyAppStack \
--query 'StackEvents[?ResourceStatus==`CREATE_FAILED` || ResourceStatus==`UPDATE_FAILED`].[LogicalResourceId,ResourceStatusReason]' \
--output table
The Misdiagnosis Pattern: When CDK Feels Broken but CloudFormation Is the Problem
A common failure mode: you update a CDK construct property, run cdk deploy, and the deployment fails with a cryptic error. The instinct is to look at CDK code. The actual problem is almost always in the synthesized CloudFormation template or the stack state.
Symptom: cdk deploy exits with 'Stack is in UPDATE_ROLLBACK_FAILED state and can not be updated.' Misdiagnosis: CDK bug, wrong construct version, synthesis error. Actual cause: A previous deployment partially failed, leaving CloudFormation in a stuck state. CDK synthesized correctly — CloudFormation refused the update. Fix: Use continue-update-rollback to recover the stack, then redeploy.
# Recover a stack stuck in UPDATE_ROLLBACK_FAILED
aws cloudformation continue-update-rollback \
--stack-name MyAppStack
# Wait for rollback to complete
aws cloudformation wait stack-rollback-complete \
--stack-name MyAppStack
# Now cdk deploy will work again
cdk deploy MyAppStack
Always inspect the synthesized template first when debugging. The template is the ground truth — CDK is just the generator.
Escape Hatches: When CDK Constructs Don't Expose What You Need
CDK L2 constructs don't expose every CloudFormation property. When you need a property that the L2 construct hasn't surfaced, use an escape hatch to access the underlying L1 resource.
bucket = s3.Bucket(self, 'DataBucket', versioned=True)
# Access the underlying L1 CfnBucket
cfn_bucket = bucket.node.default_child
# Set a property not exposed by the L2 construct
cfn_bucket.add_property_override('NotificationConfiguration.EventBridgeConfiguration.EventBridgeEnabled', True)
Escape hatches are a first-class CDK feature, not a workaround. The pattern is: use L2 for defaults and grants, drop to L1 via node.default_child for properties the L2 hasn't exposed yet. This is preferable to switching the entire resource to L1.
CDK vs CloudFormation: When to Use Each
application code?"} Q1 -->|Yes| Q2{"Need reuse across
stacks or accounts?"} Q1 -->|No| CFN["CloudFormation YAML/JSON"] Q2 -->|Yes| CDK["AWS CDK"] Q2 -->|No| Q3{"Simple, stable
infra?"} Q3 -->|Yes| CFN Q3 -->|No| CDK style CDK fill:#e3f2fd,stroke:#1e88e5 style CFN fill:#fff8e1,stroke:#f9a825
CDK is the right choice when your team writes application code in Python (or another supported language), when you need to reuse infrastructure patterns across multiple stacks or accounts, or when the verbosity of raw CloudFormation is slowing down iteration. CloudFormation directly is still appropriate for simple, stable infrastructure that rarely changes, for teams without a CDK-familiar engineer, or for environments where the CDK bootstrap stack is not permitted.
One non-obvious depth point: CDK's grant_* methods generate IAM policies using addToPolicy on the grantee's role. If the grantee is an imported resource (created outside the current CDK app), CDK cannot modify its role and will emit a warning — the grant silently does nothing. Always verify grants work by inspecting the synthesized template when the grantee is imported.
Wrap-Up: AWS CDK as a CloudFormation Compiler
AWS CDK doesn't replace CloudFormation — it compiles to it. The mental model that matters: CDK is your authoring layer, CloudFormation is your deployment engine, and the synthesized template is the contract between them. Once you internalize that, the behavior of cdk deploy, cross-stack references, and escape hatches all follow logically.
For Python developers, the practical path forward is to start with L2 constructs for standard resources, use grant_* methods for IAM instead of writing policies manually, and always run cdk diff before cdk deploy in production. When something breaks, read the CloudFormation stack events — not just the CDK output.
- AWS CDK v2 Developer Guide
- Construct Hub — community and AWS construct libraries
- CDK Python API Reference
Glossary
| Term | Definition |
|---|---|
| Construct | The fundamental CDK building block. Every resource, pattern, or logical group is a construct that composes into a tree. |
| Synthesis | The process of executing CDK Python code to produce CloudFormation templates, triggered by cdk synth or cdk deploy. |
| Token | A CDK lazy-evaluation placeholder that resolves to a CloudFormation intrinsic function (Ref, GetAtt) during synthesis. |
| Bootstrap | One-time setup that provisions the S3 bucket and ECR repository CDK needs to stage deployment assets in a target account/region. |
| Escape Hatch | A CDK pattern for accessing the underlying L1 (CfnResource) construct to set properties not exposed by an L2 construct. |
Comments
Post a Comment