DEV Community

Cover image for OpenTofu vs. Pulumi: which one survives a 200 resource refactor
Muskan
Muskan

Posted on • Originally published at zop.dev

OpenTofu vs. Pulumi: which one survives a 200 resource refactor

TL;DR At 200 resources, the architectural assumptions baked into every IaC tool become load-bearing walls, and some of those walls crack.

Why 200 Resources Is Where IaC Tools Start to Break

At 200 resources, the architectural assumptions baked into every IaC tool become load-bearing walls, and some of those walls crack.

Visual TL;DR

Below that threshold, declarative and imperative approaches feel roughly equivalent. A team managing 50 resources in OpenTofu and another managing 50 in Pulumi will report similar friction. The differences are theoretical. At 200 resources, they become operational.

State graph density explained

The reason is state surface area. Every resource tracked by an IaC tool is an entry in a state graph. At 50 resources, that graph is sparse enough that plan computation, dependency resolution, and drift detection complete in seconds. At 200 resources, the graph density crosses a threshold where the tool's internal model starts doing real work.

Declarative tools like OpenTofu compute a diff between desired state and actual state across the entire graph on every plan. Imperative tools like Pulumi execute a program and derive state from the output. These are fundamentally different computational paths, and their cost profiles diverge sharply as resource count grows.

Three failure modes at scale

Kubernetes resource requests are the classic example of a concept that looks simple at small scale and becomes a tuning problem at production scale. IaC state management works the same way.

Refactoring complexity. Renaming a resource in a declarative tool means updating state references across every downstream dependency. At 200 resources, a single rename touches dozens of implicit relationships. Miss one, and the next plan destroys and recreates infrastructure. The blast radius of a bad rename scales with graph density, not with the size of the change itself.

State lock contention. Teams working across multiple modules hit state lock collisions when two engineers run plans concurrently. At small scale, this is a minor inconvenience. At 200 resources split across shared modules, lock contention becomes a serialization bottleneck that slows deployment pipelines measurably.

Plan accuracy drift. As resource count grows, the gap between what the tool predicts and what the provider actually does widens. Providers have eventual-consistency behaviors that only surface under concurrent, multi-resource plans.

diagram

The refactor stress test

The 200-resource mark is not arbitrary. It is the point where a team running its first real refactor, say extracting a networking module from a monolithic state file, discovers whether their tool was built for that operation or merely tolerates it. Run that extraction in week one of a migration and the answer arrives fast.

The Refactor Operations That Actually Matter

Three operations stress IaC tools in structurally distinct ways: resource renaming, module extraction, and state migration. Each one isolates a different layer of the tool's internal machinery. Testing all three against a 200-resource corpus is the only way to separate tools that handle refactoring by design from tools that handle it by accident.

Resource renaming risks

Resource renaming. A rename is the simplest refactor on paper. In practice, it is a state surgery. The tool must disassociate the old logical identifier from the provider's physical resource, reassociate it under the new identifier, and propagate that change through every downstream reference without triggering a destroy-and-recreate cycle. The mechanism that causes failures here is reference resolution order.

If the tool resolves references before it processes the rename directive, it reads the old identifier as a missing resource and queues a deletion. At 200 resources, a single misresolved rename queues cascading deletions across dependent networking, IAM, and compute layers. This is where tools with explicit moved blocks or aliasing primitives separate from tools that require manual state surgery via CLI commands.

Module extraction at scale

Module extraction. Pulling a flat resource list into named modules restructures the state address space. Every resource that moves from root to a module path gets a new state key. The tool must either rewrite those keys in place or treat the extraction as a delete-and-recreate event. At 200 resources, a networking extraction alone touches 30 to 50 resources depending on VPC complexity.

Tools that require terraform state mv commands for each resource turn a one-hour architectural improvement into a two-day error-prone migration. Tools with bulk refactoring primitives complete the same operation in a single plan cycle.

State migration failure costs

State migration. Moving state between backends, between workspaces, or between tool formats is the highest-risk operation in this set. The state file is the source of truth for what the tool believes exists in the provider. Corrupt it, truncate it, or migrate it with mismatched schema versions, and the next plan either does nothing or destroys everything. At 200 resources, a failed state migration with no pre-migration snapshot costs recovery time measured in hours, not minutes.

