Skip to main content

Pulumi vs Terraform: Which IaC Tool Should You Choose in 2026?

Key Takeaway

Pulumi vs Terraform compared for 2026. Programming languages vs HCL, state management, testing, provider support, pricing, and when to choose each for AWS projects.

Table of Contents

Pulumi and Terraform are both multi-cloud IaC tools — but Pulumi uses real programming languages while Terraform uses HCL. This matters more than most comparisons admit.

Quick Comparison

FeaturePulumiTerraform
LanguageTypeScript, Python, Go, C#, Java, YAMLHCL (declarative)
Multi-cloud✅ AWS, Azure, GCP, 150+ providers✅ AWS, Azure, GCP, 3000+ providers
StatePulumi Cloud (default) or self-managedSelf-managed (S3 + DynamoDB)
LicenseApache 2.0 (open source)BSL 1.1 (not open source)
TestingNative (Jest, pytest, Go test)terraform test, Terratest
IDE supportFull (language-native)Good (HCL plugins)
SecretsBuilt-in encryptionExternal (Vault, ASM)
PricingFree tier + paid (Pulumi Cloud)Free + paid (HCP Terraform)
Importpulumi importImport blocks
MaturityFounded 2017Founded 2014
CommunityGrowingLargest IaC community
Provider count150+3000+

Language: The Core Difference

Pulumi (TypeScript)

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

// Real programming: loops, conditions, functions
const azs = ["us-east-1a", "us-east-1b", "us-east-1c"];

const vpc = new aws.ec2.Vpc("main", {
  cidrBlock: "10.0.0.0/16",
  enableDnsHostnames: true,
});

// Use a real for loop
const privateSubnets = azs.map((az, i) =>
  new aws.ec2.Subnet(`private-${az}`, {
    vpcId: vpc.id,
    cidrBlock: `10.0.${i}.0/24`,
    availabilityZone: az,
  })
);

// Conditional logic with real if/else
const db = new aws.rds.Instance("main", {
  engine: "postgres",
  instanceClass: pulumi.getStack() === "prod"
    ? "db.r6g.xlarge"
    : "db.t3.micro",
  vpcSecurityGroupIds: [dbSg.id],
  dbSubnetGroupName: subnetGroup.name,
});

// Reusable function
function tagAll(name: string): Record<string, string> {
  return {
    Name: name,
    Environment: pulumi.getStack(),
    ManagedBy: "pulumi",
  };
}

Terraform (HCL)

variable "azs" {
  default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
}

resource "aws_subnet" "private" {
  for_each = { for i, az in var.azs : az => i }

  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet("10.0.0.0/16", 8, each.value)
  availability_zone = each.key
}

resource "aws_db_instance" "main" {
  engine         = "postgres"
  instance_class = terraform.workspace == "prod" ? "db.r6g.xlarge" : "db.t3.micro"

  vpc_security_group_ids = [aws_security_group.db.id]
  db_subnet_group_name   = aws_db_subnet_group.main.name
}

Both work. Pulumi feels natural to developers. HCL feels natural to ops teams.

When Programming Languages Help

Complex Logic

// Pulumi: Deploy different configs per region
const regions = ["us-east-1", "eu-west-1", "ap-southeast-1"];

for (const region of regions) {
  const provider = new aws.Provider(`provider-${region}`, { region });

  const cluster = new aws.ecs.Cluster(`app-${region}`, {}, { provider });

  // Only deploy GPU instances in us-east-1
  if (region === "us-east-1") {
    new aws.ec2.Instance(`gpu-${region}`, {
      instanceType: "p4d.24xlarge",
      ami: gpuAmi,
    }, { provider });
  }
}

In Terraform, this requires nested for_each with conditional expressions — possible but less readable.

Dynamic Resource Generation

// Generate resources from a JSON config file
import * as fs from "fs";

interface ServiceConfig {
  name: string;
  port: number;
  replicas: number;
  cpu: number;
  memory: number;
}

const services: ServiceConfig[] = JSON.parse(
  fs.readFileSync("services.json", "utf-8")
);

for (const svc of services) {
  new aws.ecs.Service(`svc-${svc.name}`, {
    name: svc.name,
    desiredCount: svc.replicas,
    // ... use svc.port, svc.cpu, svc.memory
  });
}

When HCL Wins

Readability for Non-Developers

