Terraform lifecycle Patterns: create_before_destroy, prevent_destroy, ignore_changes, and replace_triggered_by
lifecycle Changes Terraform's Behavior, Not Provider Reality
The lifecycle block is a precision tool for resource replacement and ownership boundaries. It cannot make an API support two objects with the same unique name, turn an in-place provider update into a zero-downtime deployment, or protect a resource after its block disappears from configuration.
Use lifecycle rules only with a clear invariant and test the plan. The lifecycle safety lab combines all four patterns against disposable FakeCloud resources.
---
create_before_destroy: Preserve Capacity During Replacement
Terraform normally destroys an object before creating its replacement when both cannot coexist by default. For a service instance, that may cause an outage:
resource "fakecloud_instance" "api" {
name = "checkout-api-${var.release_id}"
image = var.image
size = var.size
lifecycle {
create_before_destroy = true
}
}
Terraform creates the replacement, updates dependencies, then destroys the old object. This requires spare quota and a naming scheme that permits coexistence. If the provider enforces a unique fixed name such as checkout-api, creation still fails. Pair the rule with release-specific names, load balancer registration, and health checks where the provider supports them.
Review propagation through dependencies: Terraform may preserve create-before-destroy behavior for dependent resources to avoid cycles. A plan is the authority, not an assumption based on one block.
---
prevent_destroy: A Guardrail With Boundaries
For a stateful production database:
resource "fakecloud_database" "orders" {
name = "orders-prod"
engine = "postgres"
size_gb = 500
lifecycle {
prevent_destroy = true
}
}
Terraform rejects a plan that would destroy or replace this resource. That catches a changed force-new argument and an explicit terraform destroy while the block remains present.
It is not an external deletion lock. It does not stop an operator deleting the database in a cloud console, and removing the entire resource block also removes the lifecycle rule from configuration. Combine it with provider-native deletion protection, backups, access control, policy checks, and review.
When destruction is intentional, remove the rule in a separately reviewed change, plan, verify backups, then perform the deletion. Do not normalize bypassing safeguards during an emergency.
---
ignore_changes: Define Shared Ownership Narrowly
Suppose an autoscaler changes desired capacity while Terraform owns the rest of the service:
resource "fakecloud_instance" "worker_pool" {
name = "worker-pool"
replicas = var.initial_replicas
size = var.size
lifecycle {
ignore_changes = [replicas]
}
}
Terraform uses replicas during creation but ignores later remote changes for update planning. This is a legitimate ownership boundary.
Bad uses include suppressing unexplained drift, ignoring security settings, or adding all to silence a noisy provider. Every ignored attribute is an attribute Terraform will stop correcting. Add a comment naming the external owner and monitor that owner separately.
Ignoring one nested field requires the provider schema's exact attribute path. Validate with a plan after both Terraform changes and external-controller changes.
---
replace_triggered_by: Rebuild on a Meaningful Dependency Change
References create ordering, but a dependency update does not always replace its consumer. replace_triggered_by makes replacement explicit:
resource "terraform_data" "bootstrap_version" {
input = var.bootstrap_version
}
resource "fakecloud_instance" "runner" {
name = "ci-runner"
image = var.base_image
lifecycle {
replace_triggered_by = [terraform_data.bootstrap_version]
}
}
When the bootstrap version changes, Terraform plans to replace the runner even if no provider argument changed. This suits immutable instances whose initialization runs only at creation.
Use resource references here, not arbitrary expressions. terraform_data can represent a plain value as a managed lifecycle signal. Avoid timestamps and constantly changing hashes; they create perpetual replacements.
---
precondition and postcondition: Assert What Must Be True
Lifecycle also supports custom conditions:
resource "fakecloud_certificate" "api" {
domain = "api.example.com"
ca_id = var.ca_id
lifecycle {
precondition {
condition = endswith(var.ca_id, "-prod")
error_message = "Production certificates must use the production CA."
}
postcondition {
condition = self.expires_in_days > 30
error_message = "Issued certificate has less than 30 days of validity."
}
}
}
Conditions turn assumptions into plan or apply failures close to the affected resource. Variable validation checks an input in isolation; a precondition can combine values and data; a postcondition validates provider results.
Review Questions
Before approving any lifecycle rule, ask:
- Which failure does this prevent?
- Can the provider support the requested behavior?
- Who owns ignored attributes?
- Does protection survive deletion outside Terraform?
- What is the reviewed escape procedure?
- Does a representative plan prove the intended ordering?
Lifecycle rules are most valuable when they encode a sentence the team already agrees on: keep old capacity until replacement is ready; never destroy this database casually; let the autoscaler own replica count; rebuild runners when bootstrap changes.