DEV Community

Leo
Leo

Posted on Originally published at cicd.deployment.to

Terraform 1.16 adds destroy-time Actions and lets child modules own their imports

I once kept a shell script called pre-destroy.sh in a repo for far longer than I'd like to admit. It took a final backup, poked an external inventory system, and only then let the pipeline run terraform destroy. Everyone on the team was a little scared of it, and nobody ever fully trusted it. Terraform 1.16 lets a lot of that logic move into the configuration, where the plan can see it.

HashiCorp's release post covers two headline changes. Actions can now fire on before_destroy and after_destroy events. Import blocks can also live inside child modules, so the root module no longer has to own every import.

Destroy gets its own hooks

The release post opens with the cases you'd expect: taking a final backup, deregistering an asset, or cleaning up an external system before the infrastructure goes away. You attach an Action to a resource through action_trigger in its lifecycle block and list the destroy event in events. The shape from HashiCorp's example looks like this:

action "example_cleanup" "archive" {
  config {
    resource_id = caller.id
  }
}

resource "example_service" "payments" {
  name = "payments"

  lifecycle {
    action_trigger {
      events  = [before_destroy]
      actions = [action.example_cleanup.archive]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

For anyone who has wired up cleanup steps in a pipeline, this is the nice part. The cleanup step sits next to the resource it belongs to, it shows up at plan time, and you stop relying on whoever runs the destroy job to remember the wrapper script.

Imports that travel with the module

The second change is quieter, and I think more people will feel it day to day. Until now, an import block could target a resource inside a child module, but the block itself had to sit in the root module. HashiCorp calls that "an awkward exception" for modules that are supposed to hide their implementation details.

In 1.16 the module can carry its own import:

variable "existing_bucket_name" {
  type = string
}

resource "aws_s3_bucket" "this" {
  bucket = var.existing_bucket_name
}

import {
  to = aws_s3_bucket.this
  id = var.existing_bucket_name
}
Enter fullscreen mode Exit fullscreen mode

The consumer passes a name and doesn't need to know any resource addresses:

module "logs" {
  source               = "./modules/log-bucket"
  existing_bucket_name = "<existing-bucket-name>"
}
Enter fullscreen mode Exit fullscreen mode

Terraform evaluates the import in the context of each module instance, and per the post that includes nested modules and module calls that use count or for_each. If you maintain a shared module catalogue, adoption of existing infrastructure becomes something the module author designs once instead of every consumer working it out alone.

The rough edges you'll hit first

Three things stood out to me as the ones that will trip up a pipeline.

The configuration and trigger condition of a destroy Action must be fully known at plan time. If your cleanup inputs only resolve during apply, you'll need to restructure before this works for you.

You can't just delete the resource block. Removing the block also removes its action_trigger, so Terraform has nothing to invoke when it plans the destroy. HashiCorp's guidance is to set count = 0 on the resource and its trigger configuration first, then remove the block in a later change. That turns one pull request into two, so tell your reviewers why.

Failure behaviour needs an explicit decision. The modes are halt (the default, which stops dependent processing after an Action error), continue (errors become warnings) and taint. The post is careful to note that taint marks a newly created resource for replacement and does not make a failed destroy Action retriable. If your backup step fails during a teardown, continue lets the destroy go ahead anyway. Choose that one on purpose.

Smaller things in the same release

  • terraform state show -json and terraform workspace list -json give scripts machine-readable output.
  • terraform graph -format=mermaid produces a diagram you can drop into a pull request description.
  • The CLI output includes an HCP Terraform policy evaluation summary.
  • Linux s390x binaries are now available.

How this compares with other tools

Teardown hooks are an old idea. Terraform itself has long had destroy-time provisioners (when = destroy), which run shell commands as a side effect of the destroy. They work, but a provisioner is an escape hatch and reviewers tend to treat it as one. AWS CloudFormation handles the backup case declaratively with DeletionPolicy: Snapshot on supported resources, and uses custom resources for arbitrary cleanup on delete. Kubernetes uses finalizers, which block deletion of an object until a controller has done its cleanup. Terraform's version stands out to me for where it puts the decision: the cleanup appears in the plan and has a documented failure mode, so a reviewer can see it before anything is destroyed.

The plan-time constraint and the two-step removal mean my old pre-destroy.sh won't disappear overnight. Still, I'd move the backup step into an Action on the next module I touch. Next I'll be watching how teams handle continue in production teardowns, because that's where a well-meant shortcut will turn into a missing backup. If you've already put destroy Actions in a pipeline, I'd like to hear how you handled the failure mode choice.

Top comments (0)