Terraform Lifecycle Meta-Arguments with Examples

Tested on Ubuntu 26.04 LTS (Resolute Raccoon)
Package terraform 1.15.8-1
hashicorp/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:

hcl
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.

NOTE
Use the Terraform lab environment on Ubuntu. Run terraform init in each subdirectory before your first plan. Examples use the 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:

text
depends_on  → dependency and ordering in the graph
lifecycle     → rules for replacement order, destroy protection, ignored drift, forced replacement

Lifecycle 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:

hcl
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:

bash
mkdir -p ~/terraform-labs/terraform-lifecycle/create-before-destroy
cd ~/terraform-labs/terraform-lifecycle/create-before-destroy

Write a local_file at demo-v1.txt:

bash
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"
}
EOF

Initialize and apply the first file:

bash
terraform init -input=false
terraform apply -auto-approve -input=false -no-color

Sample output:

output
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Change filename to demo-v2.txtfilename is a force-new attribute for local_file, so Terraform must replace the resource:

bash
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"
}
EOF

Plan with the default lifecycle — destroy first:

bash
terraform plan -input=false -no-color

Sample output:

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:

bash
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
  }
}
EOF

Plan again — the replacement order flips:

bash
terraform plan -input=false -no-color

Sample output:

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:

bash
terraform apply -auto-approve -input=false -no-color

Sample output:

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:

bash
mkdir -p ~/terraform-labs/terraform-lifecycle/prevent-destroy
cd ~/terraform-labs/terraform-lifecycle/prevent-destroy

Write a protected file resource:

bash
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
  }
}
EOF

Initialize and apply:

bash
terraform init -input=false
terraform apply -auto-approve -input=false -no-color

Run terraform destroy — Terraform stops with an error:

bash
terraform destroy -auto-approve -input=false -no-color
output
Error: 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:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.5"
    }
  }
}
EOF

Apply the empty module — Terraform destroys the orphaned instance even though prevent_destroy was previously set:

bash
terraform apply -auto-approve -input=false -no-color
output
local_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:

bash
mkdir -p ~/terraform-labs/terraform-lifecycle/ignore-changes
cd ~/terraform-labs/terraform-lifecycle/ignore-changes

Start with a terraform_data resource:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

resource "terraform_data" "demo" {
  input = "configured-value"
}
EOF

Initialize and apply:

bash
terraform init -input=false
terraform apply -auto-approve -input=false -no-color

First change the managed value and apply it so Terraform records changed-outside-terraform for the resource:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

resource "terraform_data" "demo" {
  input = "changed-outside-terraform"
}
EOF

Apply that value to state:

bash
terraform apply -auto-approve -input=false -no-color

Restore a different configured value in main.tf:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

resource "terraform_data" "demo" {
  input = "configured-value"
}
EOF

Without ignore_changes, Terraform detects that the configured input differs from the currently managed value and plans an update:

bash
terraform plan -input=false -no-color

Sample output:

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:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

resource "terraform_data" "demo" {
  input = "configured-value"

  lifecycle {
    ignore_changes = [input]
  }
}
EOF

Plan again:

bash
terraform plan -input=false -no-color
output
No 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:

hcl
lifecycle {
  ignore_changes = all
}

Use a separate directory to compare the same configured-vs-managed mismatch with ignore_changes = all:

bash
mkdir -p ~/terraform-labs/terraform-lifecycle/ignore-changes-all
cd ~/terraform-labs/terraform-lifecycle/ignore-changes-all

Apply the baseline value:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

resource "terraform_data" "demo" {
  input = "configured-value"
}
EOF

Initialize and apply the baseline:

bash
terraform init -input=false
terraform apply -auto-approve -input=false -no-color

Change the managed value and apply it so state records changed-outside-terraform:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

resource "terraform_data" "demo" {
  input = "changed-outside-terraform"
}
EOF

Apply that value:

bash
terraform apply -auto-approve -input=false -no-color

Restore configuration with ignore_changes = all:

bash
cat > main.tf <<'EOF'
terraform {
  required_version = ">= 1.12.0"
}

resource "terraform_data" "demo" {
  input = "configured-value"

  lifecycle {
    ignore_changes = all
  }
}
EOF

Plan with every attribute ignored:

bash
terraform plan -input=false -no-color
output
No 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:

bash
mkdir -p ~/terraform-labs/terraform-lifecycle/replace-triggered-by
cd ~/terraform-labs/terraform-lifecycle/replace-triggered-by

Write a trigger and a follower:

bash
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]
  }
}
EOF

Initialize and apply:

bash
terraform init -input=false
terraform apply -auto-approve -input=false -no-color

Change seed so the trigger resource updates:

bash
terraform apply -var='seed=beta' -auto-approve -input=false -no-color

Sample output:

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_destroy and prevent_destroy — they can appear in the same lifecycle block, but prevent_destroy = true still blocks a replacement because replacement ultimately destroys the existing instance. create_before_destroy changes replacement ordering; it does not bypass prevent_destroy.
  • ignore_changes and replacement — ignoring an attribute does not prevent replacement when another argument still forces it, or when replace_triggered_by fires.
  • replace_triggered_by references — 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:

bash
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 || true

References


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.


Frequently Asked Questions

1. What is a Terraform lifecycle block?

A lifecycle block is a nested block inside a resource that sets meta-arguments controlling how Terraform creates, updates, replaces, and destroys that resource instance. It does not set provider arguments; it changes Terraform's lifecycle behavior for the block it sits in.

2. What does create_before_destroy do in Terraform?

When Terraform must replace a resource, create_before_destroy true creates the new object before destroying the old one. The default is false, which destroys the existing object first then creates the replacement.

3. Does prevent_destroy stop Terraform from deleting a resource?

prevent_destroy blocks plans and applies that would destroy the resource instance, including terraform destroy. It does not keep the resource in state if you remove the resource block from configuration and apply; that still plans destruction unless you use other workflow tools outside this lesson's scope.

4. When should I use ignore_changes in Terraform?

Use ignore_changes when an attribute is legitimately managed outside Terraform and you do not want Terraform to revert drift on that attribute during plan and apply. Overusing it hides meaningful drift and can leave configuration and real infrastructure out of sync.

5. What is the difference between replace_triggered_by and depends_on?

depends_on only establishes dependency ordering — one resource waits for another before create or destroy. replace_triggered_by replaces the current resource when Terraform plans a qualifying update or replacement to the referenced managed resource, instance, or attribute.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)