AWS CDK vs CloudFormation: How Developers Define Cloud Infrastructure in Python
If you've ever stared at a 2,000-line CloudFormation YAML file and thought "there has to be a better way", AWS CDK is the answer — it lets you define cloud infrastructure using real programming languages like Python, with loops, functions, and abstractions, while still deploying through CloudFormation under the hood.
TL;DR
| Dimension | AWS CloudFormation | AWS CDK |
|---|---|---|
| Language | YAML / JSON (declarative) | Python, TypeScript, Java, Go, C# (imperative) |
| Abstraction Level | Raw AWS resource definitions | High-level constructs (L1 → L3) |
| Reusability | Nested stacks, macros (complex) | Python classes, pip packages |
| Deployment Engine | CloudFormation directly | Synthesizes to CloudFormation, then deploys |
| IDE Support | Limited (schema hints) | Full autocomplete, type checking, linting |
| Testing | Manual / cfn-lint | Unit tests with pytest, assertions on synth output |
| Learning Curve | Low (if you know YAML) | Low for developers, requires CDK CLI |
The Core Mental Model: CDK is a Compiler
Think of AWS CDK exactly like a compiler in a traditional software stack. You write high-level Python code (your "source"), and CDK synthesizes it into a CloudFormation template (the "machine code"). CloudFormation then executes that template against the AWS APIs to provision real resources.
Analogy: Writing raw CloudFormation YAML is like writing x86 assembly — precise and powerful, but tedious and error-prone at scale. AWS CDK is like writing Python — you work at a higher abstraction level, and the compiler (CDK) handles the translation to low-level instructions (CloudFormation JSON) that the machine (AWS) understands.
How CDK Works Under the Hood
- You write Python code using CDK constructs (e.g.,
aws_cdk.aws_s3.Bucket). cdk synthruns your Python app, which builds an in-memory construct tree and serializes it to a CloudFormation template JSON file inside acdk.out/directory (the "Cloud Assembly").cdk deploytakes that synthesized template and calls the CloudFormation API to create or update a Stack.- CloudFormation provisions the actual AWS resources (S3 buckets, Lambda functions, VPCs, etc.) via AWS service APIs.
- CDK Toolkit (Bootstrap): For assets like Lambda code or Docker images, CDK first uploads them to a bootstrapped S3 bucket and ECR repository in your account before deployment.
The Three Levels of CDK Constructs
CDK's power comes from its layered construct library. Understanding these three levels is fundamental to using CDK effectively.
L1 — CloudFormation Resource (Cfn* classes)
Direct 1:1 mapping to a CloudFormation resource. Every property you'd write in YAML is available as a constructor argument. These are auto-generated from the CloudFormation resource specification.
# L1: Raw CloudFormation resource — full control, maximum verbosity
from aws_cdk import aws_s3 as s3
bucket = s3.CfnBucket(
self, "MyL1Bucket",
bucket_name="my-l1-bucket",
versioning_configuration=s3.CfnBucket.VersioningConfigurationProperty(
status="Enabled"
)
)
L2 — Curated Construct (Intent-based)
The most commonly used level. L2 constructs wrap L1 resources with sensible defaults, helper methods, and security best practices baked in. They expose intent rather than raw configuration.
# L2: High-level construct — sensible defaults, grant() methods, event hooks
from aws_cdk import aws_s3 as s3
from aws_cdk import aws_lambda as lambda_
bucket = s3.Bucket(
self, "MyL2Bucket",
versioned=True,
encryption=s3.BucketEncryption.S3_MANAGED,
block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
)
my_function = lambda_.Function(
self, "MyFunction",
runtime=lambda_.Runtime.PYTHON_3_12,
handler="index.handler",
code=lambda_.Code.from_asset("lambda"),
)
# L2 magic: generates a least-privilege IAM policy automatically
bucket.grant_read(my_function)
L3 — Patterns (aws-solutions-constructs or custom)
Opinionated, multi-resource patterns that encode a complete architectural pattern. For example, an "API Gateway → Lambda → DynamoDB" pattern as a single construct. These can be from the official aws-solutions-constructs library or your own reusable Python classes.
# L3: Custom reusable pattern — encapsulates an entire microservice
from aws_cdk import Stack
from aws_cdk import aws_lambda as lambda_
from aws_cdk import aws_dynamodb as dynamodb
from aws_cdk import aws_apigateway as apigw
from constructs import Construct
class MicroservicePattern(Construct):
"""Reusable L3 pattern: API GW -> Lambda -> DynamoDB"""
def __init__(self, scope: Construct, id: str, service_name: str) -> None:
super().__init__(scope, id)
table = dynamodb.Table(
self, f"{service_name}Table",
partition_key=dynamodb.Attribute(
name="id",
type=dynamodb.AttributeType.STRING
),
billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST,
)
handler = lambda_.Function(
self, f"{service_name}Handler",
runtime=lambda_.Runtime.PYTHON_3_12,
handler="index.handler",
code=lambda_.Code.from_asset(f"services/{service_name}"),
environment={"TABLE_NAME": table.table_name},
)
table.grant_read_write_data(handler)
self.api = apigw.LambdaRestApi(
self, f"{service_name}Api",
handler=handler,
)
Project Structure: A Real Python CDK App
🔽 Click to expand: Full CDK Python project layout and app entry point
my_cdk_app/
├── app.py # CDK App entry point
├── cdk.json # CDK toolkit configuration
├── requirements.txt # Python dependencies
├── my_cdk_app/
│ ├── __init__.py
│ └── my_stack.py # Stack definition
├── lambda/
│ └── index.py # Lambda function code
└── tests/
└── unit/
└── test_my_stack.py # CDK unit tests
# app.py — The CDK App entry point
import aws_cdk as cdk
from my_cdk_app.my_stack import MyStack
app = cdk.App()
MyStack(
app, "MyStack",
env=cdk.Environment(
account="123456789012",
region="us-east-1"
)
)
app.synth()
# my_cdk_app/my_stack.py — Stack definition
import aws_cdk as cdk
from aws_cdk import (
aws_s3 as s3,
aws_lambda as lambda_,
aws_iam as iam,
)
from constructs import Construct
class MyStack(cdk.Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
# S3 Bucket with encryption and versioning
bucket = s3.Bucket(
self, "DataBucket",
versioned=True,
encryption=s3.BucketEncryption.S3_MANAGED,
block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
removal_policy=cdk.RemovalPolicy.DESTROY, # For dev only
)
# Lambda function
processor = lambda_.Function(
self, "DataProcessor",
runtime=lambda_.Runtime.PYTHON_3_12,
handler="index.handler",
code=lambda_.Code.from_asset("lambda"),
environment={
"BUCKET_NAME": bucket.bucket_name,
},
timeout=cdk.Duration.seconds(30),
)
# Least-privilege: read-only access to the bucket
bucket.grant_read(processor)
# Output the bucket name
cdk.CfnOutput(self, "BucketName", value=bucket.bucket_name)
Unit Testing CDK Stacks with pytest
One of CDK's biggest advantages over raw CloudFormation is testability. You can assert on the synthesized CloudFormation template using the aws_cdk.assertions module — no deployment required.
# tests/unit/test_my_stack.py
import aws_cdk as cdk
from aws_cdk import assertions
from my_cdk_app.my_stack import MyStack
def test_s3_bucket_created_with_versioning():
app = cdk.App()
stack = MyStack(app, "TestStack")
template = assertions.Template.from_stack(stack)
# Assert an S3 bucket exists with versioning enabled
template.has_resource_properties("AWS::S3::Bucket", {
"VersioningConfiguration": {
"Status": "Enabled"
}
})
def test_lambda_has_bucket_env_var():
app = cdk.App()
stack = MyStack(app, "TestStack")
template = assertions.Template.from_stack(stack)
# Assert Lambda has the BUCKET_NAME environment variable
template.has_resource_properties("AWS::Lambda::Function", {
"Environment": {
"Variables": assertions.Match.object_like({
"BUCKET_NAME": assertions.Match.any_value()
})
}
})
Key CDK CLI Commands
| Command | What It Does |
|---|---|
cdk init app --language python | Scaffold a new CDK Python project |
cdk bootstrap aws://123456789012/us-east-1 | Provision CDK toolkit resources (S3, ECR, IAM) in your account/region — required once per environment |
cdk synth | Synthesize CloudFormation template to cdk.out/ without deploying |
cdk diff | Show infrastructure changes before deploying (like terraform plan) |
cdk deploy | Synthesize and deploy the stack via CloudFormation |
cdk destroy | Delete the CloudFormation stack and its resources |
cdk ls | List all stacks in the app |
When to Use CDK vs Raw CloudFormation
| Use CDK When... | Use Raw CloudFormation When... |
|---|---|
| Your team is developer-centric (Python/TypeScript) | Your team is ops-centric and prefers declarative YAML |
| You need reusable infrastructure components (internal libraries) | You need a simple, self-contained template with no build step |
| You want unit tests on infrastructure logic | You're integrating with tools that consume raw CFN templates directly |
| You're building complex, multi-stack applications | You need maximum portability with zero toolchain dependencies |
Glossary
| Term | Definition |
|---|---|
| Construct | The basic building block of a CDK app. Represents a cloud component at L1, L2, or L3 abstraction. |
| Stack | A unit of deployment in CDK, maps 1:1 to a CloudFormation Stack. |
| Synthesis (synth) | The process of executing your CDK app to produce a CloudFormation template and Cloud Assembly. |
| Cloud Assembly | The output of cdk synth — a directory (cdk.out/) containing CloudFormation templates, assets, and deployment manifests. |
| Bootstrap | One-time setup that provisions CDK-managed resources (S3 bucket, IAM roles) in an AWS account/region to support CDK deployments. |
Next Steps
- 📖 AWS CDK v2 Developer Guide — official starting point
- 📦 CDK Python API Reference — all L1/L2 construct properties
- 🧩 AWS Solutions Constructs — vetted L3 patterns
- 🧪 Start with
cdk init app --language python, write a simple S3 + Lambda stack, runcdk synth, and inspect the generated CloudFormation JSON incdk.out/— this single exercise will solidify the mental model.
Comments
Post a Comment