Terraform · lesson 9 of 17
Beginner
7- 1 Terraform Fundamentals (IaC)
- 2 Terraform Workflow (init → validate → plan → apply)
- 3 Terraform State (tfstate, remote state, locking)
- 4 Terraform Variables and Outputs
- 5 Providers and Resources (the building blocks of Terraform)
- 6 Terraform Locals: Computed Values Without Repetition
- 7 Terraform Modules (reuse, composition, and clean structure)
Intermediate
7- 8 Terraform count and for_each (repeat resources safely)
- 9 The lifecycle Block: prevent_destroy, create_before_destroy, ignore_changes
- 10 Data Sources and Dependencies (read existing infrastructure)
- 11 sensitive = true: What It Hides, and What It Doesn't
- 12 Remote State Safety (locking, drift, and recovery)
- 13 Workspaces and Environments (dev/stage/prod patterns)
- 14 Terraform CI/CD with GitHub Actions (plan/apply)
Advanced
3Also part of Terraform
4The 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
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.