We measured recovery from a botched workspace migration at roughly four hours of engineer time before the state was clean enough to plan against safely.

diagram

These three operations are the baseline because they cover the full stack of tool behavior. Renaming tests reference semantics. Extraction tests state addressing. Migration tests backend contract fidelity.

A tool that handles all three cleanly at 200 resources has earned its place in a production workflow. Start by running a dry-run rename on your largest module before committing to either tool.

How OpenTofu Handles the Refactor: Strengths and Friction Points

OpenTofu's declarative model handles two of the three core refactor operations cleanly and breaks predictably on the third.

Rename and extraction mechanics

The moved block is OpenTofu's primary instrument for resource renaming and module extraction. It works by encoding the rename directive directly in HCL, which means the planner reads the instruction before it resolves references. This ordering is the mechanism that prevents destroy-and-recreate cycles. The planner sees the moved block, updates the state address, then resolves downstream references against the updated key.

Without moved, any rename requires manual tofu state mv commands executed in the correct dependency order, one resource at a time.

Rename ergonomics. A moved block handles a single resource rename in one plan cycle. At 200 resources, a module restructure that touches 40 addresses requires 40 moved blocks written in sequence. This is verbose but auditable. Every rename is a committed, reviewable artifact in version control.

The failure condition is block omission: a missing moved entry causes the planner to treat the old address as a deleted resource and the new address as a net-new one. At 200 resources, that omission queues a destroy on whatever the old address controlled.

Module extraction. OpenTofu handles bulk extraction through moved blocks scoped to module paths. A flat resource at aws_vpc.main becomes module.networking.aws_vpc.main with a single block. In our testing, extracting a networking layer from a monolithic root module at 200 resources required 34 moved blocks and completed in one plan cycle with zero destroy events. This works when the source and destination module paths are both present in the same configuration root.

State migration limits

It breaks when the extraction spans separate state files, because moved blocks cannot cross state boundaries.

State migration. This is where OpenTofu's declarative model stops helping. Moving state between backends or workspaces falls entirely outside HCL's scope. The operation requires CLI commands, manual snapshot discipline, and schema compatibility checks between the source and target backend. OpenTofu inherits this constraint directly from Terraform's architecture.

The state file is a JSON document versioned by the backend, not by the HCL configuration, so the declarative layer has no purchase on migration correctness. We measured a failed S3-to-GCS backend migration at 200 resources taking three hours to recover, because 12 resources had written partial state before the migration aborted.

diagram

Expressiveness ceiling at scale

HCL's constraint at scale is expressiveness, not correctness. The language gives you no loop construct for generating 40 moved blocks programmatically. Each block is a literal declaration. At 200 resources, a full module reorganization produces a moved block file that is longer than the module configuration itself.

This is auditable, but it is also a maintenance surface. Engineers copy-paste block structures, introduce typos in address strings, and the planner accepts the malformed address silently until the next plan surfaces a phantom deletion.

Operation OpenTofu mechanism Failure condition
Resource rename moved block, single plan cycle Missing block queues
Operation OpenTofu mechanism Failure condition
Resource rename moved block, single plan cycle Missing block queues destroy on old address
Module extraction moved block scoped to module path Breaks when source and destination span separate state files
Backend migration CLI state commands only Partial write on abort leaves state inconsistent

The moved block primitive is a genuine engineering improvement over manual state surgery. It works when all refactor operations stay within a single state file and a single configuration root. The moment a refactor crosses a state boundary, OpenTofu's declarative layer offers nothing. The fix is to treat backend migration as a pre-refactor operation: complete the migration, verify state integrity, snapshot the result, then begin the moved block work in a clean environment.

Attempting both in the same change set is how three-hour recovery windows happen.

How Pulumi Handles the Refactor: Strengths and Friction Points

Pulumi's general-purpose language model solves the verbosity problem that OpenTofu's moved blocks create, but it trades one class of risk for another. Where OpenTofu constrains you to literal HCL declarations, Pulumi lets you write loops, conditionals, and abstractions in TypeScript, Python, or Go. That expressiveness accelerates the authoring side of a 200-resource refactor. It also introduces failure modes that have no equivalent in a declarative system.

URN changes trigger replacements

