DEV Community

Libme
Libme

Posted on

Terraform vs OpenTofu vs Pulumi: Which One Should a Small Team Actually Commit To?

If your infrastructure is a handful of cloud accounts managed by two to five engineers, all three tools will work, and the deciding factor is almost never the language. Pick Terraform if you want the largest pool of copy-pasteable modules and vendor docs that assume your exact binary. Pick OpenTofu if the BUSL license or HashiCorp's ownership is a procurement or philosophical problem, or if you want client-side state encryption without buying a managed backend. Pick Pulumi only if you have real logic to express — loops over external API data, generated resources, shared code with your application — and you accept that your IaC becomes a program other people have to read.

The rest of this post is the part that actually bites: state handling, the migration path if you change your mind, and where each tool costs money.

How did this become a three-way choice at all?

Terraform was MPL-licensed open source until August 2023, when HashiCorp moved it to the Business Source License (BUSL 1.1). That license restricts production use that competes with HashiCorp's offerings. For most small teams running their own infrastructure, the BUSL grant is fine — you are not a competing IaC vendor. The fork happened anyway: OpenTofu is a Linux Foundation project, a direct descendant of Terraform 1.5.x, and it stayed on MPL 2.0. HashiCorp was subsequently acquired by IBM (the deal closed in early 2025), which is the kind of change that makes legal teams want a written answer.

Pulumi is in a separate category. Same declarative model, same provider ecosystem (it bridges Terraform providers), but your definitions are TypeScript, Python, Go, C#, or Java instead of HCL.

Takeaway: the license split is a real procurement question, not a tribal one — but it only decides the matter if someone in your company actually has to sign off on it.

What breaks first: state, not syntax

Every small team I have worked with hits the same wall in the same order. Not HCL syntax. State.

The classic symptom, usually the first time two CI jobs overlap:

Error: Error acquiring the state lock
Error message: operation error DynamoDB: PutItem, ConditionalCheckFailedException
Lock Info:
  ID:        0b3c9f41-...
  Operation: OperationTypePlan
  Who:       runner@fv-az1234
Enter fullscreen mode Exit fullscreen mode

The dead ends people try, in order: re-run the job (the lock is still held), force-unlock the ID (works, and teaches you a habit that will eventually clobber a real apply), then finally removing the lock table entirely because "locking is the problem." It isn't. The problem is that two pipelines are allowed to touch one state file at once.

Two things fix it properly. First, serialize at the pipeline level — GitHub Actions concurrency groups keyed by the state's identity, so the second run queues instead of racing:

concurrency:
  group: infra-apply-${{ github.ref }}
  cancel-in-progress: false
Enter fullscreen mode Exit fullscreen mode

Second, stop running a separate lock database if you don't need one. Recent Terraform and OpenTofu releases support a lock file inside the S3 bucket itself, which removes the DynamoDB table from your bootstrap entirely:

terraform {
  backend "s3" {
    bucket       = "acme-tfstate"
    key          = "prod/network.tfstate"
    region       = "us-east-1"
    use_lockfile = true
  }
}
Enter fullscreen mode Exit fullscreen mode

Check your version's backend docs before deleting the table — the flag landed in the 1.10-era releases of both tools and the DynamoDB path was still supported alongside it as of mid-2026. Pulumi sidesteps the question by holding locks in its own backend (Pulumi Cloud, or S3/Azure Blob/GCS if you self-manage), which is genuinely less to assemble on day one.

Takeaway: the first real IaC outage in a small team is almost always two concurrent applies, and no tool choice prevents it — pipeline concurrency control does.

Feature comparison that matters at this size

Concern Terraform OpenTofu Pulumi
License BUSL 1.1 MPL 2.0 Apache 2.0 (CLI)
Language HCL HCL (Terraform-compatible) TS/Python/Go/C#/Java
State encryption at rest, client-side via backend/KMS built-in (encryption block) via backend / provider
Managed backend HCP Terraform third-party or self-hosted Pulumi Cloud
Pricing model (paid tier) per resource under management no first-party paid tier per resource/credits + per user
Vendor docs assume it almost always usually compatible rarely
Testing terraform test tofu test host language test frameworks

OpenTofu's state encryption is the one feature difference I would actually change a decision over. If your state contains generated passwords or connection strings — and it does, because state records every attribute — being able to encrypt the file client-side without a managed vendor is worth something:

