TerraformPilot

AWS CDK vs Terraform: Which IaC Tool Should You Use in 2026?

AWS CDK vs Terraform compared for 2026. Programming languages vs HCL, L2 constructs vs modules, state management, multi-cloud

By Luca Berton ·

Side-by-side comparison of AWS CDK, Terraform
CriterionAWS CDKTerraform
LanguageTypeScript / Python / Java / C# / GoHCL
Cloud supportAWS-first (CDKTF for multi-cloud)Multi-cloud
AbstractionsL1 / L2 / L3 constructsModules
StateSynthesised to CloudFormationSelf-managed state
Best forAWS teams already in TS/PythonMulti-cloud teams using HCL

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

#
FeatureAWS CDKTerraform
LanguageTypeScript, Python, Java, Go, C#HCL (declarative)
Underlying engineCloudFormationTerraform Core
Multi-cloud❌ AWS only✅ 3000+ providers
StateAWS-managed (CloudFormation)Self-managed (S3 + DynamoDB)
AbstractionsL2/L3 constructs (high-level)Modules (community registry)
TestingJest, pytest (unit tests)terraform test, Terratest
IDE supportFull (TypeScript autocomplete)Good (HCL plugins)
Learning curveLow if you know the languageLow (HCL is simple)
Rollback✅ Automatic (CloudFormation)❌ Manual
Drift detection✅ Built-in⚠️ terraform plan
Community modulesConstruct HubTerraform Registry
Escape hatchRaw CloudFormationN/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

#

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.