AWS CDK lets you define infrastructure in TypeScript, Python, Java, or Go. Terraform uses HCL. Both deploy AWS resources — but the approach, ecosystem, and tradeoffs are very different.
Quick Comparison
| Feature | AWS CDK | Terraform |
|---|---|---|
| Language | TypeScript, Python, Java, Go, C# | HCL (declarative) |
| Underlying engine | CloudFormation | Terraform Core |
| Multi-cloud | ❌ AWS only | ✅ 3000+ providers |
| State | AWS-managed (CloudFormation) | Self-managed (S3 + DynamoDB) |
| Abstractions | L2/L3 constructs (high-level) | Modules (community registry) |
| Testing | Jest, pytest (unit tests) | terraform test, Terratest |
| IDE support | Full (TypeScript autocomplete) | Good (HCL plugins) |
| Learning curve | Low if you know the language | Low (HCL is simple) |
| Rollback | ✅ Automatic (CloudFormation) | ❌ Manual |
| Drift detection | ✅ Built-in | ⚠️ terraform plan |
| Community modules | Construct Hub | Terraform Registry |
| Escape hatch | Raw CloudFormation | N/A |
Language: Programming vs Declarative
AWS CDK (TypeScript)
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as rds from 'aws-cdk-lib/aws-rds';
export class AppStack extends cdk.Stack {
constructor(scope: cdk.App, id: string) {
super(scope, id);
const vpc = new ec2.Vpc(this, 'AppVpc', {
maxAzs: 3,
natGateways: 1,
// CDK automatically creates public/private subnets,
// route tables, internet gateway, NAT gateway
});
const db = new rds.DatabaseInstance(this, 'Database', {
engine: rds.DatabaseInstanceEngine.postgres({
version: rds.PostgresEngineVersion.VER_16_3,
}),
vpc,
instanceType: ec2.InstanceType.of(
ec2.InstanceClass.R6G, ec2.InstanceSize.LARGE
),
// CDK auto-creates security group, subnet group,
// parameter group with sensible defaults
});
}
}
~20 lines → Creates VPC (3 AZs, public/private subnets, IGW, NAT) + RDS with security groups.
Terraform (HCL)
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "app-vpc"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b", "us-east-1c"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
enable_nat_gateway = true
single_nat_gateway = true
}
resource "aws_db_instance" "main" {
identifier = "production"
engine = "postgres"
engine_version = "16.3"
instance_class = "db.r6g.large"
vpc_security_group_ids = [aws_security_group.db.id]
db_subnet_group_name = aws_db_subnet_group.main.name
# Must explicitly create security group, subnet group
}
resource "aws_security_group" "db" {
vpc_id = module.vpc.vpc_id
# Must define rules explicitly
}
resource "aws_db_subnet_group" "main" {
subnet_ids = module.vpc.private_subnets
}
~40 lines → Same result, but you define every component explicitly.
CDK’s Killer Feature: L2 Constructs
CDK’s Level 2 constructs encode AWS best practices automatically:
// L2: Smart defaults + helper methods
const bucket = new s3.Bucket(this, 'Data', {
encryption: s3.BucketEncryption.S3_MANAGED,
versioned: true,
removalPolicy: cdk.RemovalPolicy.RETAIN,
});
// Grant read access — CDK creates the IAM policy automatically
bucket.grantRead(myLambdaFunction);
// This one line creates:
// - IAM policy with s3:GetObject, s3:GetBucket*, s3:List*
// - Scoped to this specific bucket ARN
// - Attached to the Lambda's execution role
In Terraform, you write the IAM policy manually:
resource "aws_iam_role_policy" "lambda_s3" {
role = aws_iam_role.lambda.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["s3:GetObject", "s3:GetBucketLocation", "s3:ListBucket"]
Resource = [
aws_s3_bucket.data.arn,
"${aws_s3_bucket.data.arn}/*"
]
}]
})
}
Terraform’s Killer Feature: Multi-Cloud + Ecosystem
# One project: AWS + Cloudflare + Datadog + PagerDuty
resource "aws_instance" "web" { ... }
resource "cloudflare_record" "dns" { ... }
resource "datadog_monitor" "cpu" { ... }
resource "pagerduty_service" "web" { ... }
CDK can’t do this — it generates CloudFormation, which is AWS-only.
Testing
CDK: Unit Tests with Jest
import { Template } from 'aws-cdk-lib/assertions';
test('creates RDS instance with correct engine', () => {
const app = new cdk.App();
const stack = new AppStack(app, 'Test');
const template = Template.fromStack(stack);
template.hasResourceProperties('AWS::RDS::DBInstance', {
Engine: 'postgres',
DBInstanceClass: 'db.r6g.large',
});
});
CDK tests run instantly (no cloud calls) because they test the synthesized CloudFormation template.
Terraform: terraform test
# tests/vpc.tftest.hcl
run "vpc_creates_correctly" {
command = plan
assert {
condition = aws_vpc.main.cidr_block == "10.0.0.0/16"
error_message = "VPC CIDR should be 10.0.0.0/16"
}
}
When to Choose AWS CDK
- Your team writes TypeScript/Python daily and HCL feels foreign
- You want high-level abstractions (L2 constructs with smart defaults)
- You need unit testing against synthesized templates
- AWS-only infrastructure
- You value automatic IAM policies (
bucket.grantRead()) - You already use CloudFormation and want a better authoring experience
- You want automatic rollback on failures
When to Choose Terraform
- You manage multi-cloud or non-AWS resources (Cloudflare, Datadog, GitHub)
- You want explicit control over every resource (no hidden defaults)
- Your team prefers a declarative, purpose-built language (HCL)
- You need the largest module ecosystem (Terraform Registry)
- You want provider portability (AWS today, maybe Azure tomorrow)
- You’re already a Terraform shop
Common Migration Path
Many teams start with CDK for rapid prototyping, then adopt Terraform for production:
Prototype (CDK) → Production (Terraform)
- CDK for fast iteration with L2 constructs
- Terraform for explicit, auditable production config
Or the reverse — Terraform teams adopt CDK for complex application stacks where bucket.grantRead() saves hours of IAM policy writing.
Hands-On Courses
- Terraform for Beginners on CopyPasteLearn
- Terraform By Example — practical code examples
Conclusion
CDK and Terraform are both excellent — they optimize for different things. CDK gives you programming language power, smart defaults, and automatic IAM. Terraform gives you explicit control, multi-cloud reach, and the largest IaC ecosystem. If you’re AWS-only and your team loves TypeScript, CDK is compelling. If you manage anything beyond AWS or want maximum transparency in your infrastructure code, Terraform is the standard.