Pavan Rangani

HomeBlogTerraform vs Pulumi vs OpenTofu: IaC Tools Compared in 2026

Terraform vs Pulumi vs OpenTofu: IaC Tools Compared in 2026

By Pavan Rangani · February 13, 2026 · DevOps & Cloud

Terraform vs Pulumi vs OpenTofu: IaC Tools Compared in 2026

Terraform vs Pulumi vs OpenTofu: Infrastructure as Code Comparison for 2026

The Infrastructure as Code landscape fractured in 2023 when HashiCorp changed Terraform’s license from open-source to BSL. OpenTofu emerged as the community fork, and Pulumi continues to grow as the programming-language alternative to HCL. A clear-eyed Terraform Pulumi OpenTofu comparison is now a critical decision for every infrastructure team. Therefore, this guide compares all three tools honestly — language, state management, ecosystem, testing, and the practical trade-offs that only surface once you operate them at scale.

The Licensing Split: Why Three Tools Now

HashiCorp switched Terraform from Mozilla Public License (open-source) to Business Source License in August 2023. This means competitors cannot offer managed Terraform services. The Linux Foundation forked Terraform 1.5.x as OpenTofu under MPL. For end users — companies using Terraform to manage their own infrastructure — the BSL does not restrict you. You can keep using Terraform freely. Moreover, HashiCorp (now part of IBM) continues investing heavily in Terraform development.

The practical impact: AWS, Spacelift, env0, and Scalr now offer OpenTofu as an alternative to Terraform in their platforms. If you use Terraform Cloud/Enterprise, you are locked into HashiCorp’s offering. If you want vendor choice for your IaC platform, OpenTofu gives you that freedom. If you do not care about managed platforms and self-host your IaC, both work identically. The decision, in other words, is rarely about the BSL itself; it is about whether you want optionality in the platform layer that sits above your code.

HCL vs Programming Languages: The Core Difference

The fundamental question is: do you want a domain-specific language (HCL) or a general-purpose programming language (TypeScript, Python, Go, Java)?

# Terraform/OpenTofu: HCL — declarative, purpose-built
resource "aws_instance" "web" {
  count         = var.instance_count
  ami           = data.aws_ami.ubuntu.id
  instance_type = var.instance_type

  tags = merge(var.common_tags, {
    Name = "web-${count.index + 1}"
    Role = "web-server"
  })

  user_data = templatefile("scripts/init.sh", {
    db_host = aws_db_instance.main.endpoint
    app_env = var.environment
  })
}

resource "aws_lb_target_group_attachment" "web" {
  count            = var.instance_count
  target_group_arn = aws_lb_target_group.web.arn
  target_id        = aws_instance.web[count.index].id
  port             = 8080
}

# Modules for reusable infrastructure
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.5.0"

  name = "production-vpc"
  cidr = "10.0.0.0/16"
  azs  = ["us-east-1a", "us-east-1b", "us-east-1c"]

  private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]

  enable_nat_gateway = true
  single_nat_gateway = true
}
// Pulumi: TypeScript — imperative, general-purpose
import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/pulumi";

const config = new pulumi.Config();
const instanceCount = config.getNumber("instanceCount") || 3;

// Use loops, conditionals, functions — real programming
const instances = Array.from({ length: instanceCount }, (_, i) => {
    return new aws.ec2.Instance(`web-${i + 1}`, {
        ami: ubuntu.id,
        instanceType: config.get("instanceType") || "t3.medium",
        tags: {
            ...commonTags,
            Name: `web-${i + 1}`,
            Role: "web-server",
        },
        userData: buildUserData({
            dbHost: database.endpoint,
            appEnv: pulumi.getStack(),
        }),
    });
});

// Attach all instances to target group
instances.forEach((instance, i) => {
    new aws.lb.TargetGroupAttachment(`web-tg-${i}`, {
        targetGroupArn: targetGroup.arn,
        targetId: instance.id,
        port: 8080,
    });
});

// Reusable function — just a TypeScript function
function buildUserData(params: { dbHost: pulumi.Output; appEnv: string }) {
    return pulumi.interpolate`#!/bin/bash
        echo "DB_HOST=${params.dbHost}" >> /etc/app.env
        echo "APP_ENV=${params.appEnv}" >> /etc/app.env
        systemctl start myapp`;
}

