Pulumi vs Terraform: Code vs Declarative Config for IaC
Pulumi defines infrastructure in general-purpose languages like TypeScript and Python; Terraform uses its own declarative HCL. Here's how they differ.
Pulumi and Terraform are both infrastructure-as-code tools that let you define cloud resources in files, diff them against reality, and apply changes reproducibly — but they take opposite approaches to how you express that infrastructure. Terraform uses its own declarative configuration language, HCL, purpose-built for describing resources and their relationships. Pulumi lets you write the same kind of infrastructure definitions in a general-purpose language — TypeScript, Python, Go, C#, or others — using loops, conditionals, and functions the way you would in application code.
Same core model, different input language
Underneath the surface syntax, both tools work the same way: you declare the desired state of your infrastructure, the tool compares it against a state file recording what it last provisioned, computes a diff, and applies only the changes needed to reconcile the two. Both support the same category of provider ecosystem — AWS, Azure, GCP, Kubernetes, and hundreds of smaller providers — and both persist state that has to be stored and locked carefully, a topic covered in more depth in how Terraform’s state file works, which applies conceptually to Pulumi’s state backend too.
Where they genuinely diverge is what you write to describe that desired state.
| Terraform | Pulumi | |
|---|---|---|
| Language | HCL (purpose-built DSL) | TypeScript, Python, Go, C#, Java, YAML |
| Loops and conditionals | Limited (count, for_each, expressions) | Full language features |
| Reusability | Modules | Native functions, classes, packages |
| Testing | Plan output review, third-party test frameworks | Standard unit/integration testing in the host language |
| State backend | Terraform Cloud, S3 + DynamoDB, others | Pulumi Cloud, self-managed backends (S3, Azure Blob, GCS) |
| Ecosystem maturity | Larger, older, most widely adopted | Smaller, newer, growing fast in TS/Python shops |
| Learning curve | New DSL to learn | Reuses a language the team likely already knows |
Why the language choice actually matters
HCL was designed to be declarative and predictable — read a .tf file and you can reason about what it provisions without worrying about side effects, hidden state mutation, or a loop that behaves differently depending on input. That predictability is a deliberate constraint: HCL intentionally limits what you can express (no arbitrary function definitions, no full control flow) because infrastructure code that’s too clever is infrastructure code that’s hard to audit.
Pulumi’s bet is the opposite: infrastructure logic often does need real programming — generating a resource per entry in a data-driven list, computing a name from several inputs, sharing logic across environments through actual functions and classes instead of module indirection — and forcing that into a DSL not designed for general computation produces its own kind of awkwardness (Terraform’s for_each and nested dynamic blocks exist precisely to claw back some of that expressiveness). Writing infrastructure in TypeScript or Python also means it can use the same linters, type checkers, and test frameworks the team already runs against application code, rather than a separate toolchain built around .tf files.
Where Terraform’s constraints are a feature
The tradeoff is real, not one-directional. HCL’s limited expressiveness makes Terraform configuration easier to review in a pull request — a reviewer without deep familiarity with the codebase can usually tell what a .tf change provisions just from reading it, since there’s no hidden control flow to trace through. It’s also why Terraform’s larger, older ecosystem matters in practice: more existing modules, more Stack Overflow answers, more engineers who already know HCL from a previous job, and closer parity between community providers and official ones. For an infrastructure team that wants configuration to look uniform no matter who wrote it, HCL’s restrictiveness is doing useful work, not just getting in the way.
Testing infrastructure code
The language difference also shows up sharply in how each tool’s configuration gets tested before it’s applied against real cloud resources. Terraform’s primary safety net is the plan output — running terraform plan shows exactly what will change before anything is applied, and teams typically review that diff in a pull request the same way they’d review an application code change, sometimes backed by policy-as-code tools that reject plans violating organizational rules. Pulumi supports the equivalent plan-and-review workflow, but because its infrastructure definitions are ordinary functions in a general-purpose language, they can also be unit tested directly — asserting that a function which builds a set of resources produces the configuration you expect, without ever touching a cloud provider, using the same test runner the team already uses for application code.
Picking one for a real team
The strongest signal is usually the team’s existing language fluency rather than either tool’s feature list. A team of backend engineers already writing TypeScript or Python who want infrastructure code to feel like the rest of their codebase — testable, composable, using the same CI checks — tends to be more productive in Pulumi. A platform or infrastructure team that wants configuration to be uniformly declarative and reviewable by people who don’t write TypeScript day to day, or that’s already invested in Terraform modules and GitOps pipelines built around the HCL ecosystem, has less reason to switch.
Neither choice is permanent lock-in at the infrastructure level — both ultimately call the same cloud provider APIs, so migrating provisioned resources between them (rather than rewriting configuration from scratch) is possible via state import, though it’s real work, not a flag flip. The bigger cost of switching later is retraining the team on a new authoring model, not migrating the resources themselves.
The takeaway
Pulumi and Terraform solve the same infrastructure-as-code problem — declare desired state, diff against a state file, apply the difference — but Pulumi expresses that state in a general-purpose language your team may already know, while Terraform expresses it in HCL, a DSL deliberately constrained to stay predictable and reviewable. Teams that want infrastructure code to behave like application code lean toward Pulumi; teams that want infrastructure code to stay uniformly declarative, and benefit from a larger existing ecosystem, tend to stay with Terraform.
Tagged
Keep reading
The Lycoris Team · · 4 min read Deployment Rollback Strategies: Roll Back vs Forward
Rolling back reverts to the last known-good deploy; rolling forward ships a fix on top of the bad one. How to choose, and why database changes complicate both.
The Lycoris Team · · 5 min read Terraform State File Explained: What It Is, Why It Matters
Terraform's state file maps your config to real infrastructure. How it works, why remote state and locking matter, and what causes state drift.
The Lycoris Team · · 4 min read What Is an Internal Developer Platform (IDP)?
An internal developer platform packages infrastructure into self-service tools so developers ship without filing tickets or learning Kubernetes.