Skip to main content
Terraform · lesson 9 of 17
Terraform intermediate Lesson 9 of 17

The lifecycle Block: prevent_destroy, create_before_destroy, ignore_changes

Three lifecycle settings with real plans: a change that never applies, a destroy that fails, and create_before_destroy deleting the file it just created.

By SkillByExample · Updated

terraform lifecycle meta-arguments

By default Terraform decides for itself how to handle a change. It updates in place if it can, destroys and recreates if it can’t, and never refuses a destroy. The lifecycle block overrides each of those decisions. Examples use Terraform 1.16.5 and hashicorp/local 2.9.1, with this terraform block in each directory:

terraform {
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.9"
    }
  }
}

ignore_changes: a config change that never applies

Apply this with content = "version=1.0\n", then edit it to version=2.0 and plan:

resource "local_file" "app_config" {
  filename = "${path.module}/app-config.txt"
  content  = "version=2.0\n"

  lifecycle {
    ignore_changes = [content]
  }
}
$ terraform plan -no-color
local_file.app_config: Refreshing state... [id=643e0955e5646b5f6da4531506b14bb571f76f8f]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration
and found no differences, so no changes are needed.
$ cat app-config.txt
version=1.0

The plan says “No changes”, but the config and the file disagree. ignore_changes = [content] tells Terraform to use content only at creation and never compare it again. The usual reason is an attribute that something else legitimately changes after creation, like an autoscaler’s instance count or a tag added by a cost tool.

prevent_destroy: a destroy that fails

resource "local_file" "critical_record" {
  filename = "${path.module}/critical-record.txt"
  content  = "do-not-lose-me\n"

  lifecycle {
    prevent_destroy = true
  }
}
$ terraform destroy -auto-approve -no-color
local_file.critical_record: Refreshing state... [id=0bba52a8010bbc869003678a1633e57f36a5ab27]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  - destroy

Terraform planned the following actions, but then encountered a problem:

  # local_file.critical_record will be destroyed
  - resource "local_file" "critical_record" {
      - content              = <<-EOT
            do-not-lose-me
        EOT -> null
      - content_base64sha256 = "8JRaHMVbkoD5Hj9FguCciMjaUQIHgY8JfgyOmACsU+s=" -> null
      - content_base64sha512 = "/3KQdn17hK7AzYDPoz879CkgoCTdbHC0dZxBd9VjTSxsoN2k2eF2rw9oUImEsqKWn6JqSL6hNPc13X7AhO4hhA==" -> null
      - content_md5          = "bf8e813ad0436b3582944525b620de50" -> null
      - content_sha1         = "0bba52a8010bbc869003678a1633e57f36a5ab27" -> null
      - content_sha256       = "f0945a1cc55b9280f91e3f4582e09c88c8da510207818f097e0c8e9800ac53eb" -> null
      - content_sha512       = "ff7290767d7b84aec0cd80cfa33f3bf42920a024dd6c70b4759c4177d5634d2c6ca0dda4d9e176af0f68508984b2a2969fa26a48bea134f735dd7ec084ee2184" -> null
      - directory_permission = "0777" -> null
      - file_permission      = "0777" -> null
      - filename             = "./critical-record.txt" -> null
      - id                   = "0bba52a8010bbc869003678a1633e57f36a5ab27" -> null
    }

Plan: 0 to add, 0 to change, 1 to destroy.

