April 10, 2018·4 min read·engineeringterraformplansproviders

Terraform 0.11.7 fixes saved destroy plans with resource counts

A saved destroy plan followed a different count-evaluation path from a direct destroy. Terraform 0.11.7 corrects that distinction and two customized-diff defects.

Saving a plan creates a boundary between the process that decides what should happen and the process that performs it. Terraform 0.11.7 fixes a count-evaluation defect at that boundary. The April 10 release notes describe corrected handling of interpolated counts when applying a destroy plan. The implementation explains why a direct destroy could work while applying its saved equivalent failed.

In pull request 17824, James Bardin identifies the missing case in resourceCountMax. The function already treated an apply operation specially, but did not include the destroy operation in that treatment. Applying a saved plan no longer had access to the computed counts in the same way as a single destroy operation. The latter still had state updated with the latest counts from planning.

I consider saved-plan behavior particularly important for Terraform's abstraction. Separating a proposed change from its execution is useful only when the execution path retains the information required to carry out that proposal. A distinction between direct and saved execution needs a reason visible in the workflow, not an accidental difference in an internal operation flag.

Destroy is an apply operation internally

The correction is a small condition change. It includes walkDestroy alongside walkApply when deciding whether to use the apply-specific count path. The patch explicitly describes destroy as a special case of apply. That classification keeps the count logic aligned with the work being executed.

if !(i.Operation == walkApply || i.Operation == walkDestroy) {

This line is an implementation excerpt, not an expression to put in a Terraform configuration. Its importance is the information available at that stage. A plan created and executed in the same context can retain intermediate values that a newly constructed context cannot obtain through the same route. Correctness must account for what survives the saved representation.

The regression fixture uses a list variable and an interpolated length expression for resource count. Its output references resource identifiers through the splat syntax. Those constructs give the test a count derived from configuration and an output dependent on the counted resources. They make the defect more specific than a fixed numeric count in an otherwise empty configuration.

The fixture's resource is a test resource declaration handled by a mock provider. It omits the arguments a real AWS instance would require, so it should not be copied as a complete AWS deployment. The relevant count expression is ordinary 0.11 syntax.

count = "${length(var.list)}"

The surrounding test constructs state with two instances and a list output, creates a destroy plan, then writes and reads the plan before applying it. That serialization step is essential. Without it, a test could repeat the successful single-operation path and miss the failure that occurs when a separate process loads the plan.

The test reconstructs the execution context

After writing and reading the plan, the test builds a new context from the reloaded object and calls apply. It checks that planning, serialization, context construction, and application each succeed. This reproduces the distinction identified in the patch description without invoking a real cloud account.

The test therefore makes a useful claim about the fix. It demonstrates that the saved representation can support this interpolated-count destroy scenario in a fresh context. It does not establish that all count expressions or every provider operation are interchangeable under every condition. The release's claim is about the repaired path, and the regression case exercises that path directly.

For users who separate planning from execution, that scope is practical. A defect can depend on the handoff even when both commands accept the same configuration. Diagnosing only the resource definition would miss the relevant difference. The representation of the planned operation and its reconstruction are part of the behavior being relied upon.

Replacement decisions must preserve the diff

The same release repairs two provider SDK problems involving ResourceDiff.ForceNew. Chris Marchesi's patch for pull request 17811 describes both. A removed key could lose its removal status during the second diff calculation. Separately, marking a field for replacement could write zero values into previously undefined parts of complex values.

The first problem concerns the meaning of absence. A key removed from configuration must remain distinguishable as removed when customization triggers another diff. The patch checks the original diff's NewRemoved flag so that an existing intermediate value does not conceal that removal.

The second problem came from using a value-writing operation to register keys that needed another diff. Reading an existing complex value could materialize defaults for undefined subfields. Writing it back then introduced differences unrelated to the provider's intended replacement decision. The correction tracks forced-new keys separately and avoids writing values merely because replacement was requested.

I find this separation valuable for provider authors. Declaring that a change requires replacement should not silently alter the values being compared. If a provider intends to supply a new value, that should be a separate operation. The patch makes that distinction explicit in its data structures.

Terraform 0.11.7 thus repairs two related forms of lost meaning during planning. A saved destroy operation now receives the count handling appropriate to execution. Customized provider diffs retain removals and avoid fabricated zero-value differences when a provider marks a key for replacement.