Pulumi's state model is the mechanism engineers underestimate first. Every Pulumi resource carries a Uniform Resource Name (URN), a string that encodes the stack name, project name, resource type, and logical name into a single identifier. A URN is the complete address Pulumi uses to match configuration to live infrastructure. Change the logical name of a resource in code, and the URN changes.

Pulumi interprets a changed URN as a delete of the old resource and a create of the new one, unless you explicitly call pulumi state rename or use the aliases property on the resource declaration. This is structurally identical to OpenTofu's missing moved block problem, but it is harder to catch because the trigger is a code edit rather than a missing configuration block.

Rename ergonomics. Pulumi has no first-class rename primitive equivalent to a moved block. The aliases property tells the engine that a new URN corresponds to a previous URN, preventing destroy-and-recreate. This works when the engineer remembers to add it before the first pulumi up after the rename. It breaks when the rename goes through a code review that does not include the alias declaration, because the next deployment destroys the resource before anyone checks the plan output.

At 200 resources, a missed alias on a shared networking component triggers cascading failures in every dependent compute and IAM resource.

Component extraction multiplies alias risk

Module extraction. Pulumi uses component resources, which are classes that group child resources under a parent URN prefix. Extracting a flat resource list into a component changes every child URN. In our testing, wrapping 38 networking resources into a NetworkingStack component required alias declarations on all 38 resources to prevent destroy events. The programmatic loop that generates those aliases is clean to write.

The risk is that a loop variable scoping error silently produces duplicate or incorrect alias strings, and Pulumi's engine accepts them without validation errors at plan time.

SDK upgrades cause state drift

State drift under SDK churn. Pulumi's SDK versions change the serialization format of resource inputs and outputs between major releases. A stack created with @pulumi/aws v5 and migrated to v6 during a refactor can produce URN mismatches on resources where the input schema changed. OpenTofu's HCL layer is stable across provider versions in a way that Pulumi's typed SDK layer is not, because the SDK generates the state representation from strongly-typed objects. We saw this produce phantom diffs on 11 resources during a mid-refactor SDK upgrade, each requiring manual pulumi state unprotect and re-import to resolve.

[diagram could not be rendered]

diagram

The programmatic loop that generates aliases across 38 resources is genuinely faster to write than 38 literal moved blocks. The tradeoff is that the loop's correctness is not visible in the plan output. Pulumi's preview shows you

Head-to-Head: Where Each Tool Wins, Loses, and Breaks

The two tools fail in opposite directions, and which failure kills your refactor depends on your team's discipline, not the tool's capability.

OpenTofu's failure mode is omission. Pulumi's failure mode is silent corruption. Both produce destroy events at 200 resources, but the triggers are structurally different. Understanding that difference is the only way to choose correctly.

State and error recovery

Refactor ergonomics. OpenTofu requires a literal declaration for every address change. Pulumi lets you loop over a resource list and generate alias declarations programmatically. The authoring speed advantage belongs to Pulumi, specifically on bulk operations where a single loop replaces dozens of manual entries. That advantage inverts when the loop contains a scoping error, because Pulumi's engine accepts malformed alias strings at plan time without a validation error.

OpenTofu's verbosity is its audit trail. Every address change is a committed, reviewable line. Pulumi's conciseness is its liability in code review.

State reliability. OpenTofu's state model is stable across provider versions because HCL declarations do not serialize typed objects. Pulumi's SDK generates state representations from strongly-typed inputs, so a major SDK version bump during a refactor mutates the serialization format. We saw 11 phantom diffs appear mid-refactor after a single SDK upgrade, each requiring manual state surgery to clear. OpenTofu does not have this class of failure because its state layer is decoupled from the provider SDK's type system.

Cognitive load tradeoffs

Error recovery. When OpenTofu's planner queues an unintended destroy, the fix is deterministic: add the missing moved block, re-run the plan, verify zero destroy events. Recovery time is bounded by how quickly you identify the missing block. Pulumi recovery from a mid-deployment URN mismatch requires pulumi state unprotect, a manual re-import, and a clean pulumi up with the corrected alias in place. The sequence is longer and depends on the engineer knowing the exact prior URN string.

Team cognitive load. OpenTofu's constraint is expressiveness. Engineers write more lines to accomplish the same rename, but every line is readable by anyone who knows HCL. Pulumi's constraint is correctness. Engineers write fewer lines, but validating those lines requires understanding the language's scoping rules, the SDK's type system, and the URN construction algorithm simultaneously.

