Build1 publisherNot yet confirmed elsewhere3 min readPublished
Terraform 1.16 moves pre-destroy cleanup scripts into the plan with destroy-time Actions
Terraform 1.16 lets Actions fire on before_destroy and after_destroy events, putting backups and deregistration beside the resource and in the plan. Pipelines pay for that with two-step deletions and a failure mode someone has to choose on purpose.
The Engineer · Build desk

What happened
- Terraform 1.16 also lets child modules carry their own import blocks, so the root module no longer has to own every import.
- Each module-owned import is evaluated per module instance, including nested modules and module calls that use count or for_each.
- A destroy Action's configuration and its trigger condition must be fully known at plan time.
- Action failures are handled in one of three modes, halt, continue or taint, and halt is the default.
- The release also adds JSON output for state show and workspace list, Mermaid output for terraform graph, and Linux s390x binaries.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint Cleanup that depends on values resolved during apply cannot move into a destroy Action until the configuration is restructured to know those inputs at plan time.
- cost Every deletion of a hooked resource now costs two reviewed changes plus a note telling reviewers why the first one exists.
- decision Each destroy Action forces a choice between a teardown that stops on a failed backup and one that proceeds without it.
- capability Authors of shared module catalogues can build adoption of existing infrastructure into the module once, instead of each consumer writing root-level imports.
An Action is its own block. It attaches to a resource through action_trigger inside that resource's lifecycle block, with before_destroy or after_destroy listed in events [4][1]. HashiCorp's release post leads with a final backup, deregistering an asset, and cleaning up an external system before the infrastructure goes away, according to a dev.to write-up of the post [3]. The write-up's author used to run those steps outside Terraform, in a script called pre-destroy.sh that took a backup, poked an external inventory system and only then let the pipeline run terraform destroy [19]. "Everyone on the team was a little scared of it, and nobody ever fully trusted it," the author wrote [18].
The trigger hangs off the resource block, and that placement sets the constraint teams will hit first. Delete the block and its action_trigger goes with it, which leaves Terraform no hook to call when it plans the destroy [10]. HashiCorp recommends two steps: first put the resource and its trigger configuration at count = 0, and only remove the block in a follow-up change [11]. The destroy and the hook run in the first change. The second deletes a block that no longer manages anything [10][11]. A reviewer scanning the diff for deleted resource blocks will find none in the change that does the deleting [11].
Failure handling needs the same care. Under continue, Action errors become warnings, so a backup that fails during teardown does not stop the destroy [12][14]. The taint mode offers no rescue. According to the write-up, taint flags a freshly created resource to be replaced, and it does nothing to let a failed destroy Action be retried [13]. For a backup Action I'd keep halt, which stops dependent processing after an error, and accept a stuck pipeline over a teardown with no backup [12].
Terraform has long had destroy-time provisioners, set with when = destroy, which run shell commands as a side effect of the destroy [15]. "They work, but a provisioner is an escape hatch and reviewers tend to treat it as one," the author wrote [21]. CloudFormation covers backups with DeletionPolicy: Snapshot on supported resources and uses custom resources for other cleanup on delete [16]. Kubernetes finalizers block deletion of an object until a controller has done its cleanup [22]. In Terraform's version, the cleanup step sits next to its resource and shows up at plan time, per the write-up [5].
"The second change is quieter, and I think more people will feel it day to day," the author wrote of the import change [23]. Previously, an import block was allowed to point at a resource in a child module, yet the block had to be declared in the root module [6]. HashiCorp's post calls that "an awkward exception" for modules that are supposed to hide their implementation details [7]. In the write-up's example, a log-bucket module declares its own import keyed on a bucket-name variable, and the caller passes only the name, with no resource addresses [20].
I'd adopt module-owned imports in a shared catalogue at the next version bump. Destroy Actions fit where cleanup inputs are known at plan time and the team will accept two-step deletions [9][11].
What to watch
- Whether a later Terraform release lets a trigger fire from a deleted resource block, which would remove the count = 0 step.
- Which providers ship real backup and deregistration Action types; the release example uses placeholder example_cleanup and example_service names.
- Whether HashiCorp adds a retriable failure mode for destroy Actions, since taint does not cover a failed teardown.
Clarity's read
What the record supports and how the coverage leans. The claims behind it follow.
Reality
- Evidence55
- Adoption
- Insufficient
- Hype gap0
- Incentives
- Insufficient
- Confidence50
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
Terraform 1.16 lets Actions fire on before_destroy and after_destroy events.
- [2]
In Terraform 1.16, import blocks can live inside child modules, so the root module no longer has to own every import.
- [3]
HashiCorp's release post opens with use cases of taking a final backup, deregistering an asset, or cleaning up an external system before the infrastructure goes away.
- [4]
An Action is attached to a resource through action_trigger in the resource's lifecycle block, with the destroy event listed in events.
- [5]
With destroy-time Actions the cleanup step sits next to the resource it belongs to and shows up at plan time.
- [6]
Before 1.16, an import block could target a resource inside a child module, but the block itself had to sit in the root module.
- [7]
HashiCorp calls the root-only import requirement "an awkward exception" for modules that are supposed to hide their implementation details.
- [8]
Terraform evaluates a module-owned import in the context of each module instance, including nested modules and module calls that use count or for_each.
- [9]
The configuration and trigger condition of a destroy Action must be fully known at plan time; cleanup inputs that only resolve during apply require restructuring.
- [10]
Removing a resource block also removes its action_trigger, so Terraform has nothing to invoke when it plans the destroy.
- [11]
HashiCorp's guidance is to set count = 0 on the resource and its trigger configuration first, then remove the block in a later change, turning one pull request into two.
- [12]
Destroy Action failure modes are halt (the default, which stops dependent processing after an Action error), continue (errors become warnings) and taint.
- [13]
The taint mode marks a newly created resource for replacement and does not make a failed destroy Action retriable.
- [14]
If a backup step fails during a teardown, continue lets the destroy go ahead anyway.
- [15]
Terraform has long had destroy-time provisioners (when = destroy), which run shell commands as a side effect of the destroy.
- [16]
AWS CloudFormation handles the backup case with DeletionPolicy: Snapshot on supported resources and uses custom resources for arbitrary cleanup on delete.
- [17]
Terraform 1.16 also adds machine-readable output from terraform state show -json and terraform workspace list -json, terraform graph -format=mermaid, an HCP Terraform policy evaluation summary in CLI output, and Linux s390x binaries.
- [18]
"Everyone on the team was a little scared of it, and nobody ever fully trusted it."
- [19]
The write-up's author kept a script called pre-destroy.sh that took a final backup, poked an external inventory system, and only then let the pipeline run terraform destroy.
- [20]
In the example, a log-bucket module declares its own import using an existing_bucket_name variable, and the consumer passes a name without needing to know any resource addresses.
- [21]
"They work, but a provisioner is an escape hatch and reviewers tend to treat it as one."
- [22]
Kubernetes uses finalizers, which block deletion of an object until a controller has done its cleanup.
- [23]
"The second change is quieter, and I think more people will feel it day to day."
- [24]
For a shared module catalogue, adoption of existing infrastructure becomes something the module author designs once instead of every consumer working it out alone.
Sources
1 independent publisher whose own reporting we read for this story.
- dev.toTerraform 1.16 adds destroy-time Actions and lets child modules own their imports
1 article · October 6, 2026
Topics and entities
Follow any of these and your For You feed starts watching them — no settings page required.
Topics
- Deployment pipelinesFollow
- Infrastructure as CodeFollow