Terraform 1.3.9 handles deposed objects during removal planning
A planning panic exposes the distinction between a resource instance and the old remote objects Terraform still needs to destroy.
A Terraform resource address can outlive the object that currently occupies it. During replacement, Terraform can also retain an older object under that address while arranging its destruction. The distinction matters when the configuration no longer asks for the instance at all. Terraform 1.3.9, released on February 15, fixes a crash in planning removal of already-deposed instances. The underlying problem involved code that expected a current object where only an older generation remained.
The release has one listed bug fix, but the state model behind it deserves some explanation. I regard Terraform's common language for infrastructure APIs as one of its most useful properties. That language has to keep working through incomplete changes as well as successful ones. Replacement creates intermediate states that a simple mapping between an address and one remote object cannot represent.
An instance can retain several generations
The state definitions in this release distinguish Current from Deposed. The current field points to the remote object presently represented by an instance. Its deposed map holds remote objects previously represented by the same instance that have been replaced and still await destruction under create_before_destroy.
An instance can therefore have no current object while still holding deposed objects. The source comments identify a normal intermediate case during replacement, after the current object has been deposed but before Terraform creates its replacement. Absence of the current generation does not imply that Terraform has no remaining work associated with the instance.
The lifecycle documentation explains why the order can change. Terraform ordinarily destroys an object before creating its replacement when an argument cannot change in place. With create_before_destroy, it creates the replacement first. That requires the remote API to permit the old and new objects to coexist. Terraform keeps state for the older object so it can complete the later destruction.
For example, this lifecycle block requests the alternate ordering inside a resource:
lifecycle {
create_before_destroy = true
}
This is a configuration fragment, not a complete resource. The resource type still determines which arguments require replacement and whether simultaneous objects are possible. The lifecycle argument changes the sequence of operations. It does not remove the need to track each object until its work is complete.
Orphan handling expected a current object
Terraform also needs to find instances present in state that configuration no longer requests. Its orphan-processing graph nodes plan work for those instances. The regression test for this panic constructs a state with a deposed object and executes an orphan planning node against it. The test's comment says that this particular node should never have worked on deposed instances in the first place.
That combination exposed a missing check. The implementation tested whether the old state's value was null without first checking whether the old-state object itself existed. A nil object reference and a typed null value are different representations. Accessing the latter through a missing former object causes a panic before Terraform can decide that there is nothing to plan for the current generation.
The patch adds an oldState == nil check alongside the existing null-value check. When no old state exists, the node takes the same early-return path used when there is no current object left to process. The surrounding comment explains that an instance absent from both refreshed state and configuration has no further change for this node to plan.
That is a deliberately limited guarantee. The test asserts that execution does not produce an error, and its comment explicitly avoids claiming a complete specification for processing deposed objects through this node. It is evidence that the defensive path handles the constructed state. It is not evidence that every possible interrupted replacement now has a newly defined recovery sequence.
Graph construction needs the same distinction
A companion graph-transformer change addresses why an inappropriate node could appear. When enumerating instances for count-related orphan handling, Terraform now skips an instance whose Current field is nil. Its comment explains that a separate mechanism handles instances containing only deposed objects.
The transformer test makes that separation visible. It constructs current objects at two indexed addresses and a deposed object at a third. Configuration requests only the first current instance. The expected orphan graph includes the second current instance, while excluding the address that has only a deposed object. The old object's remaining destruction work belongs to another path.
I find this distinction useful when reading an infrastructure plan. A resource address identifies a place in Terraform's model, while the state behind that address may contain more than one generation of remote object. Treating every stored address as a current object loses information that replacement depends on.
For operators, the patch addresses a planning failure rather than adding a lifecycle option. Existing create_before_destroy configuration retains its meaning. For implementers, the two corrections illustrate related responsibilities in the graph and executor. Graph construction should send each generation to the appropriate processing path, and execution should tolerate absence where its state lookup can return it. In this release, the removal planner can encounter that absence without dereferencing a missing object.