HCL is simpler for straightforward infrastructure. You declare what you want, and Terraform/OpenTofu figures out the order. But when you need complex logic — dynamic resource creation based on API calls, conditional configurations, or generating configurations from external data — HCL’s limitations become painful. Pulumi lets you use the full power of your programming language: loops, functions, classes, unit tests, and existing libraries.

There is, however, a hidden cost to that power. Because Pulumi programs are real code, they can also fail in real-code ways — null references, accidental side effects during preview, and dependency drift in your package manager. HCL’s narrowness is partly a feature: there are fewer ways to write something surprising. Teams that adopt Pulumi often find they need software-engineering discipline (linting, tests, code review) applied to infrastructure, which is a benefit for some organizations and overhead for others.

Infrastructure as code deployment pipeline
HCL is simpler for basic infrastructure — programming languages excel at complex, dynamic configurations

State Management: The Operational Burden

All three tools track infrastructure state — a record of what resources exist and their current configuration. State management is where operational complexity lives.

Terraform: State stored in terraform.tfstate (JSON file). For teams, you must configure a remote backend — S3 + DynamoDB for locking is the standard AWS pattern. State locking prevents concurrent modifications. The state file contains secrets in plaintext, so the S3 bucket must be encrypted with restricted access.

OpenTofu: Identical state format and backend system as Terraform. All Terraform backends work with OpenTofu. Client-side state encryption (new in OpenTofu 1.7) encrypts the state file before storing it in the backend, addressing the plaintext secrets problem that Terraform has not solved.

Pulumi: State stored in Pulumi Cloud (free for individuals, paid for teams) or self-managed backends (S3, Azure Blob, GCS). Pulumi Cloud handles locking, encryption, and access control automatically. State is encrypted at rest and in transit. Consequently, teams spend less time managing state infrastructure but depend on Pulumi’s cloud service.

STATE MANAGEMENT COMPARISON:
                     Terraform        OpenTofu         Pulumi
Default Storage:     Local file       Local file       Pulumi Cloud
Remote Backends:     S3, GCS, Azure   S3, GCS, Azure   S3, GCS, Azure, Pulumi Cloud
State Locking:       Backend-specific Backend-specific  Built into Pulumi Cloud
State Encryption:    Backend-level    Client-side(!)    Built-in
Secret Management:   External         External          Built-in (encrypted in state)
Migration:           Manual           Manual            pulumi import
Drift Detection:     terraform plan   tofu plan         pulumi preview

Configuring a Production State Backend

The single most common cause of state corruption is two engineers applying changes at the same time without locking. For HCL-based tools, the canonical fix on AWS is an S3 backend with a DynamoDB lock table and bucket encryption. The configuration below is identical for Terraform and OpenTofu, which is exactly why migration between them is painless.

terraform {
  backend "s3" {
    bucket         = "acme-tfstate-prod"
    key            = "network/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "tf-state-locks"   # advisory lock per state key
    encrypt        = true               # SSE for the object at rest
  }
}

# OpenTofu 1.7+ adds client-side encryption on top of the backend:
terraform {
  encryption {
    key_provider "pbkdf2" "default" {
      passphrase = var.state_passphrase  # never commit this
    }
    method "aes_gcm" "default" {
      keys = key_provider.pbkdf2.default
    }
    state { method = method.aes_gcm.default }
  }
}

A few edge cases catch teams off guard. The DynamoDB table provides advisory locking only, so a force-unlock during a crashed apply can still leave state and reality out of sync. Splitting state into many small backends (per service, per environment) reduces blast radius but multiplies the number of locks and remote-state data sources you must wire together. And because Terraform never encrypted secrets in state, importing an existing Terraform state into OpenTofu and then enabling client-side encryption is a meaningful security upgrade rather than a cosmetic one.

Ecosystem and Provider Support

Terraform has the largest provider ecosystem — over 3,000 providers covering every cloud service, SaaS tool, and infrastructure component imaginable. OpenTofu uses the same providers (forked before the license change), so ecosystem parity is near-complete. Pulumi supports most major providers through Terraform bridge — a compatibility layer that wraps Terraform providers as Pulumi resources. Additionally, Pulumi has native providers for AWS, Azure, GCP, and Kubernetes that offer better TypeScript types and documentation.