# HCL: anyone can read this
resource "aws_s3_bucket" "data" {
  bucket = "company-data"
}

resource "aws_s3_bucket_versioning" "data" {
  bucket = aws_s3_bucket.data.id
  versioning_configuration {
    status = "Enabled"
  }
}

HCL is constrained by design — you can’t write arbitrary logic, so the code stays predictable. Operations teams and security reviewers prefer this.

Plan Output

Both show execution plans, but Terraform’s plan output is the gold standard:

# terraform plan
+ aws_s3_bucket.data
    bucket = "company-data"

~ aws_instance.web
    instance_type: "t3.micro" → "t3.small"

- aws_security_group.old

Pulumi’s preview is similar but less widely understood.

State Management

Pulumi Cloud (Default)

pulumi login   # Uses Pulumi Cloud by default
pulumi up      # State stored in Pulumi Cloud

Free tier: 1 user, unlimited resources. Paid: team features.

Pulumi Self-Managed

pulumi login s3://my-pulumi-state
pulumi login file://~/.pulumi-state

Terraform

terraform {
  backend "s3" {
    bucket         = "my-tf-state"
    key            = "prod/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
  }
}

Always self-managed (unless using HCP Terraform).

Built-In Secrets

Pulumi encrypts secrets in state by default:

const config = new pulumi.Config();
const dbPassword = config.requireSecret("dbPassword");
// Encrypted in state, shown as [secret] in previews

new aws.rds.Instance("db", {
  password: dbPassword,  // Decrypted only during deployment
});
pulumi config set --secret dbPassword "MyS3cur3P@ss"
# Stored encrypted in Pulumi.prod.yaml

Terraform stores secrets in plaintext state (use backend encryption or ephemeral resources).

Testing

Pulumi: Native Unit Tests

// __tests__/infra.test.ts
import * as pulumi from "@pulumi/pulumi";
import { AppStack } from "../index";

pulumi.runtime.setMocks({
  newResource: (args) => ({ id: `${args.name}-id`, state: args.inputs }),
  call: (args) => args.inputs,
});

test("S3 bucket has versioning", async () => {
  const stack = new AppStack("test");
  const versioning = await new Promise((resolve) =>
    stack.bucket.versioning.apply(resolve)
  );
  expect(versioning).toBeTruthy();
});

Terraform: terraform test

run "bucket_versioning" {
  command = plan
  assert {
    condition     = aws_s3_bucket_versioning.data.versioning_configuration[0].status == "Enabled"
    error_message = "Versioning must be enabled"
  }
}

Provider Ecosystem

MetricPulumiTerraform
Total providers~150~3,000
AWS provider✅ Full✅ Full
Azure provider✅ Full✅ Full
GCP provider✅ Full✅ Full
Niche providersLimitedExtensive
Community modulesConstruct HubTerraform Registry

Pulumi auto-generates providers from Terraform providers (via Pulumi Bridge), so AWS/Azure/GCP coverage is equivalent. But niche providers may only exist in Terraform.

When to Choose Pulumi

  • Your team writes TypeScript/Python/Go daily and doesn’t want another language
  • You need complex logic (conditional resources, dynamic generation from configs)
  • You want built-in secret encryption in state
  • Unit testing with existing test frameworks matters
  • You prefer open-source licensing (Apache 2.0)
  • You’re building a platform with programmatic infrastructure generation

When to Choose Terraform

  • You want the largest community and ecosystem (3000+ providers, thousands of modules)
  • Your team includes ops engineers who prefer declarative config over code
  • You need maximum transparency — HCL constraints prevent complex logic bugs
  • Hiring — more engineers know Terraform than Pulumi
  • You want to stay with the industry standard
  • You need niche providers (many only exist in Terraform)

Hands-On Courses

Conclusion

Pulumi is the better choice for developer-heavy teams who want TypeScript/Python infrastructure with unit tests and built-in secrets. Terraform is the better choice for ops-oriented teams, mixed-skill organizations, and projects that need the largest provider/module ecosystem. Both are multi-cloud and production-ready. The decision usually comes down to team skills: developers lean Pulumi, operations teams lean Terraform.

🚀

Level Up Your Terraform Skills

Hands-on courses, books, and resources from Luca Berton

Luca Berton
Written by

Luca Berton

DevOps Engineer, AWS Partner, Terraform expert, and author. Creator of Ansible Pilot, Terraform Pilot, and CopyPasteLearn.