August 20, 2024·4 min read·engineeringterraformmoduleslifecyclehcp

Terraform 1.9.5 preserves removal provisioners across module boundaries

A removed block in a parent module could fail to attach its destroy-time provisioner to a nested resource. Terraform 1.9.5 gathers removal declarations independently of resource location.

Removing a resource declaration can remove the only place where its cleanup behavior was written. Terraform's removed block allows that behavior to remain explicit after the resource block disappears. Terraform 1.9.5 fixes a gap in that mechanism when the removed resource belongs to a nested module and the removal declaration lives above it.

The 1.9.5 release, dated 20 August, lists the nested-module provisioner correction alongside support for hosted workspaces with HCP resource IDs. The removal fix is small in source but consequential in meaning. A declaration's location in the module tree should not prevent it from finding the resource address it names.

I value Terraform modules because they package infrastructure behavior behind a reusable interface. That abstraction also needs a coherent removal path. The operation is incomplete if a resource can be selected for destruction while the configuration intended to accompany its destruction is lost during graph construction.

Removal declarations retain lifecycle intent

The resource syntax documentation at the 1.9.5 tag distinguishes removing an object from Terraform's management from destroying the object itself. Deleting a resource block normally causes Terraform to plan destruction. A removed block can instead declare that the object should leave state while remaining in the remote system.

The destroy argument makes that distinction explicit. A false value removes the state association without destroying the managed object. A true value allows destruction and is required when a destroy-time provisioner in the removal block should execute. These are different lifecycle decisions even though both begin with removing a resource declaration.

Destroy-time provisioners provide a way to retain an extra operation in source after the resource block is gone. The documentation permits only the same restricted references allowed in ordinary destroy-time provisioners, including self, count.index and each.key. It requires when = destroy. A removal block is not an unrestricted place to recreate arbitrary dependencies on the configuration being removed.

A parent module can name a nested resource in its removal address. The affected shape can be illustrated as follows, using a command that only prints the selected instance identifier.

removed {
  from = module.foo.aws_instance.example

  lifecycle {
    destroy = true
  }

  provisioner "local-exec" {
    when    = destroy
    command = "echo Removing instance ${self.id}"
  }
}

The example assumes that state already contains the nested resource being removed. It is not a standalone configuration for creating an instance. Its purpose is to show the relationship between the declaration's location and the address of the object it targets.

The graph attachment looked in the wrong place

The fix in pull request 35611 changes the resource configuration attachment transformer. That transformer associates configuration with graph nodes. Before the patch, its search for removal declarations used the configuration module corresponding to the resource's location.

This was insufficient because a removal block can live in a parent of the referenced resource. Looking only beside the nested resource misses a valid declaration above it. The provisioner configuration then fails to attach to the resource node that needs it. The release notes describe the observable result as removal blocks with provisioners not executing for nested-module resources.

The corrected transformer first walks the configuration tree and gathers resource-targeting removal blocks into a map. The map uses the resource configuration address, retaining module context. During node processing, Terraform checks that collection before trying to attach ordinary resource configuration.

That ordering reflects a language invariant noted in the patch. Validation does not permit a resource declaration and a removal declaration to describe the same configuration target simultaneously. Finding a matching removal block therefore supplies the relevant configuration for that target. The transformer does not need to assume that the target's module also contains the declaration.

The regression test moves the resource instance into module.foo and changes the removal target to the corresponding nested address. That is a useful test change because it exercises the missing relationship directly. A root-only test could verify provisioner execution while leaving the parent-to-child attachment problem uncovered.

Hosted identifiers are a separate compatibility change

The hosted workspace enhancement comes through pull request 35495. Its dependency changes upgrade the go-tfe client used by Terraform. The release notes state the resulting capability as interaction with workspaces that have HCP resource IDs.

This should not be read as documentation of a new cloud configuration argument for entering an arbitrary workspace ID. The release describes compatibility with those workspace identifiers, while the patch updates the client dependency. That is a narrower claim than a new user-facing workspace selection syntax.

I consider the removal fix the more instructive change for module authors. It preserves the full address needed to attach lifecycle configuration across module boundaries. The hosted client update preserves a different integration boundary, allowing the CLI's existing cloud operation to work with the workspace identities returned by the service.