Hedronite · Cert Lesson · Cert-Prep / HashiCorp · Wed 2026-09-02

Terraform Pro replace_triggered_by — the replace that is not a diff

Upstream moved. This resource's arguments did not. Core will not invent the edge.

Lesson Class: Cert (Terraform Pro · Lab 32 replace_triggered_by)
Cloud Referent: Lab 32 terraform_data; overlay is Azure App Service + versionless Key Vault URI
Paired Ops: versionless URI + empty plan after rotation
Paired Dev: trigger list grammar + terraform_data marker
Grounding: Lab 32 real · Lab 22 referenced as 08-12 · Lab 18 rejected as 08-30
The index
No. Today's hole is an empty plan, not [0].
The list
Managed addresses. Variable in the list is the lab.
The control
Plan B: version bump replaces the service. Owner bump does not.
Upstream moved. This resource's arguments did not. Core will not invent the edge.

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

§I — Frame

Associate-003 is closed. Six Pro fires on this arc opened run tasks, remote unlock, test-run commands, Lab 14's sockets, Lab 08's IAM chain, and Lab 18's count-zero index. Lab 32 is the leftover that matches today's overlay.

Lab 32's README is the exam: force safe replacement when upstream dependency changes are not automatically represented as direct argument drift. The starter already names the type. Two terraform_data resources. A marker. A service. A lifecycle block that ignores owner and replaces when the marker moves. Success mode is plan. AWS cost risk is none. The lab is not "create an App Service." The lab is the replace when the service's own arguments did not move.

Coin it: the replace that is not a diff. On Ops the missing edge is a versionless Key Vault URI that sits still while azurerm_key_vault_secret.version moves. On Dev the list that names the marker is the HCL. On Lab 32 the optional cloud shirt is gone on purpose: terraform_data is Core, versions.tf still mentions aws because the lab family defaults to it, and the resources never use the plugin. The expression does not care. An empty plan after a version bump is the fail. A replace of terraform_data.service after that bump is the pass.

This is not 08-30. That lesson's command was whether the object exists versus whether the output can still be evaluated. [0] on count 0. Today's command is whether the object should be rebuilt versus whether any of its arguments moved.

This is not 08-24. That lesson's command was validate versus a module call that forgot the providers map. Today's command is plan versus a missing graph edge.

This is not 08-21. That lesson's command was plan versus apply and an output that was still last apply. Lab 29 owned that depth. Lab 32's tests/basic.tftest.hcl is one plan run. It checks that name and version are present. It does not reopen last-apply. If a stem writes terraform apply then terraform test -filter=plan, look at which lab you are in.

This is not 08-12's Cert slot. That fire closed Associate objectives 1 and 2 on the execution graph. Ops that day named Labs 22, 23, and 32 together. Cert did not drill 32. Today drills 32. Do not answer a Lab 32 stem with name_prefix and min_elb_capacity. Those are Lab 22 wearing 08-12's ASG shirt.

§II — Domain Foundations: a replace the arguments will not request

Brikman, Zero-Downtime Deployment (Ch.5, pp.271-275), is the sibling. Terraform's default replace is destroy then create. create_before_destroy flips the order. Unique names collide. 08-12 spent that paragraph on an ASG. Lab 32's starter still sets the flag. Acknowledge it. Do not reopen the collision.

The Pro leftover the 3ed does not name is the case where destroy-then-create versus create-then-destroy is not the question, because the plan has no replace at all. Upstream moved. This resource's arguments did not. Core will not invent the edge. replace_triggered_by is how the author writes it.

The Bootcamp README names the skills in one list: identify dependency changes that should trigger replacement, wire replace_triggered_by to explicit change signals, confirm replacement behavior in terraform plan. The starter already has the three lifecycle lines. The work is to make the wiring match the isolation: owner is not a reason; version is.

resource "terraform_data" "release_marker" {
  input = {
    version = var.release_version
  }
}

resource "terraform_data" "service" {
  input = {
    name    = var.service_name
    version = var.release_version
    owner   = var.external_owner
  }

  lifecycle {
    create_before_destroy = true
    ignore_changes        = [input["owner"]]
    replace_triggered_by  = [terraform_data.release_marker]
  }
}

Three lines, three labs. create_before_destroy is Lab 22 sitting next door. ignore_changes is Lab 23 sitting next door. replace_triggered_by is today's line. A stem that asks only about owner tags is Lab 23. A stem that asks only about two generations of a named resource is Lab 22. A stem that says the plan is empty after an upstream version bump is Lab 32.

