Journal — November 26, 2025 · 5 min read
Terraform vs Pulumi for Small Teams
A head-to-head on HCL versus real programming languages for infrastructure, covering state, providers, drift, the OpenTofu licence split, and a decision procedure for teams under ten people.
The strongest argument for Terraform sounds like an insult: HCL is too stupid to let you be clever. For a small team, that's the whole ballgame.
I've run both. Pulumi is the better-designed tool by most engineering measures, and I still reach for HCL on almost every project under ten people. Here's why, and where the exceptions are.
HCL's limitations are the product
HCL has no classes, no functions you can define, and loops so restricted that for_each over a map is the sophisticated end of the language. People treat this as a deficiency. It's the constraint that makes infrastructure code reviewable.
When I open a Terraform module I've never seen, I can read it top to bottom and know what will exist, because the resources are declared literally. The blast radius of a change shows up in terraform plan as a list of nouns.
The ceiling is real. Anything dynamic gets ugly fast, and the gymnastics for faking conditionals (count = var.enabled ? 1 : 0, then indexing [0] downstream) are bad. But the ugliness is a signal: when HCL fights you, it's usually telling you you're generating infrastructure rather than declaring it, and that's where Pulumi wins.
What Pulumi is actually good at
Pulumi lets you write infrastructure in TypeScript, Python, Go or C#. Real loops, real abstraction, real types, and an editor that knows a resource's properties without a DSL-specific plugin.
Two situations where this isn't a preference but a clear win:
You're generating infrastructure from data. A per-customer stack, a per-branch preview environment, a fleet of tenants defined in a database. In Pulumi that's a function you call in a loop. In HCL it's a code generator emitting .tf files, which is the same thing with an extra build step.
Your team has no ops person and lives in TypeScript. If the people writing infrastructure are product engineers who've never touched HCL, a language they already know removes a real barrier. They'll ship VPCs. Badly at first, but they'll ship them.
The Automation API is genuinely useful too: driving Pulumi from your own service is something Terraform only does awkwardly.
The abstraction trap
Here's the failure I keep watching happen, and I've caused it.
Someone capable writes a NetworkStack class. Config object, sensible defaults, three environments, genuinely elegant. Six months later you need one security group rule in staging only, and to find where it goes you read a class hierarchy, a factory function, and a config merge that resolves defaults in an order nobody wrote down.
Then that person leaves. Nobody can confidently change the network now, because the network isn't described anywhere, it's computed. A Pulumi program is a program, and programs accrete the complexity all programs do. Infrastructure is where you least want that: a mistake shows up as an outage, not a failing test.
My version was smaller and still annoying: a helper that built IAM policies by composing arrays. It removed a lot of repetition, and coming back months later I couldn't tell which principals ended up with what without executing it. HCL would have made me write it out, and writing it out would have been correct.
The general form: HCL's repetition is a cost you pay once, at write time. Pulumi's abstraction is a cost you pay at read time, forever, by everyone. Small teams read more than they write.
State, and the part both get wrong
Both keep a state file mapping code to real resource IDs, and both need it stored remotely and locked so two engineers can't apply at once.
Terraform's S3 backend with native locking is boring and works; Terraform Cloud and Spacelift do it as a service. Pulumi offers a managed service, free for individuals and priced per resource beyond that, or a self-managed S3 backend that gives up the diff UI.
State is the actual database of your infrastructure and deserves versioning, backups and restricted access. Every horror story here is a state file story: someone ran apply locally against prod, someone lost the file, someone committed it to Git with credentials inside. Neither tool protects you. Process does.
Providers, and who writes them
This is less symmetric than the marketing suggests. Pulumi's AWS, Azure and GCP providers are largely bridged from the Terraform ones, so the upstream work happens in the Terraform ecosystem and Pulumi tracks it.
For major clouds, parity is close enough that you won't notice. For a niche provider (some SaaS with a community-maintained Terraform provider and three hundred GitHub stars), the Terraform one will be more current. Check yours before committing.
Drift and importing what already exists
Drift handling is a wash. Both detect it on refresh, neither fixes the cultural problem, which is a person clicking in the console at 1am during an incident. The only answer is removing console write access in production.
Import is where Terraform pulled ahead. Import blocks with generated configuration turn "adopt this existing VPC" into a plan you can read and edit, rather than a hand-written resource you re-run until the diff is empty. Pulumi's import emits code too, but the Terraform side has been smoother for me.
The licence question
HashiCorp moved Terraform to the BSL, OpenTofu forked from the last MPL version under the Linux Foundation, then IBM acquired HashiCorp. If you're a small team not building a competing product, the BSL doesn't restrict you. But part of the ecosystem moved, OpenTofu ships features Terraform doesn't (state encryption, early variable evaluation), and switching is close to a binary swap.
My position: write HCL and treat which binary executes it as a decision you can defer. That optionality is an argument for the boring option. Pulumi is one vendor with one implementation.
How I'd decide
Ask in order, stop at the first yes:
- Are you generating infrastructure programmatically (per-tenant, per-branch, driven by data at runtime)? Pulumi.
- Is there literally nobody on the team who will learn HCL, and everyone lives in TypeScript? Pulumi, and mandate that stacks stay flat and boring.
- Otherwise? HCL, on OpenTofu or Terraform, with no module nested more than two levels deep.
Whichever you pick, enforce one rule about cleverness: if a reviewer can't tell what resources a change creates by reading the diff, the change is wrong regardless of how good the code is. Terraform enforces that for you. With Pulumi you enforce it yourself, and teams under ten people rarely have the discipline. That's not a knock on the tool. It's a knock on all of us at 6pm on a Thursday.