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

DimensionAWS CDKCloudFormation
Authoring languagePython, TypeScript, Java, Go, C#YAML / JSON
Abstraction levelHigh-level constructs + raw escape hatchesRaw resource declarations
Reuse mechanismPython classes, pip packages, Construct HubNested stacks, macros
Deployment engineCloudFormation (synthesized template)CloudFormation directly
Diff / previewcdk diff before deployChange sets
State managementDelegated to CloudFormationCloudFormation stack state
Learning curveRequires CDK concepts + CloudFormation awarenessDeclarative, 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.

graph LR A["Python CDK App"] --> B["cdk synth"] B --> C["CloudFormation Template
(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
  1. App — the root CDK construct. One app produces one or more stacks.
  2. Stack — maps 1:1 to a CloudFormation stack. Each stack is an independent deployment unit.
  3. Construct — the fundamental building block. Every resource, pattern, or logical group is a construct. Constructs compose into trees.
  4. cdk synth — executes your Python, walks the construct tree, and emits CloudFormation JSON into the cdk.out/ directory.
  5. 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.

graph TD L3["L3 Patterns
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.CfnBucket maps directly to AWS::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.Bucket adds 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.ApplicationLoadBalancedFargateService creates 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.

sequenceDiagram participant Dev as Developer participant CDK as CDK CLI participant CFN as CloudFormation participant AWS as AWS Resources Dev->>CDK: cdk deploy CDK->>CDK: Synthesize template CDK->>CFN: Upload template + assets CFN->>CFN: CreateChangeSet CFN->>CFN: ExecuteChangeSet CFN->>AWS: Create / Update resources AWS-->>CFN: Resource events CFN-->>CDK: Stack status CDK-->>Dev: Deploy complete / failed
# 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

graph TD Start(["New infrastructure project"]) --> Q1{"Team writes
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.

Glossary

TermDefinition
ConstructThe fundamental CDK building block. Every resource, pattern, or logical group is a construct that composes into a tree.
SynthesisThe process of executing CDK Python code to produce CloudFormation templates, triggered by cdk synth or cdk deploy.
TokenA CDK lazy-evaluation placeholder that resolves to a CloudFormation intrinsic function (Ref, GetAtt) during synthesis.
BootstrapOne-time setup that provisions the S3 bucket and ECR repository CDK needs to stage deployment assets in a target account/region.
Escape HatchA CDK pattern for accessing the underlying L1 (CfnResource) construct to set properties not exposed by an L2 construct.

Related Posts

Comments

Popular posts from this blog

AWS SNS Email Alerts Not Arriving? The Subscription Confirmation Trap Explained

S3 Public Access Denied: Why Your Public Object URL Still Returns 403

EC2 No Internet Access in Custom VPC: Fix Internet Gateway and Route Table