Lifecycle Meta-Arguments
The lifecycle block changes how Terraform creates, replaces, and destroys a resource without changing the resource itself. It is the escape hatch for the cases where Terraform's defaults — destroy-then-create on replacement, always converge every attribute back to config — would cause an outage or fight another system that has a legitimate claim on the resource.
Four arguments do the work: create_before_destroy, prevent_destroy, ignore_changes, and replace_triggered_by. Each one is small, but each rewrites a default that is otherwise unavoidable — and using the wrong one, or scoping it too broadly, causes its own class of subtle bug. They are precision tools, not knobs to turn preemptively.
create_before_destroy
When a change forces replacement, Terraform's default is destroy-then-create: it deletes the old object, then creates the new one. For anything fronting live traffic, that ordering means downtime — the resource is gone for the duration of the recreate. create_before_destroy = true flips it: build the new one first, cut over, then destroy the old. That is how you replace a resource serving requests without a gap, provided GCP allows two to exist briefly and their names do not collide.
resource "google_compute_instance_template" "processor" { # ... template config ... lifecycle { create_before_destroy = true } }
prevent_destroy
prevent_destroy = true is a guardrail: it makes Terraform error rather than destroy the resource. Put it on anything whose deletion is catastrophic — the analytics BigQuery dataset, the Terraform state bucket, a production database — so that an accidental terraform destroy, or a change that would force a replacement, is refused instead of executed. It does not prevent changes, only deletion, and it surfaces the danger at plan time rather than after the data is gone.
The catch is that prevent_destroy also blocks a forced replacement, since replacement deletes the old object. When an unrelated change forces the dataset to be replaced, the run errors — which is the point, but it can surprise someone who did not realize their change was destructive. That surprise is the guardrail doing exactly its job.
ignore_changes
ignore_changes tells Terraform to stop reconciling specific attributes back to config. The canonical GCP use is ignore_changes = [labels] on a resource that another system stamps labels onto — a cost-allocation tool, a CI pipeline, GKE adding its own labels — so that each apply does not strip what the other system added. Without it, Terraform sees the external labels as drift and removes them on every run, and the two systems fight forever.
Scope matters enormously here. Ignoring the whole labels map stops Terraform from managing any label, including the ones you set in config — so a real label change you make never applies, silently. The right scope is the exact key the external system owns: ignore_changes = [labels["managed-by"]] ignores that one key and keeps managing the rest.
resource "google_storage_bucket" "raw" { name = "hatch-events-raw" location = "US" lifecycle { ignore_changes = [labels["managed-by"]] # keep managing every other label } }
ignore_changes for Externally-Managed Fields
Labels are the common case, but the pattern is broader. Autoscalers adjust instance counts, GCP populates server-assigned fields you never set, and external automation mutates metadata after creation. Each produces a perpetual diff — Terraform wants to undo a change made by a system that is supposed to make it. Listing exactly those attributes in ignore_changes stops the diff without abandoning management of the rest of the resource. Ignore the autoscaled field, keep managing everything else.
replace_triggered_by
replace_triggered_by forces replacement of a resource when another referenced resource or attribute changes, even though this resource's own arguments did not. The use case is when an in-place update is not enough — you want event-processor recreated whenever a referenced config object or image revision changes, not merely updated. It is the right tool only when a fresh resource is genuinely required; reach for it when a plain reference would have caused a harmless in-place update and you have forced an unnecessary destroy/create instead.
ignore_changes stops Terraform from updating the listed attributes back to config, while still managing the rest of the resource. It silences a perpetual diff on a field another system owns.
prevent_destroy stops Terraform from deleting the resource at all and errors instead. It is a deletion guardrail. The two solve unrelated problems — one quiets a field-level diff, the other refuses a destroy — and reaching for the wrong one leaves the actual problem unsolved.
- Omitting
create_before_destroyon a resource that fronts live traffic when an argument forces replacement — Terraform destroys it first and the dependents go down for the duration of the recreate. - Using
ignore_changes = [labels](the whole map) when only one external label needs ignoring — you also stop managing the labels you do control, so a real label change in config silently never applies. - Putting
prevent_destroyon theanalyticsdataset and then being blocked mid-apply when an unrelated change forces its replacement — the run errors and you discover the forced replacement only because the guardrail caught it. - Setting
ignore_changeson an attribute and forgetting it, then wondering why a genuine config change to that attribute never takes effect — ignored means ignored, including your intended edits. - Reaching for
replace_triggered_bywhen a plain reference would have caused an in-place update anyway — you force an unnecessary destroy/create instead of letting the normal diff update in place. - Adding
create_before_destroyto a resource whose name must be globally unique without changing the naming scheme — the new and old objects collide on the name and the create fails.
- Add
create_before_destroy = trueto any resource whose replacement would otherwise interrupt live traffic, after confirming GCP allows two to exist briefly and names do not collide. - Put
prevent_destroy = trueon stateful, hard-to-rebuild resources — theanalyticsdataset, thehatch-events-rawbucket, the state bucket. - Scope
ignore_changesto the exact attribute or label key managed externally (ignore_changes = [labels["managed-by"]]), never the wholelabelsmap, so you keep managing the rest. - Use
replace_triggered_byonly when a referenced change genuinely requires a fresh resource and an in-place update cannot express it. - Comment every
lifecycleblock with why it exists, since each one overrides a safe default and the reason is rarely obvious to the next reader.
protect, ignoreChanges, and deleteBeforeReplace map almost one-to-one
Config Connector uses field managers and the state-into-spec annotation for similar control
gcloud has no equivalent — lifecycle control exists only where a stateful tool tracks desired state
Knowledge Check
What does create_before_destroy = true change about a forced replacement?
- Terraform builds the new object before destroying the old one, avoiding a gap for resources serving live traffic
- It blocks the forced replacement entirely and raises a hard error at plan time instead of ever destroying the resource
- It destroys and recreates the resource twice over for extra safety
- It converts the forced replacement into an in-place attribute update
Why is ignore_changes = [labels] on the whole map usually too broad?
- It also stops Terraform from managing the labels you set in config, so a real change silently never applies
- It prevents the resource from ever being destroyed by any future apply
- It forces every single label change to recreate the entire resource
- It applies only to the labels added by GCP itself or by external tools, never to the ones you declare in config
A change forces replacement of a dataset that has prevent_destroy = true. What happens?
- The run errors and refuses to proceed, because replacement requires deleting the protected resource
- Terraform silently skips the protected dataset, leaves it untouched, and applies all the other planned changes
- The dataset is replaced anyway, since
prevent_destroyonly blocks an explicitterraform destroycommand - Terraform downgrades the change to an in-place update to avoid the deletion
When is replace_triggered_by the right tool rather than a plain reference?
- When a referenced change genuinely requires a fresh resource and an in-place update cannot express it
- Whenever you want a change in a referenced resource to update this one in place
- When you want to stop a critical resource from ever being destroyed
- When an external system, rather than Terraform itself, takes over managing one of the resource's attributes
You got correct