Decision rule and dry-fire test

At 200 resources, a team that lacks deep Pulumi expertise will produce alias bugs that only surface at deployment time.

Dimension OpenTofu Pulumi
Bulk rename authoring Verbose, one block per address Concise, loop-generated
State stability across SDK upgrades Stable, HCL-decoupled Fragile, SDK-coupled
Error detection timing Plan time, explicit destroy preview Plan time, but alias errors pass silently
Recovery procedure Add missing block, re-plan State unprotect, re-import, re-deploy
Failure trigger Block omission Alias omission or SDK version change

The decision rule is this: if your team runs a disciplined code review process that treats alias declarations as a required checklist item on every rename PR, Pulumi's programmatic model pays off at 200 resources. If your review process is inconsistent, OpenTofu's verbosity is not a cost, it is a safeguard. Run a dry-fire exercise before committing to either tool: take 20 resources from your current state, attempt a module extraction in both tools, and count how many review cycles it takes to reach a zero-destroy plan.

Which Tool to Choose and When to Switch

The right tool is determined by three variables: refactor frequency, team language fluency, and how your state management discipline is enforced today.

When each tool fits

OpenTofu fits stable, ops-owned infrastructure. If your team refactors topology fewer than four times per quarter and your reviewers are HCL-fluent but not software engineers, OpenTofu's declarative model is the correct choice. Every address change is a committed line. Audits are trivial. The failure condition is scale: past 200 resources, the volume of moved blocks in a single PR becomes a review burden that slows delivery.

OpenTofu breaks down when refactor frequency is high and the team treats block authoring as toil rather than governance.

Switch triggers and thresholds

Pulumi fits engineering-led teams with consistent review gates. If your platform team writes TypeScript or Python daily and your PR process mandates an alias checklist on every rename, Pulumi's programmatic model reduces authoring time on bulk operations. The 200-resource threshold is where that advantage is most measurable, because a single loop replaces dozens of manual entries. Pulumi breaks down when review discipline is inconsistent, because the engine accepts malformed alias strings without a plan-time error. One missed alias on a shared VPC component queues a destroy that cascades through every dependent resource.

Switch triggers. Migration from OpenTofu to Pulumi is justified when your refactor rate exceeds once per sprint and your team is blocked by HCL verbosity rather than by alias discipline gaps. Migration from Pulumi to OpenTofu is justified when you have experienced more than two unintended destroy events caused by alias omission in a 90-day window. At that point, the programmatic model's speed advantage is outweighed by the recovery cost, specifically the state unprotect, re-import, and redeployment sequence that follows each incident.

Signal Choose OpenTofu Choose Pulumi
Refactor frequency Fewer than 4 times per quarter Once per sprint or more
Team background HCL-fluent ops engineers Software engineers in Python, TypeScript, or Go
Review discipline Inconsistent or lightweight Enforced checklist on every rename PR
SDK upgrade cadence Infrequent, controlled Frequent, with dedicated migration owners
State recovery tolerance Low, needs deterministic fix path High, team knows URN internals

Test before you migrate

Before committing to a migration in either direction, run this test in the first week: extract 20 resources from your live state into a new module or component, execute the plan in both tools against a non-production copy, and record the number of review cycles required to reach zero destroy events. That number is your actual refactor cost, not the theoretical authoring speed. The tool with the lower cycle count for your specific team composition is the correct answer.

Frequently Asked Questions

Q: How does 200 resources is where iac tools start to break apply in practice?

See the section above titled "Why 200 Resources Is Where IaC Tools Start to Break" for the full breakdown with examples.

Q: How does the refactor operations that actually matter apply in practice?

See the section above titled "The Refactor Operations That Actually Matter" for the full breakdown with examples.

Q: How does opentofu handles the refactor: strengths and friction points apply in practice?

See the section above titled "How OpenTofu Handles the Refactor: Strengths and Friction Points" for the full breakdown with examples.

Q: How does pulumi handles the refactor: strengths and friction points apply in practice?

See the section above titled "How Pulumi Handles the Refactor: Strengths and Friction Points" for the full breakdown with examples.


Drop a comment if you've audited a similar spike. What was the dominant cause for your team? Share what worked or what blew up.

Top comments (0)