Error: Instance cannot be destroyed

  on main.tf line 10:
  10: resource "local_file" "critical_record" {

Resource local_file.critical_record 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.

Terraform built the whole destroy plan, then rejected it: “planned the following actions, but then encountered a problem”. -auto-approve made no difference, because the check happens during planning, before any approval prompt. The command exits with status 1 and the file stays where it is.

create_before_destroy: flipping the replacement order

resource "local_file" "service_endpoint" {
  filename = "${path.module}/endpoint.txt"
  content  = "endpoint=v2.api.internal\n" # was v1 when first applied

  lifecycle {
    create_before_destroy = true
  }
}
$ terraform apply -auto-approve -no-color | grep -vE "content_(base64|md5|sha)"
local_file.service_endpoint: Refreshing state... [id=e503485a441de1df2133e9c2b6ac7d91d994d4d2]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+/- create replacement and then destroy

Terraform will perform the following actions:

  # local_file.service_endpoint must be replaced
+/- resource "local_file" "service_endpoint" {
      ~ content              = <<-EOT # forces replacement
          - endpoint=v1.api.internal
          + endpoint=v2.api.internal
        EOT
      ~ id                   = "e503485a441de1df2133e9c2b6ac7d91d994d4d2" -> (known after apply)
        # (3 unchanged attributes hidden)
    }

Plan: 1 to add, 0 to change, 1 to destroy.
local_file.service_endpoint: Creating...
local_file.service_endpoint: Creation complete after 0s [id=7d83d0f7adf7a96ae236eacbf8ee5236f4b7d892]
local_file.service_endpoint (deposed object 5fb17c37): Destroying... [id=e503485a441de1df2133e9c2b6ac7d91d994d4d2]
local_file.service_endpoint: Destruction complete after 0s

Apply complete! Resources: 1 added, 0 changed, 1 destroyed.
$ ls
main.tf
terraform.tfstate
terraform.tfstate.backup
$ terraform plan -no-color | grep -E "^  #|Plan:"
  # local_file.service_endpoint will be created
Plan: 1 to add, 0 to change, 0 to destroy.

The symbol changed from -/+ to +/-, and the apply did create first. While the new object existed, the old one was kept in state as a deposed object, then destroyed. But endpoint.txt is gone. Both objects had the same filename, so the new file overwrote the old one, and destroying the deposed object deleted that same path, which is why the next plan wants to create the file again. Cloud resources hit the same problem when the name has to be unique, and usually fail with “already exists” instead.

The fix: let the replacement coexist

locals {
  version = "v2"
}

resource "local_file" "service_endpoint" {
  filename = "${path.module}/endpoint-${local.version}.txt"
  content  = "endpoint=${local.version}.api.internal\n"

  lifecycle {
    create_before_destroy = true
  }
}

Apply that, then change version to "v3":

$ terraform apply -auto-approve -no-color | grep -E "^local_file|Apply"
local_file.service_endpoint: Refreshing state... [id=7d83d0f7adf7a96ae236eacbf8ee5236f4b7d892]
local_file.service_endpoint: Creating...
local_file.service_endpoint: Creation complete after 0s [id=b6187727edad8ebfd33c7edbaaf5725189668229]
local_file.service_endpoint (deposed object 06df7a8a): Destroying... [id=7d83d0f7adf7a96ae236eacbf8ee5236f4b7d892]
local_file.service_endpoint: Destruction complete after 0s
Apply complete! Resources: 1 added, 0 changed, 1 destroyed.
$ ls *.txt
endpoint-v3.txt

The version is now in the name, so endpoint-v3.txt and endpoint-v2.txt both existed between the create and the destroy, and only the old one was deleted. For cloud resources, use name_prefix where the provider offers it, or add a random suffix.

Practice

1. Remove the lifecycle block from the ignore_changes example and plan again.

Solution
$ terraform plan -no-color | sed -n "/must be replaced/,/^Plan:/p"
  # local_file.app_config must be replaced
-/+ resource "local_file" "app_config" {
      ~ content              = <<-EOT # forces replacement
          - version=1.0
          + version=2.0
        EOT
      ~ content_base64sha256 = "01yMYAHzvcLVZqvjk8Fd6mwCyaUOJbzzvyFg/ZPSnwQ=" -> (known after apply)
      ~ content_base64sha512 = "G7jlZK8HNCCjRSiD7wAcSgumayNKpC6cBBfKnhSrqyrt+Mu5DTLzmjFRnsiy1xev7MmW7Xp/PANyoLmx6U3rNQ==" -> (known after apply)
      ~ content_md5          = "607e2d3a119dfe832d937ce579ef1079" -> (known after apply)
      ~ content_sha1         = "643e0955e5646b5f6da4531506b14bb571f76f8f" -> (known after apply)
      ~ content_sha256       = "d35c8c6001f3bdc2d566abe393c15dea6c02c9a50e25bcf3bf2160fd93d29f04" -> (known after apply)
      ~ content_sha512       = "1bb8e564af073420a3452883ef001c4a0ba66b234aa42e9c0417ca9e14abab2aedf8cbb90d32f39a31519ec8b2d717afecc996ed7a7f3c0372a0b9b1e94deb35" -> (known after apply)
      ~ id                   = "643e0955e5646b5f6da4531506b14bb571f76f8f" -> (known after apply)
        # (3 unchanged attributes hidden)
    }

Plan: 1 to add, 0 to change, 1 to destroy.

The difference was there all along. ignore_changes was hiding it, not making it go away.

2. Set ignore_changes = all and also change the filename to app-config-v2.txt. Is the filename change ignored too?

Solution
$ terraform plan -no-color | grep -E "No changes|Plan:"
No changes. Your infrastructure matches the configuration.

Yes. all ignores every attribute, so even a rename is skipped. Prefer an explicit list so unrelated changes still show up.

3. On the prevent_destroy example, change content (which forces replacement) and apply. Is that blocked too?

Solution
$ terraform apply -auto-approve -no-color 2>&1 | sed -n '/^Error/,$p'
Error: Instance cannot be destroyed

  on main.tf line 10:
  10: resource "local_file" "critical_record" {

Resource local_file.critical_record 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.

Yes. Any plan that includes destroying the resource is rejected, whether it comes from terraform destroy, a deleted block or a forced replacement. Errors go to stderr, hence the 2>&1.

4. Add prevent_destroy = true next to create_before_destroy = true on the versioned endpoint, change version to "v4", and predict whether endpoint-v4.txt gets created.

Solution
  lifecycle {
    create_before_destroy = true
    prevent_destroy       = true
  }
$ terraform apply -auto-approve -no-color 2>&1 | grep -E "^\+/-|Plan:|^Error"
+/- create replacement and then destroy
+/- resource "local_file" "service_endpoint" {
Plan: 1 to add, 0 to change, 1 to destroy.
Error: Instance cannot be destroyed
$ ls *.txt
endpoint-v3.txt

It isn’t. Many people expect the create half to run and only the destroy to be refused. In fact prevent_destroy rejects the whole plan before any step runs, so nothing is created or destroyed.

Next: data sources and dependencies, which read things Terraform doesn’t manage and control the order resources are created in.

Frequently Asked Questions

Is lifecycle a resource type or a meta-argument?
A meta-argument. Like count and for_each, the lifecycle block works inside any resource of any provider, because Terraform core implements it rather than the provider. Settings tested on local_file behave exactly as they would on a cloud resource.
Does ignore_changes mean Terraform stops managing the resource?
No. It stops comparing the listed attributes between config and state; every other attribute is still managed, and the resource can still be replaced or destroyed. ignore_changes = all ignores every attribute, which is rarely what you want because unrelated fixes get silently skipped too.
Why did create_before_destroy delete the file it had just created?
Both copies used the same filename. Terraform wrote the new file over the old path, then destroyed the old object, which deletes that same path. create_before_destroy only helps when the replacement can coexist with the original, so names must differ, usually by adding a version, hash or random suffix.
How do I remove a resource that has prevent_destroy set?
Set prevent_destroy = false (or delete the lifecycle block) and then run the destroy or apply. There is no command-line flag to override it for one run; the protection lives in the configuration so that removing it shows up in code review.
Can I combine prevent_destroy with create_before_destroy?
You can, but a replacement then fails. Replacement still ends with destroying the old object, and prevent_destroy rejects the whole plan before any step runs, so not even the create half happens. Use the pair for resources that should never be replaced at all.