Pulumi: When HCL Starts Fighting You, Use a Real Language
Terraform is still the right default. But there is a specific class of problem where HCL fights back — and a real programming language quietly wins.
I do not think anyone should rewrite a working Terraform estate because a blog told them to. I also watched a team spend three weeks expressing a simple requirement — "every subnet gets a route table unless it's tagged otherwise" — in count expressions, dynamic blocks, and two modules that existed only to work around a third.
That is the boundary. HCL is a configuration language, and it is very good at declaring things. It becomes painful when you need it to compute.
What Pulumi actually is
Pulumi provisions cloud resources using a normal language — TypeScript, Python, Go, C#, YAML if you must. You write a program, it calls a provider, and it records what it created in state. There is a preview that behaves like plan, a backend that holds state, and stacks that map to environments.
import * as aws from "@pulumi/aws";
const bucket = new aws.s3.Bucket("logs", {
acl: "private",
tags: { team: "platform", managedBy: "pulumi" },
});
export const bucketName = bucket.bucket;
Nothing mystical. The mental model transfers directly — the difference is what you can do around the resource declarations.
Where a real language wins
Conditionals that are actually conditionals. In HCL you express branching with count, for_each, and dynamic blocks, then debug why an index shifted. In TypeScript it is an if.
Loops over real data. Mapping over a JSON file, a list of service names, or an API response does not require for expressions and a locals block you have to read twice.
Shared logic without module gymnastics. A helper function is a helper function. You do not need a module with nine input variables to compute a naming convention.
Unit tests. This is the big one. You can assert that a security group does not expose port 22 without provisioning anything:
import { strict as assert } from "node";
assert.ok(
ingressRules.every((r) => r.fromPort !== 22),
"port 22 must not be publicly reachable",
);
That test runs in milliseconds in CI. Terraform has testing features now, but they are a framework bolted onto a language rather than the language's natural mode.
Debugging. console.log, a real debugger, stack traces with line numbers. When a Terraform plan is wrong you read the plan; when a Pulumi program misbehaves you step through it.
Where Terraform still wins
Plan review. A terraform plan is a declarative diff a reviewer can evaluate without executing anything. A Pulumi program is imperative — to know what it will do you are, in a meaningful sense, running it. pulumi preview helps, but the trust model is different, and in a change-controlled environment that difference matters.
Ecosystem gravity is the other one. Every platform team already knows HCL, every vendor ships Terraform modules, and hiring is easier — and the OpenTofu-versus-Terraform argument shows how much energy tool selection absorbs. Adopting a second IaC tool has a real organizational cost that does not show up in the first proof of concept.
Drift and state workflows are comparable, but Terraform's are more familiar to your eventual on-call.
The migration that does not require a rewrite
You do not have to choose wholesale. Two practical entry points:
Start with a new project. Greenfield services use Pulumi; the existing estate stays in Terraform. No conversion, no risk, and you get an honest comparison over two quarters instead of an argument.
Run Terraform from Pulumi. The pulumi ecosystem can wrap existing Terraform modules and providers, so a team can adopt the language without touching the configuration underneath. It is a slower path and a reasonable one.
What I would not do is convert a stable estate for its own sake. Migration cost is real, and "principled" does not patch production.
The honest decision
Reach for Pulumi when your infrastructure logic has grown branches — multi-region fan-out, per-team customization, generated configurations, tests you actually want to run. Stay on Terraform when your estate is mostly declarative and your problem is process, not expression.
Most teams are in the second group. The ones writing dynamic blocks to simulate if statements are in the first, whether or not they have named it yet.
Summary
Terraform remains the sensible default for its ecosystem and its review model. Pulumi wins where infrastructure logic needs computation, reuse, and tests — the point where configuration stops being configuration. The mature answer is not a conversion project; it is choosing per project and keeping both in the toolbox.
SDP Clouds Team
DevOps and cloud engineers writing practical, battle-tested guides on CI/CD, Kubernetes, infrastructure as code, and production operations — every article is based on real incidents and real pipelines, not docs-page rewrites.
More about us →