Why we usually choose Pulumi over Terraform
Pulumi is our default when infrastructure starts behaving like software. Existing Terraform estates can still be the better decision when the migration cost is higher than the operational gain.
For new infrastructure, we usually choose Pulumi when the team already works comfortably in one of its supported languages and expects the platform to contain real reusable logic.
We keep Terraform when an existing estate is coherent, well operated, and not materially constrained by HCL or its module model.
It’s a maintenance decision, not a contest between programming languages and configuration. Both tools manage declarative resource state. The difference is the medium used to build and organize the desired graph.
Pulumi fits infrastructure that needs software structure
Infrastructure often develops concepts larger than one provider resource:
- a standard service with identity, networking, scaling, secrets, alerts, and domain wiring
- a governed dataset with IAM, retention, quality policy, and ownership metadata
- an environment assembled from shared platform components
- policy derived from organizational metadata
- repeated resources generated from typed domain input
Pulumi lets us model those concepts with functions, types, packages, tests, and normal refactoring tools. We can validate inputs before registering resources and give stable platform responsibilities explicit APIs.
That expressiveness helps when the abstraction is real. It also makes bad abstraction easier to build. A TypeScript class with generics and dependency injection can still be a terrible way to create a bucket. A real language removes tool constraints, not the need for taste.
Reviewability matters more than expressiveness
The reason to choose Pulumi isn’t that any logic can be written. It’s that the required logic can remain direct and reviewable.
A conditional can appear where the decision is made. A shared component can have a typed contract. Common naming and IAM policy can be ordinary code. Tests and static analysis can use the team’s established tooling.
Keep the resulting resource graph visible through previews. Avoid dynamic behavior dependent on external mutable systems during deployment. Avoid abstraction layers that hide provider resources without enforcing a useful platform contract.
If reviewers must execute a miniature application mentally to know what will be created, Pulumi’s power has been spent badly.
Terraform remains excellent for stable estates
Terraform has a broad ecosystem, a familiar workflow, and a constrained declarative language that many teams operate effectively. Mature modules, policy checks, CI plans, state boundaries, and organizational fluency are assets worth keeping.
Don’t migrate a healthy estate because greenfield Pulumi code would look nicer. The migration must preserve resource identity, state ownership, delivery controls, provider behavior, and team confidence. That work competes with application and platform improvements that may have clearer returns.
Stay with Terraform when:
- the infrastructure shape is stable
- modules express the required boundaries cleanly
- plans remain understandable
- state and apply paths are disciplined
- the team can refactor without recurring fear
- HCL limitations aren’t creating meaningful delivery cost
Continuity isn’t technical debt when it keeps producing safe change.
State doesn’t disappear
Pulumi and Terraform both depend on state, ownership, locking, and controlled mutation. Switching tools doesn’t repair an organization that allows several writers, keeps huge state boundaries, or treats imports and moves casually.
The principles in How we treat Terraform state in team environments apply to Pulumi backends as well: durable remote state, constrained access, one authoritative update path, recoverability, and boundaries that match lifecycle.
Choose Pulumi for a better programming model where that matters, not to escape state discipline.
Provider coverage is a workload check
Evaluate the exact resources and features the platform needs. Both ecosystems can lag a cloud API, differ in schema behavior, or require a lower-level escape hatch.
Build a small proof for critical resources, imports, replacements, secrets, and preview behavior. Check upgrade cadence and ownership of third-party providers. “Supports GCP” is too broad to settle a production estate.
Use native or generic API providers only when their weaker typing and lifecycle semantics are understood. Don’t let the escape hatch quietly become the default implementation for half the platform.
Language alignment has real value
Using TypeScript, Go, Python, C#, Java, or YAML can let infrastructure share linting, testing, packaging, and review skills with the wider engineering team. That can reduce the number of special conventions a small team carries.
Choose the language the infrastructure maintainers can operate, not the one the application happens to use. A company writing TypeScript doesn’t automatically benefit from TypeScript infrastructure if nobody responsible for production understands asynchronous values, package upgrades, or the component model.
Keep the infrastructure dependency graph conservative. A deployment program doesn’t need the application monorepo’s entire framework ecosystem.
Migration needs a concrete payoff
Consider moving an existing Terraform area when one or more costs are persistent:
- module interfaces are dominated by workarounds and indirection
- policy or generation logic is hard to express safely
- environment variance produces repeated HCL machinery
- refactors and imports consume disproportionate effort
- the team is already maintaining code generators around Terraform
- a new platform boundary can migrate independently
Start with a bounded state whose resources can be imported or transferred with a reversible plan. Preserve functional behavior first. Don’t rewrite the platform while changing its state engine.
Measure whether review time, duplication, defects, and change lead time improve. A migration that produces more elegant source and the same operating pain has mostly changed syntax.
Our default
Use Pulumi for greenfield or actively evolving infrastructure when typed reusable components and normal software tooling make the desired resource graph easier to express and review.
Keep Terraform where it’s already calm. Replace it only when the present delivery cost is large enough to fund state migration, retraining, new pipelines, and a period of reduced confidence.
Pulumi is our default because infrastructure frequently becomes software-shaped. Keep existing Terraform when it already works well.
More in this domain: Infrastructure
Browse allHow we decide between Cloud SQL connectors, Auth Proxy, and private IP
Cloud SQL connectors, the Auth Proxy, and private IP are not interchangeable secure connection options. They change identity, routing, deployment shape, and how much network plumbing the team actually owns.
Safe scaling defaults for Cloud Run + Postgres
Cloud Run autoscaling is not a database strategy. Safe defaults keep the application from scaling itself into a Postgres incident before the team understands the workload.
IAM DB auth for Cloud SQL: when it simplifies security and when it complicates delivery
IAM DB auth can reduce password sprawl and make revocation cleaner, but it also turns database access into an identity operating model that depends on disciplined service-account boundaries.
Cloud Run request timeouts don't kill your code (so your architecture has to)
A Cloud Run request timeout ends the request, not necessarily the work. If the operation can outlive its caller, the system needs explicit job semantics instead of hope.
Cloud Run scaling from zero is a feature until it isn't
Scale to zero is a good default for request-driven services, until startup delay, warm-capacity needs, or instance caps turn it into user-visible reliability behavior instead of a pricing feature.
Related patterns
When repeated Pulumi code earns abstraction and when it doesn't
We don't abstract repeated Pulumi code just because it shows up more than once. We do it when the shared shape is real, the behavior is stable enough to deserve a boundary, and the result is easier to read than the duplication it replaces.
How we decide between directory per environment and shared stacks in Pulumi
We do not force DRY across environments by default. We keep Pulumi environments separate until shared code, shared rules, and drift risk make consolidation cheaper than duplication.
How we structure a directory per environment in Pulumi
When we keep Pulumi environments separate, we make the environment boundary obvious in the filesystem and keep shared logic outside it.
What goes in Pulumi stack config and what doesn't
We use Pulumi stack config for environment-specific values, not as a hiding place for infrastructure logic.