The Terraform module registry (registry.terraform.io) is a significant advantage. Thousands of community modules handle common patterns — VPCs, EKS clusters, RDS instances — with battle-tested configurations. OpenTofu can use these modules directly. Pulumi has a smaller module ecosystem but lets you wrap Terraform modules as Pulumi components.

Cloud infrastructure management and deployment
Terraform’s 3,000+ provider ecosystem is shared with OpenTofu — Pulumi bridges most of them

Testing Infrastructure: A Quiet Differentiator

Testing is where the language choice produces the sharpest practical divergence. With Pulumi, infrastructure is ordinary code, so you can unit test it with the same frameworks your application uses — Jest, pytest, or Go’s testing package. You assert on resource properties before anything is provisioned, catching mistakes like an open security group or a missing tag in milliseconds.

// Pulumi unit test with mocks — no cloud calls, runs in CI in seconds
import * as pulumi from "@pulumi/pulumi";

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

test("web instances are not publicly exposed", async () => {
  const infra = await import("./index");
  const sg = await promiseOf(infra.webSg.ingress);
  // fail the build if anyone opens 0.0.0.0/0 to SSH
  expect(sg.some((r) => r.cidrBlocks?.includes("0.0.0.0/0") && r.fromPort === 22))
    .toBe(false);
});

HCL tools take a different route. Terraform’s native terraform test framework and OpenTofu’s equivalent run plan/apply assertions against real or ephemeral resources, while policy-as-code tools such as OPA/Conftest and Sentinel enforce guardrails at plan time. These approaches work well, but they generally validate the generated plan rather than letting you exercise arbitrary logic in fast, isolated unit tests. If your infrastructure encodes substantial conditional logic, Pulumi’s testing story is genuinely easier to live with.

When NOT to Switch Tools

It is worth being blunt about the trade-offs, because tool migrations are rarely free. Do not migrate a large, stable HCL codebase to Pulumi purely for elegance; the rewrite risk and the cost of retraining the team usually outweigh the benefit when the existing configuration is mostly static. Likewise, do not adopt Pulumi if your team has no programming-language depth, because you will simply trade HCL’s quirks for a new class of runtime bugs.

On the other side, do not rush a Terraform-to-OpenTofu switch if you depend on Terraform Cloud features that have no OpenTofu equivalent, or if a specific newer Terraform provider feature has not yet landed in the fork. And avoid running multiple IaC tools against the same resources — overlapping ownership produces drift that is painful to untangle. The honest default for most teams is continuity: change tools when a concrete pain point demands it, not on principle.

Which Should You Choose?

Keep using Terraform when: Your team already knows HCL. You use Terraform Cloud/Enterprise and are satisfied. Your infrastructure is straightforward. The BSL does not affect your use case (it probably does not).

Switch to OpenTofu when: You want open-source guarantee. You use a third-party IaC platform (Spacelift, env0). You want client-side state encryption. You are starting a new project and want to avoid vendor lock-in.

Adopt Pulumi when: Your infrastructure has complex logic that fights HCL’s limitations. Your team is stronger in TypeScript/Python/Go than HCL. You want to unit test your infrastructure code with familiar testing frameworks. You are building a platform where developers self-service infrastructure through code. You want built-in secret management without external tools.

A pragmatic approach: use OpenTofu for existing HCL codebases (it is a drop-in replacement for Terraform), and evaluate Pulumi for new projects where complex logic is needed. You do not need to choose one tool — many organizations use HCL-based tools for platform infrastructure and Pulumi for application-specific infrastructure.

Infrastructure as code tooling comparison
Use OpenTofu as a Terraform drop-in replacement — evaluate Pulumi for complex, logic-heavy infrastructure

Related Reading:

Resources:

In conclusion, the Terraform vs Pulumi vs OpenTofu decision comes down to language preference, testing needs, and licensing requirements. HCL works well for straightforward infrastructure. Programming languages excel at complex, dynamic configurations. OpenTofu is the open-source-safe choice for HCL users. All three are production-ready, well-maintained, and capable of managing infrastructure at any scale — so let your team’s real constraints, not the hype, drive the choice.

← Back to all articles