CloudFormation vs CDK
Compare AWS CloudFormation and CDK for infrastructure as code with real-world examples.
Prerequisites
- Basic understanding of infrastructure as code concepts
- AWS account with CloudFormation permissions
- Familiarity with at least one programming language (for CDK)
Infrastructure as Code on AWS
AWS offers two first-party Infrastructure as Code (IaC) tools: CloudFormation and the Cloud Development Kit (CDK). CloudFormation is a declarative template-based system using JSON or YAML, while CDK lets you define infrastructure using general-purpose programming languages like TypeScript, Python, Java, Go, and C#. Understanding the strengths and trade-offs of each helps you choose the right tool for your team and project.
Importantly, CDK is not a replacement for CloudFormation. CDK synthesizes CloudFormation templates under the hood. Every CDK deployment ultimately creates or updates a CloudFormation stack. CDK is a higher-level abstraction that generates CloudFormation, so understanding CloudFormation fundamentals is valuable regardless of which tool you use. When something goes wrong with a CDK deployment, you will be troubleshooting CloudFormation stack events.
This guide provides a comprehensive comparison of both tools, including real-world code examples, testing strategies, migration paths, and a decision framework to help you choose the right approach for your organization.
Third-Party Alternatives
While this guide focuses on AWS first-party tools, Terraform (by HashiCorp) and Pulumi are popular multi-cloud alternatives. Terraform uses HCL (a declarative DSL) and manages state externally. Pulumi, like CDK, uses general-purpose programming languages but supports multiple cloud providers. If multi-cloud IaC is a requirement, these tools deserve evaluation alongside CDK and CloudFormation.
CloudFormation: The Foundation
CloudFormation has been available since 2011 and is the most battle-tested IaC tool on AWS. It provides declarative infrastructure definitions where you describe the desired end state and AWS handles the provisioning order, dependency resolution, rollbacks, and resource lifecycle. Every AWS service launches with CloudFormation support on day one, making it the most comprehensive IaC tool for AWS resources.
CloudFormation Architecture
When you deploy a CloudFormation template, the service creates a stack, a collection of AWS resources managed as a single unit. CloudFormation determines the dependency order, creates resources in parallel where possible, and tracks the state of every resource. If any resource fails to create, CloudFormation automatically rolls back the entire stack to its previous state, ensuring you never end up with a partially deployed environment.
AWSTemplateFormatVersion: '2010-09-09'
Description: Serverless API with Lambda, API Gateway, and DynamoDB
Parameters:
Environment:
Type: String
AllowedValues: [dev, staging, prod]
Default: dev
LogRetentionDays:
Type: Number
Default: 30
AllowedValues: [7, 14, 30, 60, 90, 365]
Conditions:
IsProd: !Equals [!Ref Environment, prod]
Resources:
ApiFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Sub "${Environment}-api-handler"
Runtime: python3.12
Handler: index.handler
MemorySize: !If [IsProd, 512, 256]
Timeout: 30
Code:
S3Bucket: !Ref DeploymentBucket
S3Key: !Ref CodePackageKey
Environment:
Variables:
TABLE_NAME: !Ref DataTable
ENVIRONMENT: !Ref Environment
Tracing:
Mode: Active
Tags:
- Key: Environment
Value: !Ref Environment
DataTable:
Type: AWS::DynamoDB::Table
DeletionPolicy: !If [IsProd, Retain, Delete]
Properties:
TableName: !Sub "${Environment}-data"
BillingMode: PAY_PER_REQUEST
PointInTimeRecoverySpecification:
PointInTimeRecoveryEnabled: !If [IsProd, true, false]
AttributeDefinitions:
- AttributeName: pk
AttributeType: S
- AttributeName: sk
AttributeType: S
KeySchema:
- AttributeName: pk
KeyType: HASH
- AttributeName: sk
KeyType: RANGE
SSESpecification:
SSEEnabled: true
FunctionLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub "/aws/lambda/${Environment}-api-handler"
RetentionInDays: !Ref LogRetentionDays
Outputs:
FunctionArn:
Value: !GetAtt ApiFunction.Arn
Export:
Name: !Sub "${Environment}-api-function-arn"
TableName:
Value: !Ref DataTableCloudFormation Strengths
- Mature and stable: Over a decade of production use across millions of stacks worldwide. Edge cases are well-documented.
- No additional tooling: Templates are plain YAML/JSON files. No build step, no compiler, no package manager required.
- Direct AWS support: CloudFormation is a first-class AWS service with premium support coverage and same-day resource type support for new services.
- Automatic rollback: Failed deployments automatically roll back to the previous known-good state, ensuring consistency.
- StackSets: Deploy identical stacks across multiple accounts and regions from a single template. Essential for organizational governance.
- Drift detection: Identifies resources that have been modified outside CloudFormation, helping maintain configuration consistency.
- Change sets: Preview exactly what resources will be created, modified, or deleted before applying changes.
CloudFormation Weaknesses
- Verbose templates: YAML templates grow unwieldy quickly. A moderately complex stack can reach 1,000+ lines.
- Limited logic: No real loops.
Fn::Ifis the only conditional, and it cannot be nested easily. Complex logic requires creative workarounds. - Poor code reuse: Nested stacks are cumbersome. Modules exist but have limitations. Copy-pasting between templates is common.
- Slow feedback: You must deploy to validate many things. Template validation catches syntax errors but not logical ones.
- 200-resource limit per stack: Large applications must be split across multiple stacks with cross-stack references.
CDK: Infrastructure with Programming Languages
CDK brings the full power of programming languages to infrastructure definition. You get loops, conditionals, type safety, IDE autocomplete, unit testing, and the ability to create reusable abstractions. CDK introduces the concept of “constructs,” cloud components at three levels of abstraction.
CDK Construct Levels
| Level | Name | Description | Example |
|---|---|---|---|
| L1 | Cfn Resources | Direct 1:1 mapping to CloudFormation resources. Auto-generated from the CloudFormation spec. | CfnBucket, CfnFunction |
| L2 | Curated Constructs | Opinionated defaults with helper methods. Written by the CDK team with best practices built in. | Bucket, Function with grantRead() |
| L3 | Patterns | High-level patterns that wire multiple resources together into common architectures. | LambdaRestApi, ApplicationLoadBalancedFargateService |
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as logs from 'aws-cdk-lib/aws-logs';
interface ApiStackProps extends cdk.StackProps {
environment: 'dev' | 'staging' | 'prod';
}
export class ApiStack extends cdk.Stack {
public readonly apiUrl: string;
constructor(scope: Construct, id: string, props: ApiStackProps) {
super(scope, id, props);
const isProd = props.environment === 'prod';
// DynamoDB table with environment-specific settings
const table = new dynamodb.Table(this, 'DataTable', {
partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'sk', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
pointInTimeRecovery: isProd,
encryption: dynamodb.TableEncryption.AWS_MANAGED,
removalPolicy: isProd
? cdk.RemovalPolicy.RETAIN
: cdk.RemovalPolicy.DESTROY,
});
// Lambda function with best practices baked in
const fn = new lambda.Function(this, 'ApiHandler', {
runtime: lambda.Runtime.PYTHON_3_12,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda/'),
memorySize: isProd ? 512 : 256,
timeout: cdk.Duration.seconds(30),
tracing: lambda.Tracing.ACTIVE,
environment: {
TABLE_NAME: table.tableName,
ENVIRONMENT: props.environment,
POWERTOOLS_SERVICE_NAME: 'api',
},
logRetention: isProd
? logs.RetentionDays.ONE_YEAR
: logs.RetentionDays.ONE_WEEK,
});
// One line grants least-privilege read/write access
// CDK automatically generates the precise IAM policy
table.grantReadWriteData(fn);
// API Gateway wired to Lambda
const api = new apigateway.LambdaRestApi(this, 'Api', {
handler: fn,
proxy: true,
deployOptions: {
stageName: props.environment,
tracingEnabled: true,
metricsEnabled: true,
},
});
this.apiUrl = api.url;
}
}The Power of grant* Methods
CDK L2 constructs include grant*() methods that automatically generate least-privilege IAM policies. table.grantReadWriteData(fn) creates a policy allowing only GetItem, PutItem, UpdateItem, DeleteItem, BatchGetItem, BatchWriteItem, and Query on that specific table ARN. This is much safer and easier than hand-writing IAM policies in CloudFormation, where getting the exact action list and resource ARN format correct is error-prone.
CDK Strengths
- Real programming languages: Loops, conditionals, abstractions, and composition using TypeScript, Python, Java, Go, or C#
- Type safety and IDE support: Autocomplete, inline documentation, and compile-time error detection catch mistakes before deployment
- Reusable constructs: Create custom construct libraries and publish them to npm, PyPI, or Maven for organization-wide reuse
- Built-in best practices: L2 constructs include sensible defaults (encryption enabled, removal policies, security settings)
- Unit testing: Test infrastructure configuration with assertion libraries before deploying
- CDK Pipelines: Self-mutating CI/CD pipelines that deploy infrastructure changes through stages with approvals
CDK Weaknesses
- Programming knowledge required: Teams without software engineering experience face a steeper learning curve
- Build step required: CDK must synthesize templates before deployment, adding complexity to the toolchain
- Non-determinism risk: CDK can make API calls at synthesis time (VPC lookups, AZ lookups), introducing potential drift
- Abstraction leakage: When CDK abstractions don't cover your use case, you drop to L1 constructs and lose the ergonomic benefits
- Breaking changes: CDK library updates occasionally include breaking changes that require code modifications
- Debugging complexity: Errors in synthesized CloudFormation templates can be difficult to trace back to CDK code
Side-by-Side Comparison
| Dimension | CloudFormation | CDK |
|---|---|---|
| Language | JSON / YAML | TypeScript, Python, Java, Go, C# |
| Learning curve | Low (declarative syntax) | Medium (requires programming knowledge) |
| Code reuse | Nested stacks, modules, macros | Classes, packages, npm/PyPI/Maven |
| Testing | cfn-lint, TaskCat, cfn-guard | Unit tests, snapshot tests, fine-grained assertions |
| Abstraction level | Resource-level only | L1 (CFN), L2 (curated), L3 (patterns) |
| Rollback | Automatic | Automatic (uses CloudFormation under the hood) |
| State management | CloudFormation service (no external state) | CloudFormation service + cdk.context.json |
| Drift detection | Built-in | Via CloudFormation (same mechanism) |
| Multi-account deploy | StackSets (native, battle-tested) | CDK Pipelines, cdk-nag, custom constructs |
| IDE experience | Basic YAML/JSON editing | Full IDE support (autocomplete, go-to-definition, refactoring) |
| Community constructs | AWS resource types, macros | Construct Hub (1,400+ constructs) |
Testing Infrastructure Code
One of CDK's strongest advantages is testability. You can write unit tests that verify your infrastructure configuration before deploying, catching issues like missing encryption, overly permissive security groups, or incorrect resource configurations. CloudFormation also has testing tools, but they are less integrated into the development workflow.
CDK Testing with Assertions
import { Template, Match, Capture } from 'aws-cdk-lib/assertions';
import * as cdk from 'aws-cdk-lib';
import { ApiStack } from '../lib/api-stack';
describe('ApiStack', () => {
let template: Template;
beforeAll(() => {
const app = new cdk.App();
const stack = new ApiStack(app, 'TestStack', {
environment: 'prod',
});
template = Template.fromStack(stack);
});
test('DynamoDB table has point-in-time recovery in prod', () => {
template.hasResourceProperties('AWS::DynamoDB::Table', {
PointInTimeRecoverySpecification: {
PointInTimeRecoveryEnabled: true,
},
});
});
test('DynamoDB table uses PAY_PER_REQUEST billing', () => {
template.hasResourceProperties('AWS::DynamoDB::Table', {
BillingMode: 'PAY_PER_REQUEST',
});
});
test('Lambda function has X-Ray tracing enabled', () => {
template.hasResourceProperties('AWS::Lambda::Function', {
TracingConfig: { Mode: 'Active' },
});
});
test('Lambda has correct memory for prod', () => {
template.hasResourceProperties('AWS::Lambda::Function', {
MemorySize: 512,
});
});
test('No S3 buckets are publicly accessible', () => {
const buckets = template.findResources('AWS::S3::Bucket');
for (const [id, bucket] of Object.entries(buckets)) {
expect((bucket as any).Properties?.PublicAccessBlockConfiguration).toEqual(
expect.objectContaining({
BlockPublicAcls: true,
BlockPublicPolicy: true,
})
);
}
});
test('IAM roles have least-privilege policies', () => {
const policyCapture = new Capture();
template.hasResourceProperties('AWS::IAM::Policy', {
PolicyDocument: policyCapture,
});
// Verify no wildcard actions
const policy = policyCapture.asObject();
for (const statement of policy.Statement) {
expect(statement.Action).not.toContain('*');
}
});
});CDK Nag for Compliance
CDK Nag is a validation tool that checks your CDK constructs against sets of rules (called Nag Packs). It catches security misconfigurations, compliance violations, and best practice deviations at synthesis time, before any resources are deployed.
import { App, Aspects } from 'aws-cdk-lib';
import { AwsSolutionsChecks, NagSuppressions } from 'cdk-nag';
import { ApiStack } from '../lib/api-stack';
const app = new App();
const stack = new ApiStack(app, 'ProdStack', {
environment: 'prod',
});
// Apply AWS Solutions checks to all constructs
Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true }));
// Suppress known-acceptable findings with justification
NagSuppressions.addStackSuppressions(stack, [
{
id: 'AwsSolutions-APIG1',
reason: 'Access logging configured at the account level via centralized logging',
},
]);CloudFormation Testing Tools
| Tool | Purpose | When to Use |
|---|---|---|
cfn-lint | Template linting and validation | Pre-commit hook, CI/CD pipeline |
cfn-guard | Policy-as-code validation rules | Compliance checks before deployment |
| TaskCat | Deploy templates in test accounts | Integration testing CloudFormation templates |
| Change Sets | Preview resource changes | Before every production update |
# CloudFormation Guard rules for security compliance
# Run: cfn-guard validate -d template.yaml -r rules.guard
# All S3 buckets must have encryption enabled
AWS::S3::Bucket {
Properties.BucketEncryption.ServerSideEncryptionConfiguration[*] {
ServerSideEncryptionByDefault.SSEAlgorithm in ["aws:kms", "AES256"]
}
}
# All Lambda functions must have tracing enabled
AWS::Lambda::Function {
Properties.TracingConfig.Mode == "Active"
}
# No security groups should allow SSH from 0.0.0.0/0
AWS::EC2::SecurityGroup {
Properties.SecurityGroupIngress[*] {
when IpProtocol == "tcp" and FromPort == 22 {
CidrIp != "0.0.0.0/0"
}
}
}CDK Context and Non-Determinism
CDK can make API calls at synthesis time to look up VPCs, AMIs, and Availability Zones. These values are cached in cdk.context.json. Always commit this file to version control. Without it, your templates may change unexpectedly when context values are re-fetched (for example, if a new AZ becomes available), leading to unintended infrastructure changes. Run cdk diff before every deployment to catch unexpected changes.
Creating Reusable Constructs
One of CDK's most powerful capabilities is the ability to create reusable construct libraries that encode your organization's best practices. Instead of documenting standards in a wiki and hoping teams follow them, you encode them directly in shared constructs that teams import and use.
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as kms from 'aws-cdk-lib/aws-kms';
import * as cdk from 'aws-cdk-lib';
export interface SecureBucketProps {
/** Bucket name (optional - CDK will generate if omitted) */
bucketName?: string;
/** Enable versioning (default: true) */
versioned?: boolean;
/** Days before transitioning to IA (default: 30) */
iaTransitionDays?: number;
/** Days before transitioning to Glacier (default: 90) */
glacierTransitionDays?: number;
/** Days before expiring objects (default: no expiration) */
expirationDays?: number;
}
/**
* A secure S3 bucket with encryption, access logging, lifecycle policies,
* and public access blocking baked in.
*
* Usage:
* const bucket = new SecureBucket(this, 'DataBucket', {
* versioned: true,
* iaTransitionDays: 30,
* });
*/
export class SecureBucket extends Construct {
public readonly bucket: s3.Bucket;
public readonly key: kms.Key;
constructor(scope: Construct, id: string, props: SecureBucketProps = {}) {
super(scope, id);
this.key = new kms.Key(this, 'Key', {
enableKeyRotation: true,
description: `Encryption key for ${id}`,
});
this.bucket = new s3.Bucket(this, 'Bucket', {
bucketName: props.bucketName,
encryption: s3.BucketEncryption.KMS,
encryptionKey: this.key,
bucketKeyEnabled: true,
versioned: props.versioned ?? true,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
enforceSSL: true,
minimumTLSVersion: 1.2,
objectOwnership: s3.ObjectOwnership.BUCKET_OWNER_ENFORCED,
removalPolicy: cdk.RemovalPolicy.RETAIN,
lifecycleRules: [
{
transitions: [
{
storageClass: s3.StorageClass.INFREQUENT_ACCESS,
transitionAfter: cdk.Duration.days(props.iaTransitionDays ?? 30),
},
{
storageClass: s3.StorageClass.GLACIER,
transitionAfter: cdk.Duration.days(props.glacierTransitionDays ?? 90),
},
],
...(props.expirationDays && {
expiration: cdk.Duration.days(props.expirationDays),
}),
abortIncompleteMultipartUploadAfter: cdk.Duration.days(7),
},
],
});
}
}CDK Pipelines for CI/CD
CDK Pipelines is a high-level construct that creates a self-mutating CI/CD pipeline for deploying CDK applications. The pipeline deploys your infrastructure changes through stages (dev, staging, prod) with optional manual approvals. The “self-mutating” aspect means that when you modify the pipeline definition in code, the pipeline updates itself on the next run.
import * as cdk from 'aws-cdk-lib';
import { CodePipeline, CodePipelineSource, ShellStep } from 'aws-cdk-lib/pipelines';
import { ApiStage } from './api-stage';
export class PipelineStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const pipeline = new CodePipeline(this, 'Pipeline', {
pipelineName: 'api-pipeline',
synth: new ShellStep('Synth', {
input: CodePipelineSource.gitHub('my-org/my-repo', 'main'),
commands: [
'npm ci',
'npm run build',
'npm run test',
'npx cdk synth',
],
}),
});
// Deploy to dev (no approval)
pipeline.addStage(new ApiStage(this, 'Dev', {
environment: 'dev',
env: { account: '111111111111', region: 'us-east-1' },
}));
// Deploy to staging with integration tests
const staging = pipeline.addStage(new ApiStage(this, 'Staging', {
environment: 'staging',
env: { account: '222222222222', region: 'us-east-1' },
}));
staging.addPost(new ShellStep('IntegrationTests', {
commands: ['npm run test:integration'],
}));
// Deploy to prod with manual approval
const prod = pipeline.addStage(new ApiStage(this, 'Prod', {
environment: 'prod',
env: { account: '333333333333', region: 'us-east-1' },
}));
prod.addPre(new cdk.pipelines.ManualApprovalStep('PromoteToProd'));
}
}StackSets for Governance, CDK Pipelines for Applications
Use CloudFormation StackSets for deploying organizational governance resources (IAM roles, Config rules, GuardDuty enablement) across all accounts. Use CDK Pipelines for deploying application infrastructure through dev/staging/prod stages. These tools serve different purposes and complement each other well in a well-architected multi-account environment.
Migration Strategies
Many organizations have existing CloudFormation templates and want to adopt CDK for new projects. CDK provides several migration paths.
Importing Existing CloudFormation Stacks
CDK can import existing CloudFormation resources into a CDK-managed stack using the cdk import command. This allows you to gradually migrate resources from manually managed CloudFormation stacks to CDK without recreating them.
Using L1 Constructs as a Bridge
L1 constructs (prefixed with Cfn) provide a 1:1 mapping to CloudFormation resources. You can translate CloudFormation YAML directly to L1 CDK code as a first migration step, then gradually refactor to L2 constructs to gain the ergonomic benefits.
Migration Decision Table
| Scenario | Recommended Approach |
|---|---|
| New project, no existing templates | Start with CDK (TypeScript recommended) |
| Existing CFN templates, team knows programming | New features in CDK, gradually migrate existing stacks |
| Existing CFN templates, team prefers declarative | Stay with CloudFormation, adopt cfn-guard and modules |
| Multi-cloud requirement | Consider Terraform or Pulumi instead |
| Organization-wide governance | CloudFormation StackSets for governance, CDK for apps |
When to Choose Which
The right choice depends on your team, not the technology. Both tools are production-ready and can handle any scale of AWS deployment.
Choose CloudFormation When
- Your team is not comfortable with programming languages and prefers declarative configuration
- You need StackSets for multi-account/multi-region governance deployments
- You are working with existing CloudFormation templates and do not want to migrate
- You want the simplest possible toolchain with no build step, compiler, or package manager
- You need to support teams across your organization with varying technical skills
- You are deploying simple, well-defined infrastructure that does not need complex logic
Choose CDK When
- Your team has software engineering experience and wants to leverage it for infrastructure
- You want to create reusable infrastructure libraries shared across teams via package managers
- You need complex logic: loops for creating multiple similar resources, environment-specific configuration, conditional resource creation
- You want to unit test your infrastructure configuration as part of your CI/CD pipeline
- You value IDE support with autocomplete, type checking, and inline documentation
- You want built-in best practices from L2 constructs (encryption defaults, grant methods, security settings)
Consider Both
Many organizations use CDK for new projects while maintaining existing CloudFormation templates. CDK can import existing CloudFormation stacks, and you can use L1 constructs to work with CloudFormation resources directly when needed. There is no requirement to standardize on a single tool across your entire organization.
Key Takeaways
CDK and CloudFormation are complementary, not competing tools. CDK generates CloudFormation under the hood and adds the power of general-purpose programming languages. Start with CDK for new projects if your team has programming experience ; the testing, reuse, and IDE support significantly improve productivity. Use CloudFormation directly for simpler stacks, governance resources deployed via StackSets, or when your team prefers declarative configuration. Regardless of which tool you choose, invest in testing and validation before deploying to production. Always commit cdk.context.json for CDK projects and run cdk diffbefore every deployment.
Key Takeaways
- 1CloudFormation uses declarative YAML/JSON; CDK uses imperative code in TypeScript, Python, etc.
- 2CDK synthesizes to CloudFormation templates; it is an abstraction layer, not a replacement.
- 3CDK constructs (L1, L2, L3) provide increasing levels of abstraction and best-practice defaults.
- 4CloudFormation is better for teams with YAML expertise and simpler infrastructure.
- 5CDK excels when infrastructure needs loops, conditionals, or shared patterns across teams.
- 6Both support drift detection, change sets, and stack policies for safe deployments.
Frequently Asked Questions
Is AWS CDK replacing CloudFormation?
Which should I choose: CloudFormation or CDK?
Can I mix CloudFormation and CDK in the same project?
What are CDK constructs?
How does Terraform compare to CloudFormation and CDK?
Written by CloudToolStack Team
Cloud engineers and architects with hands-on experience across AWS, Azure, and GCP. We write guides based on real-world production patterns, not just documentation rewrites.
Disclaimer: This guide is for educational purposes. Cloud services change frequently; always refer to official documentation for the latest information. AWS, Azure, and GCP are trademarks of their respective owners.