Hedronite · Cert Lesson · HashiCorp · Tue 2026-09-29

Terraform Associate: lifecycle meta-arguments ignore_changes, create_before_destroy, prevent_destroy

Reorders, blocks, or hides. Know which knob you turned.

Lesson Class: Cert (T2 · Terraform Associate 003 Obj. 8 · Pro-depth)
Vendor: HashiCorp
Verified: every output real · Terraform 1.14.3 · terraform_data only
Paired Ops: Artifact Registry cleanup + Rust census
Paired Dev: Rust TF integration tests
Grounding: Brikman pp.248-255 · Lab 01 · Lab 22 referenced
Lifecycle Shelf
Name the failure mode before picking the knob.
ignore_changes hides a field, not a ForceNew replace.

<!-- hal:authoritative:yaml -->

lifecycle does not stop every replace. It changes order, blocks destroy, or hides selected attributes from the diff. Know which knob you turned.

§I. Frame: Associate objectives for tonight

09-26 covered type constraints and optional(). 09-23 covered data sources. 09-20 covered dynamic blocks. 09-17 covered ephemeral values. 09-14 covered import and moved. 09-02 covered replace_triggered_by. Leave all of that.

Tonight is the lifecycle meta-argument: create_before_destroy, prevent_destroy, and ignore_changes. Ops declares an Artifact Registry repository whose format is immutable after create. That is the concrete surface. The drills below use terraform_data so every plan is real and credential-free.

§II. Three knobs, three jobs

Meta-argumentJobExam trap
create_before_destroyOn replace, create the new object before destroying the oldDoes not avoid replace; only reorders it
prevent_destroyRefuse a plan that would destroy this addressApply fails if destroy is required; fix the config or remove the flag
ignore_changesDrop listed attributes from the diffForce-new attributes you ignore still force a replace when other ForceNew fields change; ignored fields drift from code

Named technique: Lifecycle Shelf. Before you add a lifecycle block, name the failure mode: gap during replace, accidental destroy, or out-of-band mutation. Pick the matching knob. Do not stack all three by habit.

§III. create_before_destroy: order, not avoidance

variable "reader_token" {
  type    = string
  default = "a"
}

resource "terraform_data" "reader" {
  triggers_replace = var.reader_token
  lifecycle {
    create_before_destroy = true
  }
}

resource "terraform_data" "plain" {
  triggers_replace = var.reader_token
}

Apply token a, then plan with token b. Terraform 1.14.3 on the lab Mac printed two symbols in one plan:

-/+ destroy and then create replacement   # terraform_data.plain
+/- create replacement and then destroy   # terraform_data.reader

terraform show -json tfplan gave the action arrays:

terraform_data.plain  ["delete", "create"]
terraform_data.reader ["create", "delete"]

The flag does not cancel the replace. It swaps the order. The UI symbol +/- is create-first. -/+ is destroy-first. Associate stems that ask about a gap want this pair.

§IV. prevent_destroy: the flag has to still be in the file

variable "token" {
  type = string
}

resource "terraform_data" "registry" {
  triggers_replace = var.token
  lifecycle {
    prevent_destroy = true
  }
}

Apply -var=token=a. Then plan -var=token=b (a replace). Exit code 1:

Error: Instance cannot be destroyed

Resource terraform_data.registry has lifecycle.prevent_destroy set, but the
plan calls for this resource to be destroyed. To avoid this error and
continue with the plan, either disable lifecycle.prevent_destroy or reduce
the scope of the plan using the -target option.

terraform destroy while the block is still in the file fails the same way.

The trap. Delete the resource block entirely and the lifecycle block goes with it. A plan then destroys the object with exit 0. prevent_destroy only fires while the argument remains in configuration. On an Artifact Registry production repo, the flag stops a replace or a terraform destroy. It does not survive you deleting the resource from the module.

§V. ignore_changes: hide a field, not a replace

resource "terraform_data" "registry" {
  input            = var.desired_input
  triggers_replace = var.force_token
  lifecycle {
    ignore_changes = [input]
  }
}

After apply of desired_input = "v1" and force_token = "tok-a":

Ops adjacency. On google_artifact_registry_repository, ignore_changes = [labels] can hide labels edited in the Console. Ignoring format does not turn a format change into a no-op: format is ForceNew, and you should not ignore a create-time field you intend to manage. Prefer a correct plan over a quiet one.

§VI. Five drills

  1. With create_before_destroy = true, which JSON action array appears on a ForceNew change? (["create", "delete"]. The UI symbol is +/-.)
  2. prevent_destroy = true stays in the file and a ForceNew change arrives. What is the plan exit code? (1. Error: Instance cannot be destroyed.)
  3. You delete the whole resource block, including prevent_destroy. Does the next plan still refuse to destroy? (No. The flag left with the block.)
  4. ignore_changes = [input] and only input changes. Does the plan show an update? (No. "No changes.")
  5. Same resource, triggers_replace changes and input is ignored. Does replace still happen? (Yes. Actions ["delete", "create"].)

§VII. Close

Lifecycle meta-arguments are surgical. create_before_destroy reorders (+/- versus -/+). prevent_destroy blocks only while it remains in the file. ignore_changes hides listed attributes and leaves ForceNew fields alone. Ops tonight needs the first two rarely and the third only for true out-of-band fields. Format immutability stays a replace.

Related