Skip to main content
โšก Calmops

IaC Comparison: Terraform vs Pulumi vs CDK

Introduction

Infrastructure as Code (IaC) has become essential for managing cloud resources. Three tools dominate the space: Terraform (declarative HCL), Pulumi (imperative code), and AWS CDK (cloud-oriented programming).

This guide compares these tools across multiple dimensions to help you choose the right one for your organization.


Overview

Aspect Terraform Pulumi AWS CDK
Language HCL Python, TS, Go, C# TypeScript, Python, Java, C#
Approach Declarative Imperative/Declarative Imperative/Declarative
State Local/Remote Local/Remote Cloud-native
Learning Curve Low Medium Medium
Provider Support Multi-cloud Multi-cloud AWS-first

Terraform

Overview

Terraform uses HashiCorp Configuration Language (HCL) for declarative infrastructure definitions.

Basic Example

# main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true
  
  tags = {
    Name = "main-vpc"
  }
}

resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  map_public_ip_on_launch = true
  
  tags = {
    Name = "public-subnet"
  }
}

resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"
  subnet_id     = aws_subnet.public.id
  
  tags = {
    Name = "web-server"
  }
}

Modules

# modules/vpc/main.tf
variable "cidr_block" {}
variable "environment" {}

resource "aws_vpc" "main" {
  cidr_block = var.cidr_block
  tags = { Name = var.environment }
}

output "vpc_id" {
  value = aws_vpc.main.id
}
# main.tf
module "production" {
  source = "./modules/vpc"
  cidr_block = "10.0.0.0/16"
  environment = "production"
}

Workspaces

# Create workspace
terraform workspace new production

# Switch workspace
terraform workspace select production

# Use workspace in config
resource "aws_instance" "web" {
  # ...
  tags = {
    Name = "web-${terraform.workspace}"
  }
}

Pulumi

Overview

Pulumi lets you define infrastructure using general-purpose programming languages.

Basic Example (Python)

# __main__.py
import pulumi
import pulumi_aws as aws

# Create VPC
vpc = aws.ec2.Vpc(
    "main",
    cidr_block="10.0.0.0/16",
    enable_dns_hostnames=True,
    enable_dns_support=True,
    tags={"Name": "main-vpc"}
)

# Create Subnet
subnet = aws.ec2.Subnet(
    "public",
    vpc_id=vpc.id,
    cidr_block="10.0.1.0/24",
    map_public_ip_on_launch=True,
    tags={"Name": "public-subnet"}
)

# Create Security Group
sg = aws.ec2.SecurityGroup(
    "web-sg",
    description="Security group for web servers",
    vpc_id=vpc.id,
    ingress=[
        aws.ec2.SecurityGroupIngressArgs(
            protocol="tcp",
            from_port=80,
            to_port=80,
            cidr_blocks=["0.0.0.0/0"]
        ),
        aws.ec2.SecurityGroupIngressArgs(
            protocol="tcp",
            from_port=443,
            to_port=443,
            cidr_blocks=["0.0.0.0/0"]
        )
    ],
    tags={"Name": "web-sg"}
)

# Export results
pulumi.export("vpc_id", vpc.id)
pulumi.export("subnet_id", subnet.id)

TypeScript Example

import * as aws from "@pulumi/aws";
import * as awsx from "@pulumi/awsx";

const vpc = new awsx.ec2.Vpc("main", {
    cidrBlock: "10.0.0.0/16",
    numberOfAvailabilityZones: 3,
});

const cluster = new aws.ecs.Cluster("app", {
    vpcId: vpc.vpcId,
});

export const clusterId = cluster.id;

AWS CDK

Overview

AWS CDK uses cloud-oriented abstractions to define AWS resources.

Basic Example (TypeScript)

import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';

class MyStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const vpc = new ec2.Vpc(this, 'MainVPC', {
      cidr: '10.0.0.0/16',
      maxAzs: 3,
    });

    const cluster = new ecs.Cluster(this, 'AppCluster', {
      vpc: vpc,
    });
  }
}

const app = new cdk.App();
new MyStack(app, 'MyStack');

Constructs

// Simple ECS service with ALB
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ecs_patterns from 'aws-cdk-lib/aws-ecs-patterns';

const service = new ecs_patterns.ApplicationLoadBalancedFargateService(this, 'WebService', {
  cluster: cluster,
  memoryLimitMiB: 1024,
  cpu: 512,
  desiredCount: 2,
  publicLoadBalancer: true,
  taskImageOptions: {
    image: ecs.ContainerImage.fromRegistry("nginx"),
  },
});

Feature Comparison

State Management

# Terraform
terraform init
terraform plan
terraform apply
# State stored in tfstate file or remote backend

# Pulumi
pulumi login --local  # or pulumi login
pulumi up
# State stored in Pulumi Cloud or local file

# CDK
cdk deploy
# State stored in CloudFormation (hidden)

Iteration and Logic

# Terraform - for_each
resource "aws_instance" "servers" {
  for_each = toset(["web1", "web2", "web3"])
  
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"
  tags = {
    Name = each.value
  }
}
# Pulumi - standard loops
servers = []
for i in range(3):
    server = aws.ec2.Instance(f"server-{i}",
        ami="ami-0c55b159cbfafe1f0",
        instance_type="t3.micro",
        tags={"Name": f"server-{i}"}
    )
    servers.append(server)
// CDK - standard TypeScript
const servers = Array.from({length: 3}, (_, i) => 
  new ec2.Instance(this, `server-${i}`, {
    instanceType: ec2.InstanceType.T3_MICRO,
    machineImage: ec2.MachineImage.latestAmazonLinux(),
  })
);

Secrets Management

# Terraform - sensitive variables
variable "db_password" {
  sensitive = true
}

resource "aws_db_instance" "main" {
  password = var.db_password
}
# Pulumi - secret config
import pulumi
import pulumi_aws as aws

config = pulumi.Config()
db_password = config.require_secret("db-password")

db = aws.rds.Instance("main",
    password=db_password,
    # ...
)

Decision Framework

Choose Terraform If:

  • You want broad multi-cloud support
  • Your team prefers declarative configs
  • Strong community and ecosystem
  • Need mature tooling and workflows

Choose Pulumi If:

  • You want to use general-purpose languages
  • Need complex logic in infrastructure
  • Want strong typing with IDE support
  • Need to share code between projects

Choose AWS CDK If:

  • You work primarily with AWS
  • Want highest-level abstractions
  • Prefer TypeScript/Python
  • Need CloudFormation integration

Migration Patterns

Terraform to Pulumi

# Pulumi can import existing Terraform state
import pulumi_terraform as terraform

state = terraform.StateReference(
    name="terraform-state",
    stack="production",
    backend=terraform.StateBackend.BLOB,
    config={"container_name": "tfstate"}
)

# Import resources
aws_instance = state.get_resource(
    resource_type="aws_instance",
    id="i-1234567890"
)

Implementation Checklist

Terraform

  • Install Terraform CLI
  • Configure remote state backend
  • Set up variable files
  • Create module structure

Pulumi

  • Choose language
  • Install SDK
  • Configure state backend
  • Organize project structure

CDK

  • Install CDK CLI
  • Set up AWS credentials
  • Choose language
  • Create constructs

Summary

Each tool has distinct strengths:

  1. Terraform excels with multi-cloud and declarative approach with the largest ecosystem.

  2. Pulumi offers programmatic flexibility with general-purpose languages.

  3. AWS CDK provides the best AWS experience with high-level abstractions.

Choose based on your cloud provider, team expertise, and specific requirements.


External Resources

Comments