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:
-
Terraform excels with multi-cloud and declarative approach with the largest ecosystem.
-
Pulumi offers programmatic flexibility with general-purpose languages.
-
AWS CDK provides the best AWS experience with high-level abstractions.
Choose based on your cloud provider, team expertise, and specific requirements.
External Resources
- Terraform Documentation
- Pulumi Documentation
- AWS CDK Documentation
- Terraform Registry
- CDK Construct Library
Comments