skip to content

What is a destroy-time provisioner in Terraform, and what restrictions apply to what it can reference?

level: seniorimportance: nice to knowfreq 30%

answer

  1. when = destroy, a bare keyword
  2. runs before the delete call
  3. only self, count.index, each.key
  4. delete the block and nothing fires
  5. must be safe to run twice

basics

~20 s

A provisioner with when = destroy runs before Terraform destroys the resource, typically to deregister or drain it. It may only reference self, count.index and each.key — not variables, locals or other resources — and it must still be in the configuration when the destroy happens.

solid answer

~50 s

Setting `when = destroy` on a provisioner flips it from create-time to destroy-time: Terraform runs it just before the resource is destroyed, which people use for graceful shutdowns, deregistering an agent, or draining a node. Two restrictions bite. First, references are limited to `self`, `count.index` and `each.key` — reaching for a variable, a local or another resource fails with an error saying destroy-time provisioners may only reference attributes of the related resource. That is deliberate: at destroy time the surrounding configuration may no longer exist. Second, the provisioner must still be present in the configuration when the destroy runs, so deleting the resource block outright deletes the provisioner with it and nothing fires — you have to keep the block, apply, then remove it in a second step. Failures do not taint; Terraform errors and retries the provisioner on the next destroy attempt, so it must be safe to run more than once.

code

hcl · 15 lines
hcl
resource "aws_instance" "node" {
  ami           = var.ami_id
  instance_type = "t3.micro"

  connection {
    type = "ssh"
    user = "ubuntu"
    host = self.private_ip
  }

  provisioner "remote-exec" {
    when   = destroy
    inline = ["/opt/drain-node.sh --timeout 60"]
  }
}

go deeper

for a junior

Know that when = destroy makes a provisioner run just before the resource is deleted, and that it is used for things like draining or deregistering a node.

for a middle

State the reference restriction precisely — self, count.index, each.key only, connection block included — and explain that the configuration may be gone at destroy time.

for a senior

Show the operational traps: deleting the block silently skips the hook, failures re-run rather than taint, and the script must be idempotent. Have the two-step removal workflow ready.

for a principal

Own the position that destroy-time hooks are best-effort: they miss every termination Terraform did not initiate, so required cleanup belongs in the machine's shutdown path or a platform feature instead.

## The mechanics By default a provisioner is a *creation-time* provisioner: it runs after the resource is created. Adding `when = destroy` converts it: ```hcl resource "aws_instance" "node" { ami = var.ami_id instance_type = "t3.micro" provisioner "remote-exec" { when = destroy inline = ["/opt/drain-node.sh"] } } ``` Note `destroy` is a bare keyword, not a quoted string, exactly like `on_failure = continue`. A single resource can carry both kinds; each provisioner block declares its own `when`. Terraform runs destroy-time provisioners *before* it calls the provider's delete operation, which is the whole point — the resource still exists, so you can still talk to it. Typical uses are draining connections from a node, deregistering an agent from a monitoring or service-discovery system, or copying a final log bundle off the machine. ## Restriction one: what you may reference This is the part interviewers probe. A destroy-time provisioner, and the `connection` block it uses, may only reference: - `self` — attributes of the resource being destroyed - `count.index` - `each.key` Anything else — an input variable, a `local`, another resource's attribute, a data source — produces an error stating that destroy-time provisioners and their connection configurations may only reference attributes of the related resource. The reasoning is about ordering and lifetime. When the resource is being destroyed, the rest of the configuration around it may be being destroyed too, or may already be gone; Terraform would have to guarantee that whatever you referenced still exists and still holds a usable value at that moment. Rather than invent ordering rules for that, the language forbids the reference. The practical consequence bites hardest on `connection`: you cannot write `private_key = var.ssh_key`. Anything the destroy step needs must already be an attribute of the resource — which usually means baking the credential path or the drain script into the image, or storing what you need on the instance itself. ## Restriction two: it must be in the configuration at destroy time Provisioners are not stored in state as executable content; Terraform reads them from the configuration at run time. So if you delete the `resource` block, the provisioner goes with it, and the destroy runs with no hook at all. The workflow to remove a resource that has a destroy-time provisioner is therefore two steps: 1. Keep the block in place, remove it from the desired set some other way (or run `terraform destroy -target`), and let the provisioner fire. 2. Then delete the block from the configuration. The same trap applies to *adding* one: a destroy-time provisioner added today does nothing for resources that already exist unless the configuration is still in place when they are eventually destroyed — which it will be, since the block lives in the config rather than in state. The failure mode people actually hit is the deletion order. ## Failure behaviour differs from creation-time A failed creation-time provisioner taints the resource, so the next apply replaces it. A failed destroy-time provisioner cannot do that — the resource is on its way out. Instead the apply errors, the resource stays in state undeleted, and Terraform will attempt the provisioner again on the next destroy. HashiCorp's documentation therefore warns explicitly that destroy-time provisioners must be safe to run multiple times: a drain script that half-completed will be re-run from the top. ## Why they are rarer than people expect Most of the jobs destroy-time provisioners are reached for have better homes: - Draining a load-balancer target is usually a platform feature — a deregistration delay or connection-draining timeout configured on the target group as an attribute. - Deregistering from service discovery is normally the agent's own responsibility on shutdown, or a TTL expiry. - Graceful termination on a VM is a shutdown hook inside the operating system, which fires whether the termination came from Terraform, an autoscaler, or a hardware failure — the last of which no Terraform provisioner can help with. That last point is the strongest argument: Terraform only runs a destroy-time provisioner when *Terraform* performs the destroy. An instance killed by a spot interruption, an autoscaling event, or a console click never runs it. Depending on one for correctness builds a cleanup path with holes in it. Treat destroy-time provisioners as best-effort convenience, and put anything that must happen in the machine's own shutdown path or in the platform's configuration.

  • Why can't a destroy-time provisioner reference an input variable for its SSH key?
    Because the restriction covers the `connection` block too: only `self`, `count.index` and `each.key` are allowed. At destroy time the surrounding configuration may itself be being destroyed, so Terraform refuses to depend on values whose lifetime it cannot guarantee. The credential has to be something already available on the resource, or the design has to change.
  • You need to remove a resource that has a destroy-time provisioner. What order do you do it in?
    Two applies. First destroy the resource while its block — and therefore its provisioner — is still in the configuration, so the hook fires. Only then delete the resource block from the code. Deleting the block first removes the provisioner along with it, and the destroy proceeds with no hook at all.
  • Why is a destroy-time provisioner a weak place to put required cleanup?
    It only runs when Terraform performs the destroy. A spot interruption, an autoscaling scale-in, a console deletion or a host failure all bypass it entirely. Anything that must happen belongs in the operating system's shutdown path or in a platform feature such as a deregistration delay, where it fires regardless of who initiated the termination.

saying these in an interview costs you the question

  • Writing when = "destroy" as a quoted string
  • Expecting a removed resource block to still run its provisioner
  • Referencing var or local from a destroy-time connection
  • Believing a failed destroy provisioner taints the resource
  • Relying on it to clean up after a spot interruption

context