| Tested on | Ubuntu 26.04 LTS (Resolute Raccoon) |
|---|---|
| Package | terraform 1.15.8-1hashicorp/local 2.6.1 |
| Applies to | Any host with Terraform installed |
| Lab environment | Single Ubuntu VM with Terraform — Terraform lab environment on Ubuntu |
| Privilege | Normal user; sudo only if Terraform is not installed yet |
| Scope | Terraform lifecycle management rules — create_before_destroy, prevent_destroy, ignore_changes, replace_triggered_by, replacement ordering with local_file, drift handling, lifecycle vs depends_on, documented rule interactions, and common mistakes. Does not cover lifecycle precondition and postcondition, action_trigger, dependency graph depth, moved or removed blocks, state manipulation, import, or general destroy workflows. |
| Related guides | Terraform resource dependencies Terraform resource block Terraform count vs for_each Terraform Associate certification course |
Most resource arguments describe what infrastructure should look like. A lifecycle block describes how Terraform should manage that resource when plans require create, update, replace, or destroy operations:
resource "local_file" "example" {
filename = "${path.module}/example.txt"
content = "hello"
lifecycle {
create_before_destroy = true
}
}Lifecycle rules do not change provider API arguments. They change Terraform's behavior during diff, plan, and apply — replacement order, destruction guards, ignored drift, and forced replacement triggers.
Each hands-on demo uses its own subdirectory under ~/terraform-labs/terraform-lifecycle/ so before-and-after plans stay isolated.
hashicorp/local provider (local_file) and the built-in terraform_data resource.
What Terraform lifecycle rules control
depends_on answers when Terraform may start an operation relative to another resource. Lifecycle meta-arguments answer how Terraform should treat this resource during those operations:
depends_on → dependency and ordering in the graph
lifecycle → rules for replacement order, destroy protection, ignored drift, forced replacementLifecycle settings apply to the resource instance that contains the block. They are not a separate resource type and they do not replace explicit or implicit dependencies.
Terraform lifecycle block syntax
Place these lifecycle management rules inside the lifecycle block of a managed resource block:
resource "<TYPE>" "<NAME>" {
# provider arguments …
lifecycle {
create_before_destroy = true
prevent_destroy = false
ignore_changes = [content]
replace_triggered_by = [terraform_data.trigger]
}
}Data sources also support a lifecycle block, but only for precondition and postcondition validation checks — outside this article's scope.
This guide focuses on the four lifecycle rules used to control resource replacement, destruction, and drift. Current Terraform also supports lifecycle validation checks and action_trigger; those belong to separate validation and actions workflows.
| Meta-argument | Purpose |
|---|---|
create_before_destroy |
Create replacement before destroying the old instance |
prevent_destroy |
Error when Terraform would destroy this instance |
ignore_changes |
Ignore changes to listed attributes (or all) when planning updates |
replace_triggered_by |
Replace this resource when a referenced resource, instance, or attribute has a qualifying planned change |
Only one lifecycle block is allowed per resource. A single block may set multiple meta-arguments together when their behaviors do not conflict.
create_before_destroy
When Terraform must replace a resource, the default order is destroy, then create. That appears in plan output as -/+ destroy and then create replacement.
Some resources cannot exist twice under the same unique name at the same time — load balancer names, fixed hostnames, or a single file path. create_before_destroy = true reverses the order to create, then destroy (+/- create replacement and then destroy), reducing downtime when the provider allows both objects to exist briefly.
Default destroy-then-create order
Create an isolated directory for the ordering demo:
mkdir -p ~/terraform-labs/terraform-lifecycle/create-before-destroy
cd ~/terraform-labs/terraform-lifecycle/create-before-destroyWrite a local_file at demo-v1.txt:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
resource "local_file" "demo" {
filename = "${path.module}/demo-v1.txt"
content = "version 1"
}
EOFInitialize and apply the first file:
terraform init -input=false
terraform apply -auto-approve -input=false -no-colorSample output:
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.Change filename to demo-v2.txt — filename is a force-new attribute for local_file, so Terraform must replace the resource:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
resource "local_file" "demo" {
filename = "${path.module}/demo-v2.txt"
content = "version 2"
}
EOFPlan with the default lifecycle — destroy first:
terraform plan -input=false -no-colorSample output:
-/+ destroy and then create replacement
# local_file.demo must be replaced
Plan: 1 to add, 0 to change, 1 to destroy.The -/+ prefix means Terraform destroys the existing demo-v1.txt object before creating demo-v2.txt. Between those steps the file at the old path is gone.
Create-before-destroy order
Add create_before_destroy = true to the same replacement change:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
resource "local_file" "demo" {
filename = "${path.module}/demo-v2.txt"
content = "version 2"
lifecycle {
create_before_destroy = true
}
}
EOFPlan again — the replacement order flips:
terraform plan -input=false -no-colorSample output:
+/- create replacement and then destroy
# local_file.demo must be replaced
Plan: 1 to add, 0 to change, 1 to destroy.Apply and watch apply logs create the new file before destroying the deposed object:
terraform apply -auto-approve -input=false -no-colorSample output:
local_file.demo: Creating...
local_file.demo (deposed object ...): Destroying...
Apply complete! Resources: 1 added, 0 changed, 1 destroyed.create_before_destroy does not remove the need for unique names where the provider enforces them. If only one object can hold a fixed name, create-before-destroy may still fail — Terraform reports the constraint instead of silently succeeding.
prevent_destroy
prevent_destroy = true tells Terraform to error when a plan or apply would destroy that resource instance.
Block explicit destroy
Use a separate directory for the protection demo:
mkdir -p ~/terraform-labs/terraform-lifecycle/prevent-destroy
cd ~/terraform-labs/terraform-lifecycle/prevent-destroyWrite a protected file resource:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
resource "local_file" "protected" {
filename = "${path.module}/protected.txt"
content = "do not delete"
lifecycle {
prevent_destroy = true
}
}
EOFInitialize and apply:
terraform init -input=false
terraform apply -auto-approve -input=false -no-colorRun terraform destroy — Terraform stops with an error:
terraform destroy -auto-approve -input=false -no-colorError: Instance cannot be destroyed
Resource local_file.protected 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.prevent_destroy guards against accidental terraform destroy runs and against plans that would delete the instance while the resource block still exists.
Configuration removal nuance
prevent_destroy does not make the resource immortal. If you remove the resource block from configuration, the next apply still plans to destroy the orphaned instance — and apply succeeds.
Delete the local_file block from main.tf, leaving only the terraform block, then apply:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
EOFApply the empty module — Terraform destroys the orphaned instance even though prevent_destroy was previously set:
terraform apply -auto-approve -input=false -no-colorlocal_file.protected: Destroying...
Apply complete! Resources: 0 added, 0 changed, 1 destroyed.prevent_destroy blocks destruction of a managed instance while Terraform still tracks it under a resource block. It does not prevent destruction caused by removing that block from configuration. Treat it as a guardrail against operator mistakes, not as a substitute for backup policy or access control.
ignore_changes
ignore_changes tells Terraform to ignore differences on listed arguments when planning updates. Terraform still refreshes remote objects during plan; it simply does not propose changes that would only realign ignored attributes to configuration.
Ignore selected attributes
Create an isolated directory:
mkdir -p ~/terraform-labs/terraform-lifecycle/ignore-changes
cd ~/terraform-labs/terraform-lifecycle/ignore-changesStart with a terraform_data resource:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
resource "terraform_data" "demo" {
input = "configured-value"
}
EOFInitialize and apply:
terraform init -input=false
terraform apply -auto-approve -input=false -no-colorFirst change the managed value and apply it so Terraform records changed-outside-terraform for the resource:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
resource "terraform_data" "demo" {
input = "changed-outside-terraform"
}
EOFApply that value to state:
terraform apply -auto-approve -input=false -no-colorRestore a different configured value in main.tf:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
resource "terraform_data" "demo" {
input = "configured-value"
}
EOFWithout ignore_changes, Terraform detects that the configured input differs from the currently managed value and plans an update:
terraform plan -input=false -no-colorSample output:
# terraform_data.demo will be updated in-place
~ resource "terraform_data" "demo" {
~ input = "changed-outside-terraform" -> "configured-value"
}
Plan: 0 to add, 1 to change, 0 to destroy.Add ignore_changes
Add ignore_changes = [input]. Terraform now ignores that argument difference when planning an update:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
resource "terraform_data" "demo" {
input = "configured-value"
lifecycle {
ignore_changes = [input]
}
}
EOFPlan again:
terraform plan -input=false -no-colorNo changes. Your infrastructure matches the configuration.In real infrastructure, ignore_changes is commonly used when an external system legitimately modifies an attribute. Terraform still refreshes remote state, but it does not propose an update merely to restore an ignored attribute to its configured value. For operational drift workflows when state must catch up before you change configuration, see drift and refresh-only. Only list attributes you truly share with external managers — overusing ignore_changes hides real problems and makes configuration a lie relative to production.
ignore_changes = all
Terraform also accepts the keyword all to ignore differences on every argument when planning updates:
lifecycle {
ignore_changes = all
}Use a separate directory to compare the same configured-vs-managed mismatch with ignore_changes = all:
mkdir -p ~/terraform-labs/terraform-lifecycle/ignore-changes-all
cd ~/terraform-labs/terraform-lifecycle/ignore-changes-allApply the baseline value:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
resource "terraform_data" "demo" {
input = "configured-value"
}
EOFInitialize and apply the baseline:
terraform init -input=false
terraform apply -auto-approve -input=false -no-colorChange the managed value and apply it so state records changed-outside-terraform:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
resource "terraform_data" "demo" {
input = "changed-outside-terraform"
}
EOFApply that value:
terraform apply -auto-approve -input=false -no-colorRestore configuration with ignore_changes = all:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
resource "terraform_data" "demo" {
input = "configured-value"
lifecycle {
ignore_changes = all
}
}
EOFPlan with every attribute ignored:
terraform plan -input=false -no-colorNo changes. Your infrastructure matches the configuration.ignore_changes = all is rarely appropriate for resources Terraform should fully own. It is mainly useful when a resource must stay registered in state but nearly every attribute is owned elsewhere. Prefer explicit attribute lists so future arguments you add to the block still participate in drift detection.
replace_triggered_by
replace_triggered_by forces replacement of the current resource when Terraform plans a qualifying change to a referenced managed resource, resource instance, or resource attribute — even when no argument on the current resource directly references that trigger.
Depending on what you reference, an in-place update or a replacement on the trigger can cause replacement of the current resource. If you reference a specific attribute, a change to that attribute alone can be enough.
Use a separate directory:
mkdir -p ~/terraform-labs/terraform-lifecycle/replace-triggered-by
cd ~/terraform-labs/terraform-lifecycle/replace-triggered-byWrite a trigger and a follower:
cat > main.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
variable "seed" {
type = string
default = "alpha"
}
resource "terraform_data" "trigger" {
input = var.seed
}
resource "terraform_data" "follower" {
input = "follows trigger"
lifecycle {
replace_triggered_by = [terraform_data.trigger]
}
}
EOFInitialize and apply:
terraform init -input=false
terraform apply -auto-approve -input=false -no-colorChange seed so the trigger resource updates:
terraform apply -var='seed=beta' -auto-approve -input=false -no-colorSample output:
# terraform_data.follower will be replaced due to changes in replace_triggered_by
Plan: 1 to add, 1 to change, 1 to destroy.
Apply complete! Resources: 1 added, 1 changed, 1 destroyed.The follower's input argument did not change. Terraform replaced terraform_data.follower because terraform_data.trigger received an in-place update from alpha to beta and was listed in replace_triggered_by — the trigger was not replaced, but the planned change still qualified.
References must point to resources in the same module. replace_triggered_by makes changes to one managed resource trigger replacement of another — distinct from depends_on, which only establishes ordering. For ordering without forced replacement, use depends_on in Terraform resource dependencies.
Lifecycle interactions and depends_on
lifecycle vs depends_on
| Mechanism | Controls |
|---|---|
depends_on |
Order — target resource waits for dependencies to finish create or destroy |
lifecycle |
Rules — how Terraform handles replace, destroy protection, ignored drift, and forced replacement |
depends_on does not force replacement when a dependency changes. replace_triggered_by replaces the current resource when Terraform plans a qualifying update or replacement on the reference. You can use both on related resources when you need ordered graph edges and explicit replace propagation.
Rule interactions
Documented interactions worth remembering in practice:
create_before_destroyandprevent_destroy— they can appear in the samelifecycleblock, butprevent_destroy = truestill blocks a replacement because replacement ultimately destroys the existing instance.create_before_destroychanges replacement ordering; it does not bypassprevent_destroy.ignore_changesand replacement — ignoring an attribute does not prevent replacement when another argument still forces it, or whenreplace_triggered_byfires.replace_triggered_byreferences — must be static resource references in the same module; you cannot compute them from unknown values at plan time.
If two lifecycle settings appear to conflict, run terraform plan and read the proposed actions — Terraform's plan output is the source of truth for how rules combined on that resource.
Common lifecycle mistakes
| Mistake | Why it hurts | Better approach |
|---|---|---|
Assuming prevent_destroy survives config removal |
Removing the resource block still destroys the instance on apply | Keep the block; use -target only with care; rely on process and backups for true retention |
ignore_changes on too many attributes |
Hides real drift; config no longer describes production | Ignore only externally owned fields; review regularly |
create_before_destroy on uniquely named resources |
Provider may not allow two live objects with the same name | Confirm provider constraints; use name prefixes or alternate patterns |
Using replace_triggered_by for ordering only |
Causes unnecessary replacement churn when only graph order is needed | Use depends_on for ordering; reserve replace_triggered_by when a change on one resource must force replacement of another |
| Setting lifecycle values from unknown attributes | Plan cannot evaluate lifecycle rules that depend on values known only after apply | Reference resources with known plan-time addresses only |
Cleanup
Destroy resources in each subdirectory you created:
cd ~/terraform-labs/terraform-lifecycle/create-before-destroy && terraform destroy -auto-approve -input=false 2>/dev/null || true
cd ~/terraform-labs/terraform-lifecycle/prevent-destroy && terraform destroy -auto-approve -input=false 2>/dev/null || true
cd ~/terraform-labs/terraform-lifecycle/ignore-changes && terraform destroy -auto-approve -input=false 2>/dev/null || true
cd ~/terraform-labs/terraform-lifecycle/ignore-changes-all && terraform destroy -auto-approve -input=false 2>/dev/null || true
cd ~/terraform-labs/terraform-lifecycle/replace-triggered-by && terraform destroy -auto-approve -input=false 2>/dev/null || trueReferences
- The
lifecycleMeta-argument create_before_destroyprevent_destroyignore_changesreplace_triggered_by
Summary
Terraform lifecycle blocks adjust how Terraform manages individual resource instances — not what the provider creates, but the rules around replacement, destruction, drift, and forced replacement triggers. You compared default destroy-then-create replacement (-/+) with create_before_destroy (+/-) on local_file, saw prevent_destroy block terraform destroy while configuration removal still allows deletion on apply, and used ignore_changes to stop Terraform from reverting legitimate out-of-band updates.
replace_triggered_by makes changes to one managed resource trigger replacement of another — distinct from depends_on, which only orders operations. Use lifecycle rules sparingly and deliberately: they are powerful guards and escape hatches, but overusing ignore_changes or misunderstanding prevent_destroy limits creates silent drift or false confidence.
For dependency ordering without lifecycle replacement rules, review Terraform resource dependencies. For repeating resources themselves, see Terraform count vs for_each.

