Terraform vs Pulumi: Infrastructure as Code Compared
Compare Terraform and Pulumi for infrastructure as code. Understand the tradeoffs between HCL and general-purpose languages for managing cloud resources.
What you'll learn
- ✓How Terraform and Pulumi approach infrastructure as code differently
- ✓HCL vs general-purpose languages for infrastructure
- ✓State management and ecosystem differences
- ✓When to choose each tool
Prerequisites
- •Basic understanding of cloud infrastructure (AWS, GCP, or Azure)
Infrastructure as Code (IaC) has two leading approaches: Terraform uses HCL, a domain-specific language designed for infrastructure. Pulumi lets you write infrastructure using general-purpose languages like TypeScript, Python, Go, or C#. Both accomplish the same goal but with fundamentally different developer experiences.
Quick Comparison
| Feature | Terraform | Pulumi |
|---|---|---|
| Language | HCL (domain-specific) | TypeScript, Python, Go, C#, Java, YAML |
| Created by | HashiCorp (2014) | Pulumi Corp (2018) |
| License | BSL 1.1 (was MPL 2.0) | Apache 2.0 |
| State management | Local file, S3, Terraform Cloud | Local file, S3, Pulumi Cloud |
| Provider ecosystem | 4,000+ providers | Uses Terraform providers via bridge |
| Testing | terraform test (limited) | Standard test frameworks (Jest, pytest, Go test) |
| IDE support | HCL extensions | Full IDE support (any language) |
| Open-source fork | OpenTofu (MPL 2.0) | N/A |
| Plan/Preview | terraform plan | pulumi preview |
| Modularity | Modules | Functions, classes, packages |
Terraform
Terraform is the most widely adopted IaC tool. It uses HCL (HashiCorp Configuration Language), a declarative language where you describe the desired state of your infrastructure. Terraform calculates what changes are needed and applies them.
How It Works
# main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_s3_bucket" "app_data" {
bucket = "my-app-data-bucket"
tags = {
Environment = "production"
ManagedBy = "terraform"
}
}
resource "aws_dynamodb_table" "sessions" {
name = "app-sessions"
billing_mode = "PAY_PER_REQUEST"
hash_key = "session_id"
attribute {
name = "session_id"
type = "S"
}
}
output "bucket_arn" {
value = aws_s3_bucket.app_data.arn
}
terraform init # Download providers
terraform plan # Preview changes
terraform apply # Apply changes
terraform destroy # Tear down everything
Strengths
HCL is purpose-built for infrastructure. Its declarative nature means you describe what you want, not how to create it. The language is intentionally limited, which prevents over-engineering and keeps infrastructure code readable by non-developers.
Terraform’s provider ecosystem is the largest in the IaC world with over 4,000 providers covering every major cloud service, SaaS product, and platform. The plan/apply workflow is well-understood and provides a clear preview of changes before they happen.
The community is enormous. Stack Overflow answers, blog posts, and example modules cover virtually every infrastructure pattern. Terraform Cloud and Terraform Enterprise provide team collaboration features.
Weaknesses
HCL is limited. Loops, conditionals, and dynamic blocks exist but are awkward compared to a real programming language. Complex logic like “create this resource only if these three conditions are met, and name it based on a computed value” becomes painful in HCL.
Testing infrastructure code in Terraform is basic. terraform test was added recently but lacks the maturity of standard test frameworks. The BSL license change in 2023 concerned many users, leading to the OpenTofu fork.
# HCL's awkward conditional logic
resource "aws_instance" "web" {
count = var.enable_web_server ? var.instance_count : 0
ami = var.ami_id
instance_type = var.environment == "production" ? "t3.large" : "t3.micro"
dynamic "ebs_block_device" {
for_each = var.additional_volumes
content {
device_name = ebs_block_device.value.device_name
volume_size = ebs_block_device.value.size
}
}
}
Pulumi
Pulumi lets you define infrastructure using the same programming languages you use for application code. You get loops, functions, classes, conditionals, error handling, and package management from your language of choice.
How It Works
// index.ts
import * as aws from '@pulumi/aws';
const bucket = new aws.s3.Bucket('app-data', {
bucket: 'my-app-data-bucket',
tags: {
Environment: 'production',
ManagedBy: 'pulumi',
},
});
const sessionsTable = new aws.dynamodb.Table('sessions', {
name: 'app-sessions',
billingMode: 'PAY_PER_REQUEST',
hashKey: 'session_id',
attributes: [
{ name: 'session_id', type: 'S' },
],
});
export const bucketArn = bucket.arn;
pulumi up # Preview and apply changes
pulumi preview # Preview only
pulumi destroy # Tear down everything
Strengths
Using a real programming language is Pulumi’s greatest advantage. You can write functions to create reusable infrastructure patterns, use conditionals naturally, iterate over collections with standard loops, and handle errors with try/catch. Abstraction is straightforward: create a class that provisions a complete microservice with all its resources.
// Reusable infrastructure with real code
function createMicroservice(name: string, config: ServiceConfig) {
const repo = new aws.ecr.Repository(`${name}-repo`);
const cluster = new aws.ecs.Cluster(`${name}-cluster`);
const service = new aws.ecs.FargateService(`${name}-service`, {
cluster: cluster.arn,
desiredCount: config.replicas,
taskDefinition: createTaskDef(name, repo, config),
});
return { repo, cluster, service };
}
// Use it like any function
const api = createMicroservice('api', { replicas: 3, cpu: 512, memory: 1024 });
const worker = createMicroservice('worker', { replicas: 2, cpu: 256, memory: 512 });
Testing is natural. Use your language’s test framework to validate infrastructure code:
import { describe, it, expect } from 'vitest';
import * as pulumi from '@pulumi/pulumi/runtime';
describe('Infrastructure', () => {
it('should create S3 bucket with correct tags', async () => {
const bucket = new aws.s3.Bucket('test-bucket', {
tags: { Environment: 'test' },
});
const tags = await pulumi.output(bucket.tags).promise();
expect(tags?.Environment).toBe('test');
});
});
Weaknesses
Pulumi introduces the complexity of a general-purpose language into infrastructure code. A Terraform file is always declarative; a Pulumi program can include arbitrary logic, HTTP calls, or database queries during deployment. This power can be misused.
Pulumi’s provider ecosystem is largely bridged from Terraform providers, which means it covers the same ground but occasionally lags behind Terraform provider updates. Pulumi Cloud is the default state backend, and while you can use S3 or local storage, the experience is best with Pulumi Cloud (which has a free tier but charges for advanced features).
The community and documentation are smaller than Terraform’s. Finding solutions to common problems requires more effort.
State Management
Both tools track the current state of your infrastructure to calculate changes.
Terraform State
# Store state in S3 (common production setup)
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
}
}
Pulumi State
# Default: Pulumi Cloud (managed)
pulumi login
# Self-managed S3 backend
pulumi login s3://my-pulumi-state
# Local file
pulumi login --local
Both approaches store a JSON representation of your infrastructure. State locking prevents concurrent modifications. The main difference is that Pulumi Cloud is the default, while Terraform expects you to configure a backend.
Modularity and Reuse
Terraform Modules
# modules/vpc/main.tf
variable "cidr_block" {
type = string
}
resource "aws_vpc" "main" {
cidr_block = var.cidr_block
}
# Usage
module "production_vpc" {
source = "./modules/vpc"
cidr_block = "10.0.0.0/16"
}
Pulumi Components
// components/vpc.ts
class Vpc extends pulumi.ComponentResource {
public readonly vpcId: pulumi.Output<string>;
constructor(name: string, cidrBlock: string, opts?: pulumi.ComponentResourceOptions) {
super('custom:network:Vpc', name, {}, opts);
const vpc = new aws.ec2.Vpc(`${name}-vpc`, {
cidrBlock,
}, { parent: this });
this.vpcId = vpc.id;
}
}
// Usage
const prodVpc = new Vpc('production', '10.0.0.0/16');
Pulumi’s approach uses standard language features (classes, packages, npm/pip), while Terraform modules use a custom module system with its own versioning and publishing mechanisms.
When to Choose Terraform
- Your team includes infrastructure engineers who prefer declarative configuration
- You need the largest possible provider ecosystem with first-party support
- You want the most documentation, examples, and community support
- Your organization has existing Terraform expertise and modules
- You prefer a DSL that limits what infrastructure code can do (guardrails)
- You want to use OpenTofu for a fully open-source option
When to Choose Pulumi
- Your team is primarily software developers managing infrastructure
- You need complex logic (conditionals, loops, abstractions) in infrastructure code
- Testing infrastructure code with standard test frameworks matters
- You want to share types and logic between application and infrastructure code
- You prefer using a language you already know (TypeScript, Python, Go)
- You are building reusable infrastructure components distributed as packages
Final Verdict
Terraform is the industry standard with the largest ecosystem and community. If your team is comfortable with HCL and your infrastructure patterns are straightforward, Terraform (or OpenTofu) is the safe, well-supported choice.
Pulumi is the better choice when your infrastructure is complex enough to benefit from a real programming language. If you find yourself fighting HCL’s limitations regularly, or if your team is primarily developers rather than dedicated infrastructure engineers, Pulumi’s approach is more natural.
Both tools manage the same cloud resources and produce the same infrastructure. The choice is about developer experience: declarative DSL (Terraform) versus imperative programming (Pulumi). Neither is universally better.
Related articles
- DevOps Ansible vs Terraform: Configuration vs Infrastructure
Compare Ansible and Terraform for DevOps automation. Learn when to use each tool, how they differ in approach, and how to combine them effectively.
- DevOps Infrastructure as Code: Terraform Getting Started Guide
Learn Infrastructure as Code with Terraform. This beginner guide covers HCL syntax, providers, state management, modules, and deploying your first cloud resources step by step.
- DevOps Infrastructure Drift Detection and Remediation
Detect and fix infrastructure drift with Terraform, automated CI checks, and policy enforcement to keep your cloud state in sync with code.
- DevOps Pulumi for Infrastructure as Code with Real Programming Languages
Learn how Pulumi lets you define cloud infrastructure using TypeScript, Python, or Go instead of domain-specific languages.