terraform {
  encryption {
    key_provider "aws_kms" "primary" {
      kms_key_id = "arn:aws:kms:us-east-1:111122223333:key/abcd-1234"
      key_spec   = "AES_256"
    }
    method "aes_gcm" "default" {
      keys = key_provider.aws_kms.primary
    }
    state {
      method = method.aes_gcm.default
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

If you want the managed version of all of this — remote runs, policy enforcement, a private module registry — HCP Terraform is the option that works out of the box with the stock binary and no third-party backend assembly. Its cost scales with resources under management rather than seats, which is worth modeling before you commit: a team that manages thousands of small resources can pay more than a larger team managing a few big ones. Pulumi Cloud bundles state, secrets, and RBAC with a free individual tier, and is the fastest path from zero to a locked remote backend.

Takeaway: choose Terraform or OpenTofu on licensing and backend strategy; choose Pulumi on whether your team wants to maintain a program instead of a configuration.

Can you switch later without a rewrite?

Mostly, and this is why the decision is less terrifying than it looks.

Terraform to OpenTofu is a binary swap for configurations originating in the 1.5.x lineage. The documented migration is: pin your versions, back up state, then run the tofu binary against the same directory.

aws s3 cp s3://acme-tfstate/prod/network.tfstate ./backup.tfstate
tofu init -upgrade
tofu plan -detailed-exitcode   # exit 0 = no changes; that is the result you want
Enter fullscreen mode Exit fullscreen mode

An empty plan is the entire acceptance test. If you see unexpected replacements, stop and inspect provider versions before applying — provider constraint resolution, not the fork, is the usual culprit.

Going the other direction, or adopting newer Terraform features first, is where it gets sticky: configurations written against post-1.6 Terraform-only features have no guaranteed OpenTofu equivalent, and HCP Terraform does not run the tofu binary. Treat "we can always fork back" as true today and unverified for whatever you write next year.

Pulumi is a one-way-ish door with a real on-ramp: pulumi convert --from terraform translates HCL, and Pulumi can adopt existing resources and read Terraform state during migration. Expect to hand-fix the output. There is no comparable automated path back to HCL.

Takeaway: HCL-to-HCL moves are cheap and reversible; HCL-to-Pulumi is a port you schedule, not a flag you flip.

Where each one genuinely annoys you

Terraform's drawback is governance, not engineering: the license is a standing question you re-answer every time legal or an acquirer asks, and the paid tier's resource-based metering can surprise you as resource counts grow.

OpenTofu's drawback is ecosystem friction. The code is compatible; the surrounding world is not always. Vendor quickstarts say "run terraform init", CI actions and pre-commit hooks default to the Terraform binary, and third-party tooling occasionally pins Terraform version strings. None of it is blocking, all of it is small ongoing tax.

Pulumi's drawback is that general-purpose languages let you build things nobody can review. A plan diff is still a diff, but the code that generated it can be four layers of abstraction deep. Teams that succeed with Pulumi are strict about keeping stack code boring.

FAQ

Is OpenTofu a drop-in replacement for Terraform?
For configurations and state from the Terraform 1.5.x lineage, yes — swap the binary, run tofu init -upgrade, and confirm an empty plan. Configurations that depend on Terraform features added after the fork are not guaranteed to work, so verify with a plan before applying.

Do I need a DynamoDB table for Terraform state locking?
Not with current versions. The S3 backend supports a lock file in the bucket via use_lockfile = true, which covers single-bucket setups without a separate table. Confirm the flag in your installed version's backend documentation first.

Is Pulumi worth it over Terraform for a small team?
Only if you need real programming constructs — generating resources from external data, sharing types with application code, or testing infrastructure with your language's test framework. If your infrastructure is static resources with a few variables, HCL is less code and more reviewable.

Bottom line

Default to Terraform if nobody in your organization is asking about the license: the ecosystem gravity is real and you will spend less time translating docs. Switch to OpenTofu if licensing is a live question or you want built-in client-side state encryption without a managed backend, and accept the small tax of tooling that assumes the other binary. Reach for Pulumi when your infrastructure has logic in it, not just values. Whichever you choose, fix concurrency in your pipeline on day one — that, not the tool, is what breaks first.

Related reading

Top comments (0)