Terraform Lifecycle Meta-Arguments: Replacement, Guardrails, and Drift Ownership
Use Terraform lifecycle rules deliberately: plan safe replacements, understand destroy protection limits, and assign clear ownership for ignored changes.
Terraform lifecycle meta-arguments change how a managed resource is planned, replaced, protected, or reconciled. They are powerful because they affect the dependency graph and the actions Terraform may take against real infrastructure. A lifecycle rule is not a substitute for a reviewed plan, a tested recovery path, or a clear ownership model between Terraform and external controllers.
This guide focuses on four common resource lifecycle controls: create_before_destroy, prevent_destroy, ignore_changes, and replace_triggered_by. Terraform also supports other lifecycle-related capabilities, and provider resource types vary in what they support. Confirm the current language reference and the specific provider resource documentation before applying any rule to a production object.
Lifecycle processing happens early
Lifecycle configuration influences graph construction and traversal. Terraform processes it before it can evaluate arbitrary expressions, so lifecycle settings must use literal values rather than conditional expressions or dynamically computed lists. The syntax may look like normal HCL, but its evaluation boundary is different from ordinary resource arguments.
resource "example_service" "api" {
name = "api"
lifecycle {
create_before_destroy = true
prevent_destroy = true
ignore_changes = [tags["managed-by-controller"]]
}
}
This is syntactically illustrative, not a recommendation to combine the three rules. Each changes behavior in a different way and can make a change harder to apply. Prefer the smallest lifecycle rule that expresses a documented operational requirement. Record why it exists, who owns the affected fields, and how it will be removed.
Lifecycle semantics are not a generic switch that behaves identically for every Terraform block. The language reference lists behavior and supported block types; provider documentation defines resource-specific replacement behavior. Review both before relying on a lifecycle rule for a resource whose deletion, replacement, or mutability has high impact.
create_before_destroy: overlap is part of the design
Terraform normally destroys an object before creating its replacement when a changed argument cannot be updated in place. create_before_destroy = true requests the reverse order: create the new object first, then destroy the old object. This can reduce downtime, but only if the remote system can host both objects concurrently and the surrounding dependencies tolerate overlap.
Before enabling it, answer concrete questions:
- Can old and new objects exist at the same time without colliding on a unique name, IP address, listener, or external identifier?
- Can downstream resources temporarily reference both instances, or must traffic switch at a specific point?
- Is there enough quota, subnet capacity, disk capacity, or licensing headroom for the overlap?
- How will the new object become healthy before clients are directed to it?
- What is the rollback action if creation succeeds but health validation or a later dependency fails?
Some resources expose a provider-specific option such as a generated name suffix that enables parallel creation. Terraform cannot assume or invent this option. Inspect the plan and provider documentation; do not use create_before_destroy merely because “create first” sounds safer.
The rule can propagate through dependencies. If resource A requires create-before-destroy and depends on resource B, Terraform can implicitly apply the behavior to B and record it in state. You cannot necessarily set B back to false when doing so would create a dependency cycle. A lifecycle decision on one resource can therefore affect a larger graph than the block where it appears.
Provisioners have an additional caveat: Terraform does not run destroy-time provisioners for a resource that has create_before_destroy enabled. If cleanup depends on such a provisioner, design an explicit managed cleanup or migration process rather than assuming it will execute during replacement.
Use a plan to inspect both replacement ordering and dependency effects:
terraform plan -out=change.tfplan
terraform show change.tfplan
The plan is evidence about Terraform’s proposed operations, not proof that the provider or remote API will make the new object healthy. In a non-production workspace, test the overlap path and the failure path, including what happens if the apply is interrupted between creation and destruction.
prevent_destroy: a configuration guard, not an indestructible resource
prevent_destroy = true causes Terraform to reject a plan that would destroy the associated object while the lifecycle rule remains in its configuration. It is useful as a deliberate guardrail for expensive or stateful objects, but it is not a complete deletion lock.
Terraform does not persist most lifecycle rules as an independent policy that survives configuration removal. If the resource block and its lifecycle rule are removed, Terraform can plan to destroy the object because the configuration no longer declares it. The rule also cannot stop an administrator, another automation system, or a provider-side operation from deleting the remote object outside Terraform.
That boundary changes how teams should use it:
- Keep the resource and guardrail in a reviewed, protected configuration path.
- Treat a plan failure caused by
prevent_destroyas a prompt to review intent, not as an obstacle to bypass mechanically. - Use cloud IAM, organizational policy, backup protection, or provider-specific deletion protection when the remote API needs an independent control.
- Follow the documented state-removal process when transferring ownership without deleting the object.
- Test the intended replacement and decommissioning procedure in a safe environment.
prevent_destroy can also block legitimate updates when the provider must replace the object to implement a change. That may be intentional for a database or object store, but it can make some configuration changes impossible until an operator deliberately changes the guardrail. Avoid scattering it across every resource: excessive use turns normal maintenance into a repeated manual exception and can encourage unsafe workarounds.
ignore_changes: define shared ownership narrowly
Terraform normally compares configured arguments with the refreshed remote object and plans changes to bring managed settings back toward configuration. ignore_changes tells Terraform not to propose update changes for specified resource attributes. Terraform still considers those arguments when creating the object; the ignore behavior is for subsequent updates.
A valid use case is an attribute deliberately managed by another controller after initial creation. For example, a cloud controller may add a provider-generated tag, or a deployment controller may own a replica count while Terraform owns the workload template. The important requirement is a clear, bounded division of responsibility.
resource "example_instance" "worker" {
name = "worker-a"
instance_type = "compute-standard"
tags = {
environment = "production"
owner = "platform"
}
lifecycle {
ignore_changes = [tags["last-scaled-by"]]
}
}
Here the example ignores one externally owned map entry rather than all tags. Whether a provider resource supports that exact attribute path depends on its schema; validate it against the provider documentation and a plan. If Terraform owns instance_type but an operator changes it manually, ignoring the entire resource’s drift would hide an important configuration deviation.
Prefer the narrowest attribute path the resource schema allows. ignore_changes = all is especially broad: Terraform can still create and destroy the object, but it will not propose updates to any of its attributes. This can make the configuration look authoritative while the remote object’s actual behavior is controlled elsewhere.
An ignore rule does not reconcile two owners. It suppresses one part of Terraform’s update response. Define which system is authoritative, how that system reports failure, whether Terraform should use the value at creation time, and how the team detects unexpected drift. Document an owner for removing the ignore rule when the external controller no longer manages the field.
Do not use ignore_changes to make a noisy plan disappear without explaining the difference. For a deliberate exception, keep a code comment that names the external writer and tracking reference, and add a monitoring or policy check that can detect an invalid value. If no other system is supposed to own the attribute, fix the configuration or investigate why the remote API keeps changing it.
replace_triggered_by: connect replacement to managed changes
replace_triggered_by causes a resource to be replaced when a referenced managed resource or resource attribute has a relevant planned change. This is useful when two objects are operationally coupled but the Terraform expression graph alone does not say that one must be recreated when the other changes.
resource "example_service" "api" {
image = var.image
}
resource "example_monitor" "api" {
target = example_service.api.id
lifecycle {
replace_triggered_by = [example_service.api]
}
}
The example says that a planned change to the service can trigger replacement of the monitor. Use an attribute reference when only a particular planned attribute change should trigger replacement. Understand the provider’s update-versus-replace behavior for both resources; “a value changed” and “the remote object will be replaced” are not equivalent in every plan.
The argument references managed resources because the trigger is based on planned resource actions. Plain values such as local values and input variables do not have their own planned actions, so they cannot be used directly as a replacement trigger. When a plain value needs resource-like lifecycle behavior, Terraform’s built-in terraform_data resource can track that value and then be referenced by replace_triggered_by:
variable "bootstrap_revision" {
type = string
}
resource "terraform_data" "bootstrap_revision" {
input = var.bootstrap_revision
}
resource "example_service" "api" {
name = "api"
lifecycle {
replace_triggered_by = [terraform_data.bootstrap_revision]
}
}
This does not make every variable change a safe reason to replace a service. Treat the input as a versioned operational contract: explain what it means, why a change requires replacement, and how much parallel capacity the replacement needs. Avoid turning arbitrary release identifiers or timestamps into triggers, which can force unnecessary replacement on every run.
Do not confuse lifecycle behavior with other Terraform controls
Lifecycle rules solve different problems from depends_on, moved blocks, import, and state removal. depends_on orders actions when a hidden dependency exists; it does not by itself force a replacement. A moved block changes a resource address mapping and preserves identity in state; it does not mean the remote object should be recreated. A removed block can hand state ownership away while preserving the object, when configured for that purpose. Choose the control that matches the operation rather than reaching for a lifecycle rule because it changes plan output.
Likewise, a lifecycle rule is not a backup, transaction, or service-level availability guarantee. create_before_destroy cannot guarantee health. prevent_destroy cannot protect against every external delete path. ignore_changes cannot make external control safe by itself. replace_triggered_by cannot ensure that the new object is compatible with its consumers. A production change still needs state locking, plan review, observability, and a tested recovery route.
Review and test a lifecycle change
For each lifecycle rule, write down the exact state transition it should cause and inspect a plan that exercises it. A practical review should include:
- The current object address and the desired configuration change.
- Whether the provider plans an in-place update, replacement, or no update.
- Any dependency propagation or temporary duplicate objects.
- Quota, naming, connection, and data-migration requirements during overlap.
- Which fields are owned by Terraform and which are owned by an external system.
- What happens if apply stops after each significant operation.
- How to roll back or deliberately transfer state ownership.
Save and inspect the exact plan reviewed by the team rather than approving a new, unreviewed plan generated later. For high-impact changes, use a disposable environment that exercises provider behavior, and compare the resulting remote state with the expected lifecycle. Provider upgrades should also rerun these checks because schemas and replacement requirements can change.
Before applying, ensure the state is backed up according to the backend’s documented recovery process and that only the intended workspace and account are selected. After applying, verify the remote object, the refreshed state, application health, and the absence of unintended replacements. A clean exit code is necessary but not sufficient evidence of safe lifecycle behavior.
Production acceptance checklist
- The lifecycle rule has one explicit, documented purpose and a named owner.
- The exact provider resource type supports the chosen setting and attribute path.
- A saved plan demonstrates the expected action and dependency order.
- Create-before-destroy overlap fits remote naming, quotas, health checks, and capacity.
- Destroy-time provisioner assumptions have been removed or replaced with an explicit process.
- Prevent-destroy is paired with an independent remote deletion control when required.
- Ignore-changes is scoped to the smallest externally owned attribute and has drift visibility.
- Replacement triggers reference managed changes or intentionally use
terraform_datafor a plain value. - State backup, interruption behavior, rollback, and ownership handoff are understood.
- Post-apply verification checks the provider object and the service outcome, not only Terraform’s status.
Lifecycle meta-arguments make plans encode operational intent, but they also narrow or redirect Terraform’s behavior. The safest configuration is the one where the plan is predictable, ownership is unambiguous, and an operator can explain both the expected path and the failure path before applying it.
Related:
- Terraform Moved Blocks: Safe Resource Renames and Module Refactors
- Terraform removed Blocks: Decommission and Hand Off State Safely
Sources: