Cloud

Terraform 1.16: Destroy-Time Actions and Module Imports

Terraform 1.16 adds destroy-time actions, imports inside child modules, Mermaid graphs and JSON output for more commands.

3 October 2026  ·  4 min read  ·  Anand

Terraform 1.16: Destroy-Time Actions and Module Imports

HashiCorp has released Terraform 1.16, and it fills two gaps that many infrastructure teams have worked around for years. Actions can now run when a resource is destroyed, not just when it is created or updated. Import blocks can now sit inside child modules, so a module can bring existing infrastructure under management on its own. The release also adds smaller CLI improvements that make Terraform easier to script and document. Here is what changed and how to adopt it safely.

Destroy-time actions

Actions let a provider run an operation that is not itself a managed resource, such as triggering a backup or calling an external system. Until now there was no clean way to run that kind of step when a resource was being removed. Terraform 1.16 adds two new events to the action_trigger block in a resource’s lifecycle: before_destroy and after_destroy.

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]
    }
  }
}

HashiCorp suggests uses such as taking a final database backup before deletion, removing records from a CMDB, releasing reserved addresses and checking that backups exist. Until now, these steps usually lived in separate scripts or a runbook that someone had to remember to follow.

Rules to keep in mind

  • The action’s configuration and trigger condition must be fully known at plan time.
  • The trigger block must still be in your configuration when Terraform plans the destroy. If you delete the resource block and its trigger together, the action has nothing to run from.
  • To retire a resource that has destroy actions, first set count = 0 and apply. Then remove the block in a later change.
  • You choose what happens when an action fails: halt (the default), taint or continue. Use halt for steps such as a final backup, where it is safer to stop than to delete without one.
  • Actions come from providers. The provider you use must implement the action you want to call.

Import blocks in child modules

Before 1.16, import blocks had to live in the root module. To adopt existing infrastructure through a shared module, the caller had to know the module’s internal resource addresses and write imports against them. In 1.16, the module can contain the import itself, and the caller only supplies the identity of the existing object:

# modules/log-bucket/main.tf
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
}

# root module
module "logs" {
  source               = "./modules/log-bucket"
  existing_bucket_name = "platform-audit-logs"
}

Terraform evaluates the import separately for each module instance. That includes nested modules and module calls that use count or for_each. For teams bringing hand-built AWS resources into Terraform, this means one well-tested module can adopt dozens of existing buckets, security groups or DNS zones without separate import code in every root configuration.

Smaller improvements

  • terraform state show -json and terraform workspace list -json now give machine-readable output, which is useful in CI pipelines and audit scripts.
  • terraform graph -format=mermaid outputs a Mermaid diagram that you can paste into Markdown documentation or a wiki.
  • The CLI now shows a summary of HCP Terraform policy evaluation results.
  • Prebuilt binaries are now available for Linux on s390x.

Upgrading to Terraform 1.16

Terraform minor releases are usually smooth, but treat the upgrade as a change in its own right and do not combine it with infrastructure changes.

  1. Read the changelog and upgrade guide for 1.16 before you start, especially if you use HCP Terraform or custom providers.
  2. Back up remote state, or confirm that your backend keeps versions, as S3 does with bucket versioning enabled.
  3. Upgrade the CLI in CI first. If you installed it from the HashiCorp package repository, use your normal package manager:
    # RHEL, Rocky Linux, AlmaLinux
    sudo dnf upgrade terraform
    
    # Debian, Ubuntu
    sudo apt update && sudo apt install --only-upgrade terraform
    
    terraform version
  4. Re-initialise and plan each workspace. A plan with no changes is the result you want before using any new features:
    terraform init -upgrade
    terraform plan
  5. Pin the version once you depend on the new features, so older CLIs fail early instead of misreading your configuration:
    terraform {
      required_version = ">= 1.16.0"
    }
  6. Adopt features one at a time. Try destroy-time actions first on a non-production resource, and check that a failing action does what your chosen failure mode says it will.

How TechProvidence can help

TechProvidence builds and maintains Terraform code for AWS environments. That includes bringing manually created resources under code, refactoring root configurations into reusable modules, setting up CI pipelines with plan review, and running Terraform upgrades without surprise changes. If you want help adopting 1.16 or getting your existing cloud setup into code, contact us.

Source: HashiCorp Blog

Running into something similar?

We look after Linux servers, control panels and virtualization for businesses every day. If an update, vulnerability or outage here affects you, we can help.