September 29, 2021·4 min read·engineeringterraformstateplanning

Refresh-only planning retains orphaned Terraform instances

Terraform 1.0.8 repairs refresh-only handling when count or for_each changes leave instances outside the configuration. The fix preserves the distinction between observing infrastructure and planning its removal.

Reducing a resource's count changes the desired set of instances. It does not establish that the corresponding remote object has disappeared. That distinction is essential in a refresh-only plan, whose purpose is to observe existing infrastructure without planning configuration-driven changes. Terraform 1.0.8, released on September 29, repairs a case where the planner failed to preserve that distinction.

The affected instances are orphans in Terraform's planning terminology. An instance remains recorded in state but no longer belongs to the set selected by the configuration's count or for_each. Normal planning needs to consider what to do with that mismatch. Refresh-only planning must not turn that configuration change into removal from the planned state.

I find this distinction central to trusting Terraform's state operations. A configuration can express a new desired topology while the operator separately examines what exists now. Refresh-only mode is useful only if the planner keeps those questions separate throughout its graph, including for instances that no longer match configuration.

A mode flag did not reach every instance

Alisdair McDiarmid's pull request 29640 identifies the mechanism. The graph's orphan-instance node did not receive the plan's flag for skipping configuration-driven changes. Without that flag, orphan handling could remove the instance from the planned state even though the overall operation was refresh-only.

The production-code correction is small. It passes skipPlanChanges into orphan nodes in two expansion paths. One path also receives the skipRefresh flag. The size of the patch is less informative than the boundary it repairs. A planning mode chosen at the top of the operation has to reach every relevant graph node to retain its meaning.

This is also why the release note's phrase about removing orphaned instances needs careful interpretation. The patch description explicitly discusses their removal from planned state. It is not a claim that this bug issued a provider deletion during a refresh-only apply. Losing the tracked instance from Terraform's proposed records is itself the behavior the patch addresses.

The code's flags describe separate decisions. Skipping refresh concerns whether Terraform observes remote objects. Skipping planned changes concerns whether configuration differences should generate the normal resource actions. Refresh-only needs observation while suppressing those configuration-driven actions, so passing the right combination is essential.

The regression test separates four values

The added test starts with two instances in state while configuration sets count = 1. That makes the second instance an orphan. Both instances begin with an attribute value of before, while the configuration requests after. A mock provider then supplies different values during state upgrade and refresh.

The upgrade response changes the value to upgraded. The read response changes it to current. Those deliberately distinct strings allow the test to identify which phase produced each result. A test that used the same value everywhere could preserve the wrong state without detecting the mistake.

After refresh-only planning, the test expects no resource changes. It also checks that both instances still exist in the refreshed prior state with current values. The prior-run state separately retains the upgraded representation. That checks the preservation of both instances and the separation of historical and refreshed records.

The output assertion adds another useful condition. The configuration exports the instances' attribute values through a splat expression, and the test expects both refreshed values in the resulting output. Merely avoiding a deletion action would not prove that the planner's working state still contains the orphan. The output checks that later evaluation sees it too.

The intended operation remains straightforward at the CLI:

terraform plan -refresh-only

The significance of the patch is what happens beneath that flag when configuration and state disagree about instance membership. Refresh still reads the object. Configuration no longer causes the orphaned instance to disappear from the planned state in the affected paths.

Initialization also improves the order of errors

Version 1.0.8 has another fix with a related concern about interpreting information in the right phase. James Bardin's pull request 29665 makes init check required_version before reporting configuration diagnostics that may result from running an incompatible Terraform executable.

An executable that does not understand a newer construct can otherwise report the construct as invalid without explaining that the configuration excludes that executable's version. The patch preserves partially loaded configuration information so the version requirement remains available even when other diagnostics exist.

Its tests cover the requirement in both a root module and a child module. Each expects the unsupported-Core-version diagnostic despite other invalid constructs. The change does not make unsupported syntax valid. It makes the unmet version condition available as the explanation before less useful downstream errors.

I appreciate these corrections because both preserve meaning through implementation phases. Refresh-only planning must carry its mode into orphan-instance handling. Initialization must retain enough parsed information to identify a version mismatch. In the orphan case, the resulting plan now records the observed instances without treating a changed instance count as evidence that those objects should vanish from state.