-replace is a cousin, not a synonym. terraform apply -replace=terraform_data.service forces a replace for one run. The lifecycle block forces it for every run where the marker moves. The exam will offer the CLI flag as the standing procedure. The standing procedure is the block. Ops already refused az webapp restart as a census write. Same altitude: do not make the operator remember a flag.

§III — Authoring flavor: the address in the list

The list holds managed addresses. terraform_data.release_marker is legal. terraform_data.release_marker.output is legal and tighter. var.release_version is not a managed address. local.version is not. A boolean is not.

If the author leaves version inside service.input, a version bump is also argument drift on the service. The trigger still fires. The isolation is dirty. Dev named the cleanup: take version off the service input so the only reason the service replaces is the list. A Pro stem that shows both and asks "why did the service replace" has two true answers. Pick the stem's emphasis. If the stem says "no argument of the service changed," the file has already removed version from service.input. If the stem shows the starter as shipped, both reasons are present.

ignore_changes = all on the service would swallow input.version as well. Then the trigger is the only remaining reason, and if the trigger points at the variable, you have an empty plan. Scoped ignore is the starter's design. Copy it.

Ops's Azure shirt is the same grammar on a plugin resource. azurerm_linux_web_app with a versionless Key Vault URI in app_settings has no argument drift when the secret version rotates. replace_triggered_by = [terraform_data.release_marker] or [azurerm_key_vault_secret.app.version] is the edge. Do not turn Lab 32 into an azurerm lab. Mention the overlay so the stem that says "App Service still serves the old connection string after a Key Vault rotation" maps. Then return to terraform_data. Cost risk on the lab is none for a reason.

§IV — Operations flavor: plan is the room

Lab 32 success mode is plan. tests/basic.tftest.hcl uses command = plan. 08-21's Lab 29 used plan versus apply to catch an output that was still last apply. Do not import that. Today's plan is the first instrument that can show a replace you asked for. Apply is how you hide: you -replace once, the lab goes green, and the block was never wired.

terraform validate is necessary and not sufficient. A trigger list that points at a legal address validates. Whether that address moves when the version moves is a plan question. Validate cannot see var.release_version change. Plan can.

HCP Terraform is adjacent, not today's drill. Lab 25 was 08-15 (run tasks) and 08-09 (Sentinel). A speculative plan will show the same replace the local plan shows, if the block is wired. Do not answer Lab 32 with a run-task. Do not answer a run-task stem with replace_triggered_by.

prevent_destroy plus replace_triggered_by in the same block is a fight. The trigger asks for destroy-and-create. The prevent asks to refuse destroy. The exam will stack them. Lab 32's starter does not. Do not stack them to look thorough.

HCP Terraform's remote plan is the same plan. If the block is wired, the run shows a replace of terraform_data.service when the marker's version input changes. If the block is missing, the remote plan is as empty as the local one. Do not treat a green HCP run as evidence the rotation reached the worker. Green can mean "no diff," which is today's fail. Read the plan verbs. Replace of the service on a version bump is the verb you want. No-op is the verb you do not.

Lab 32's versions.tf requires Terraform >= 1.6 and still declares hashicorp/aws. The resources never use it. A candidate who adds an aws_instance to "make the lab real" has left the success-mode room (stateless, cost none) and has also created argument drift the moment AMI or user_data moves. Stay on terraform_data. The overlay lives in Ops.

§V — Worked Example

Two files, one lab. main.tf as shipped. tests/basic.tftest.hcl as shipped. terraform test runs the plan. Name and version present. That run is the smoke.

The actual exam work is the two mental plans Dev named.

Owner changes, version does not. Ignore on input["owner"]. Marker still. Service should not replace. If the plan replaces the service, you pointed the trigger at something owner also changes, or you dropped the ignore.

Version changes, owner does not. Marker replaces. Service replaces via the list. If the plan is empty, the list is missing, pointed at the variable, or pointed at an attribute that did not move.

Overlay check, not a lab step. Versionless URI on a Web App, secret version bump, empty plan. Same fail. Same fix. Different type. The azurecheatsheet names App Services and Key Vault on two rows. The join is the URI. The edge is the list. Then close the overlay. Lab 32 has no Key Vault.

