← All articles
CI/CD Infrastructure as Code: Terraform, Pulumi, and CDK C... 2026-02-09 · 6 min read · infrastructure · terraform · pulumi

Infrastructure as Code: Terraform, Pulumi, and CDK Compared

CI/CD 2026-02-09 · 6 min read infrastructure terraform pulumi cdk devops cloud

Infrastructure as Code: Terraform, Pulumi, and CDK Compared

Managing cloud infrastructure by clicking through a web console doesn't scale. Infrastructure as Code (IaC) lets you define servers, databases, DNS records, and networking in version-controlled files that can be reviewed, tested, and reproduced. But the tooling landscape is fragmented, and choosing wrong means painful migrations later.

This guide compares the tools that matter in 2026: Terraform, OpenTofu, Pulumi, AWS CDK, and SST.

Terraform and OpenTofu

Terraform is the most widely adopted IaC tool. It uses HCL (HashiCorp Configuration Language), a declarative DSL designed specifically for infrastructure definitions. You describe the desired state, and Terraform figures out how to get there.

Basic Terraform Workflow

# main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
  backend "s3" {
    bucket = "my-terraform-state"
    key    = "prod/terraform.tfstate"
    region = "us-west-2"
  }
}

provider "aws" {
  region = "us-west-2"
}

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  tags = { Name = "production", Environment = "prod" }
}

resource "aws_subnet" "public" {
  count             = 2
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)
  availability_zone = data.aws_availability_zones.available.names[count.index]
  tags = { Name = "public-${count.index}" }
}
terraform init      # Download providers, initialize backend
terraform plan      # Preview changes (always review this)
terraform apply     # Apply changes
terraform destroy   # Tear down everything

The plan step is Terraform's killer feature. It shows you exactly what will be created, modified, or destroyed before anything happens. Never skip it.

State Management

Terraform tracks every resource it manages in a state file. This is both its greatest strength (drift detection, dependency tracking) and its biggest operational headache.

Remote state is mandatory for teams. Local state files cause conflicts the moment two people run terraform apply. Use S3 with DynamoDB locking, GCS, or Terraform Cloud.

State pitfalls to avoid:

Modules

Modules are Terraform's abstraction for reusable components -- a directory of .tf files with input variables and outputs.

# modules/vpc/main.tf
variable "name" { type = string }
variable "cidr" { type = string, default = "10.0.0.0/16" }

resource "aws_vpc" "this" {
  cidr_block           = var.cidr
  enable_dns_hostnames = true
  tags = { Name = var.name }
}

output "vpc_id" { value = aws_vpc.this.id }
module "production_vpc" {
  source = "./modules/vpc"
  name   = "production"
  cidr   = "10.0.0.0/16"
}

The Terraform Registry has thousands of community modules, but be cautious -- third-party modules can be poorly maintained. Prefer modules from terraform-aws-modules or write your own.

OpenTofu: The Fork

When HashiCorp changed Terraform's license from MPL to BSL in 2023, the community forked it as OpenTofu under the Linux Foundation. It's a drop-in replacement -- same HCL syntax, same providers, same state format. Just swap terraform for tofu.

OpenTofu has added features Terraform lacks, including client-side state encryption and for_each on provider blocks. If you're starting fresh, OpenTofu is the better choice -- it's truly open source and moving faster on developer-requested features.

Pulumi

Pulumi takes a fundamentally different approach: you write infrastructure in real programming languages. TypeScript, Python, Go, C#, Java -- whatever your team already knows.

TypeScript Example

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

const vpc = new aws.ec2.Vpc("production", {
  cidrBlock: "10.0.0.0/16",
  enableDnsHostnames: true,
  tags: { Name: "production", Environment: "prod" },
});

const publicSubnets = [0, 1].map(
  (i) =>
    new aws.ec2.Subnet(`public-${i}`, {
      vpcId: vpc.id,
      cidrBlock: `10.0.${i}.0/24`,
      availabilityZone: pulumi.output(
        aws.getAvailabilityZones()
      ).apply((azs) => azs.names[i]),
      tags: { Name: `public-${i}` },
    })
);

const db = new aws.rds.Instance("app-database", {
  engine: "postgres",
  engineVersion: "16.2",
  instanceClass: "db.t4g.micro",
  allocatedStorage: 20,
  dbName: "myapp",
  username: "admin",
  password: config.requireSecret("db-password"),
  skipFinalSnapshot: true,
});

export const vpcId = vpc.id;
export const dbEndpoint = db.endpoint;

The benefit is immediate: loops, conditionals, functions, type checking, IDE autocomplete, and unit testing all work natively. No learning HCL's quirky for_each syntax or wrestling with count vs for_each decisions.

Pulumi Workflow

pulumi new aws-typescript   # Scaffold a new project
pulumi up                   # Preview and deploy
pulumi preview              # Preview only (like terraform plan)
pulumi destroy              # Tear down