Do not terraform apply -replace=terraform_data.service to green the owner-change plan. That hides the ignore. Do not apply at all if the success mode is plan. 08-30's Q4 already caught the candidate who applies so [0] exists. Today's cousin is the candidate who -replaces so the next plan looks like a rotation already happened.

Write the second plan as a test run if you want it on disk. A run "version_bump_replaces_service" with variables { release_version = "v2" } and an assert on terraform_data.service wanting replacement is the control Lab 32's shipped test does not include. The shipped test is smoke. The control is yours. Dev called it Plan B. Put it next to the smoke, same file, two runs, the way Lab 18 shipped two plan runs for presence and absence. Do not apply to create the control. Plan is the room the control fits in.

§VI — Connection to Today's Ops + Dev Lessons

Ops wrote the versionless URI and treated an empty plan as a finding. Dev wrote the address grammar and the four mistakes (variable, local, dirty isolation, all). Cert reads the lab that already holds both terraform_data objects and asks which line is the exam.

If Cert only restates App Service vocabulary, it has skipped Lab 32. If Dev only wraps lifecycle {} without the list typing, it has skipped the address. If Ops only prints the Web App name, it has skipped the secret id. The hinge is absence of a diff at three altitudes. Versionless URI. Trigger list of managed addresses. terraform_data.service replaced when the marker moves and not when owner moves.

Five traps, one line each, so a stem can name them. Variable in the list. Local in the list. Version still on service.input so you cannot tell trigger from drift. ignore_changes = all. -replace as the standing procedure. Any one of them produces a plan that lies about a rotation. The lie is empty, which is why it passes a careless eye.

08-21's plan-versus-apply remains in the other room. A Lab 32 plan run that shows the replace still has not applied. That is correct. You do not apply a terraform_data to prove you can see a replace.

§VII — Practice Questions

Question 1
Lab 32's starter plans after external_owner changes. The service replaces. Which line is missing or wrong?
tap to reveal
ignore_changes = [input["owner"]] is missing, or replace_triggered_by points at something owner also changes. Owner is Lab 23 sitting next door. Version is today's trigger. Do not replace the service for a portal tag.
Question 2
A candidate writes replace_triggered_by = [var.release_version]. Validate fails, or the plan stays empty after a version bump. Why?
tap to reveal
A variable is not a managed address. Point the list at terraform_data.release_marker or at a resource attribute. The marker consumes the variable. The service watches the marker.
Question 3
Ops's Web App uses @Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.app.id}) (versioned) and also sets replace_triggered_by on the secret version. After a rotation the plan shows an in-place update of app_settings and a replace. What was skipped?
tap to reveal
The versionless URI. Today's coin is no argument drift. A versioned .id is a diff. Two reasons to move. You will not know which one the plan used. Use versionless_id plus the trigger.
Question 4
Lab 32's tests use command = plan. A candidate runs terraform apply -replace=terraform_data.service first so the next plan is empty, then runs the tests. Has the lab been completed?
tap to reveal
No. Success mode is plan. The skill is seeing the replace the arguments did not request. -replace is a one-run override and hides the block. 08-30 Q4 was the same hide on count 0.
Question 5
A stem stacks prevent_destroy = true with replace_triggered_by on the same resource, then bumps the marker. What happens?
tap to reveal
A fight. The trigger asks for destroy-and-create. The prevent refuses destroy. Lab 32's starter does not stack them. Lab 01 owns prevent. Do not add it to look complete.

§VIII — Closing

An upstream can move while this resource's arguments sit still. Core will not invent the edge. replace_triggered_by is the edge. The list holds managed addresses. Owner is ignored. Version lives on the marker. Plan is enough. -replace is how you hide.

Name it when you see it. The replace that is not a diff. Write the marker. Point the list at the marker. Leave owner ignored. Leave Lab 22's flag if two generations must overlap. Ask the plan for a replace on version. Ask the plan for quiet on owner.

Examine well. The service output will still be pretty. The latch is the marker. The door is the list. The plugin will not help you. Core will, if the address is real.

A last habit. Read the test file before you rewrite the resource. Lab 32 already authored a plan run. If your local terraform plan looks clean because you -replaced by hand, you have not run the lab. Run terraform test. Watch lifecycle_meta_arguments_are_present. Then change version in a second plan of your own. That second plan is the isolation. Dev named it Plan B. Ops named it the empty plan after a rotation. Same fact, three rooms.

Related