Pulumi manages state like Terraform but defaults to Pulumi Cloud for storage. You can self-host with S3 or use a local file backend.

Where Pulumi really shines is complex infrastructure with conditional logic and shared abstractions. You can create reusable component classes with full type safety -- something that's painful in HCL but natural in TypeScript.

AWS CDK

The AWS Cloud Development Kit is Amazon's "infrastructure in real languages" offering. Like Pulumi, you write TypeScript (or Python, Go, Java, C#). Unlike Pulumi, CDK generates CloudFormation templates under the hood and is AWS-only.

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";

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

    const vpc = new ec2.Vpc(this, "AppVpc", {
      maxAzs: 2,
      natGateways: 1,
    });

    new rds.DatabaseInstance(this, "Database", {
      engine: rds.DatabaseInstanceEngine.postgres({
        version: rds.PostgresEngineVersion.VER_16_2,
      }),
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T4G, ec2.InstanceSize.MICRO
      ),
      vpc,
      databaseName: "myapp",
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });
  }
}

CDK's "constructs" are its defining feature. The Vpc construct above creates subnets, route tables, internet gateways, and NAT gateways automatically -- things that take 50+ lines of Terraform. AWS maintains an extensive library of high-level constructs that encode best practices.

cdk synth     # Generate CloudFormation template
cdk diff      # Preview changes
cdk deploy    # Deploy
cdk destroy   # Tear down

The downside: CloudFormation deployments are slower than Terraform, stack updates can get stuck, and debugging failures means reading CloudFormation events. Multi-cloud is not an option.

SST (Serverless Stack)

SST is built on Pulumi and focuses on serverless full-stack applications. If you're building a web app with Lambda, API Gateway, DynamoDB, and a frontend, SST is purpose-built for that.

export default $config({
  app(input) {
    return {
      name: "my-app",
      removal: input?.stage === "production" ? "retain" : "remove",
    };
  },
  async run() {
    const bucket = new sst.aws.Bucket("Uploads");
    const api = new sst.aws.Function("Api", {
      handler: "packages/functions/src/api.handler",
      link: [bucket],
      url: true,
    });
    new sst.aws.StaticSite("Web", {
      path: "packages/web",
      build: { command: "npm run build", output: "dist" },
      environment: { VITE_API_URL: api.url },
    });
  },
});

The link: [bucket] automatically grants the Lambda function permissions to access the S3 bucket and injects the bucket name as an environment variable -- eliminating IAM policy boilerplate. SST is excellent within its niche but limited outside it.

Comparison Table

Feature Terraform/OpenTofu Pulumi AWS CDK SST
Language HCL (DSL) TS, Python, Go, C#, Java TS, Python, Go, C#, Java TypeScript
Multi-Cloud Yes Yes No (AWS only) No (AWS only)
State Self-managed or TF Cloud Pulumi Cloud or self-managed CloudFormation Pulumi (managed)
Preview terraform plan pulumi preview cdk diff sst diff
Learning Curve HCL syntax + concepts Low (familiar languages) Medium (constructs + CFN) Low (opinionated)
Ecosystem Largest Growing Large (AWS-backed) Smaller (focused)
Type Safety Limited Full Full Full
Deploy Speed Fast (direct API) Fast (direct API) Slow (CloudFormation) Fast (Pulumi engine)
Cost Free / TF Cloud paid Free / Cloud paid Free Free / Console paid

When to Use Which

Terraform/OpenTofu -- Your team includes ops engineers who aren't software developers, you need multi-cloud, or you want the largest ecosystem. Choose OpenTofu over Terraform for new projects (open-source license, client-side state encryption).

Pulumi -- Your team is primarily developers who want type safety, IDE support, and standard testing. Best for complex infrastructure with lots of conditional logic and shared abstractions across multiple clouds.

AWS CDK -- You are all-in on AWS and want high-level constructs that encode best practices. Good for standard architectures (VPC + ECS + RDS, API Gateway + Lambda) where CloudFormation's deployment speed isn't a blocker.

SST -- You're building a full-stack TypeScript serverless application on AWS and want the fastest path from code to deployment with resource linking and live debugging.

The Bottom Line

If you're unsure, start with OpenTofu. It has the largest ecosystem, the most learning resources, and HCL is straightforward enough that any developer can pick it up in a day. The declarative model prevents the "clever infrastructure code" that can make Pulumi projects hard to debug, and the plan/apply workflow is the gold standard for safe changes.

If your team writes TypeScript all day and finds HCL frustrating, Pulumi is the strongest alternative. The ability to use real abstractions without fighting a DSL is a genuine productivity win -- just stay disciplined about keeping infrastructure code simple.

Avoid CDK unless you're deeply committed to AWS and CloudFormation. Reach for SST only when your project fits its serverless mold.

Whatever you choose, the critical habit is the same: review plans before applying, use remote state with locking, and structure code into small, composable modules. The tool matters less than the discipline.