Fundamental Workshop 2 — Terraform: Modify, Replace, Destroy

Guided practice15 min
Duration
20 min
Module
1/7
Prerequisites
Fundamental Workshop 1 completed, without destroy: the travail/atelier-1 folder contains main.tf, bonjour.txt and terraform.tfstate, and terraform state list answers local_file.bonjour there
You will build
nothing new; you are going to bring what you created to life: break it by hand, see it repaired, change its content, see it replaced, then destroy everything down to an empty state
Deliverable
the output of the terraform plan from step 5 (the one containing must be replaced) and that of terraform state list from step 9 (empty)

How to read this page. Ten steps, one command at a time. For each: the command, the exact output from the course machine (Terraform 1.12.2), and what to look at. Type each command yourself. The "To understand better" blocks are optional. The terraform … commands are identical everywhere; when a command depends on the system, both versions are given, one below the other. If the Workshop 1 folder no longer exists or if state list does not answer local_file.bonjour, redo Fundamental Workshop 1 first (kit: https://github.com/hrhouma2/aiopsatlas-terraform-labo-fr).

Objective

Workshop 1 had you create a resource and read the state. But an infrastructure is not created once and for all: it gets modified, someone breaks it by mistake, and one day it gets taken down. Those are the three gestures of this workshop. You will first delete bonjour.txt by hand, like a colleague who "cleans up" a folder, and see Terraform notice it then recreate it: that is the drift from Lesson 01. You will then change one line of main.tf and discover that, for the local provider, changing the content of a file does not modify it: it replaces it, with a symbol you must know how to recognize before saying yes. Finally you will destroy everything, and check that the record is empty and that the kit counts zero.

The vocabulary in one image

Still the architect. A drift is a worker who moved a wall without saying so: the building no longer matches the record; the architect notices when going to see on site (Refreshing state...). Modifying in place (~) is repainting a wall: the wall stays the same. Replacing (-/+) is demolishing the wall and rebuilding one in the same spot: it has a new number in the record. Destroying (-) is demolishing without rebuilding. The estimate always says which of the three it foresees; you read it before signing.

Symbol in the planSentence in the planWhat Terraform doesIn this workshop
+will be createdcreates a new objectstep 3: the file deleted by hand
~will be updated in-placechanges an attribute, the object remainsnever for the content of a local_file
-/+must be replaceddestroys the object, then creates another onestep 5: content changes
-will be destroyedremoves the objectstep 8: destroy

Where to type

The same terminal as for Workshop 1, in the travail/atelier-1 folder of the lab-terraform kit. And VS Code to modify main.tf. The terraform … commands are identical everywhere; the commands to delete, read or list a file are given in both versions.

Step 1 — Find the starting point again

Windows (PowerShell), from the root of the kit:

powershell
cd travail\atelier-1

Linux, macOS, WSL 2, Git Bash:

bash
cd travail/atelier-1

Then:

text
terraform state list
text
local_file.bonjour

What the command asks: "What do you manage in this folder?"

What to look at: one line, local_file.bonjour. That is the state Workshop 1 left you in. If you read No state file was found! or nothing at all, the folder is not the right one or the resource was destroyed: redo Workshop 1 before continuing.

Step 2 — Break it by hand

Windows (PowerShell):

powershell
Remove-Item .\bonjour.txt
Get-ChildItem

Linux, macOS, WSL 2, Git Bash:

bash
rm bonjour.txt
ls

What the commands ask: "Delete bonjour.txt, then show me the folder."

What to look at: bonjour.txt is no longer there; main.tf and terraform.tfstate still are. You just did what a colleague does in the AWS console when deleting a bucket "that was of no use": reality changed, but neither the code nor the record knows it yet.

Step 3 — See the drift

text
terraform plan
text
local_file.bonjour: Refreshing state... [id=fd9aee5556589b4d797e6d49f8ec894e29e57713]

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

Terraform will perform the following actions:

  # local_file.bonjour will be created
  + resource "local_file" "bonjour" {
      + content              = "Bonjour Terraform"
      + content_base64sha256 = (known after apply)

      + filename             = "./bonjour.txt"
      + id                   = (known after apply)
    }

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

What the command asks: "Compare the code, the record and reality, and tell me what is missing."

What to look at: Refreshing state... [id=fd9aee55…]: Terraform went to look at the file its record mentions, and did not find it. Result: it removes it from its record and the estimate says will be created, Plan: 1 to add. The same plan as in Workshop 1, step 5, even though you did not touch the code. That is what a drift is: reality moved without going through Terraform, and Terraform detected it at plan time, not before.

To understand better
  • Terraform monitors nothing continuously. Between two commands, it does not know what is happening. It is the Refreshing state... of the plan (and of the apply) that rereads reality. A file deleted three days ago is only discovered at the next plan.
  • Why + and not -/+? The object no longer exists at all: there is nothing to destroy, only to create. -/+ (step 5) is for an object that still exists but must be redone.
  • In the cloud, this is the most frequent situation: someone modified a firewall rule in the console, and the next plan offers to put it back as the code says. The plan shows it to you; it is up to you to decide whether the code or the console was right.

Step 4 — Repair

text
terraform apply
text
local_file.bonjour: Refreshing state... [id=fd9aee5556589b4d797e6d49f8ec894e29e57713]



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

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

local_file.bonjour: Creating...
local_file.bonjour: Creation complete after 0s [id=fd9aee5556589b4d797e6d49f8ec894e29e57713]

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

What the command asks: "Recompute the estimate, wait for my yes, then do it."

What to look at: bonjour.txt is back, and its id is the same as before (fd9aee55…): the identifier of a local_file is the fingerprint of its content, and the content did not change. Check with Get-Content .\bonjour.txt or cat bonjour.txt: Bonjour Terraform. A script would not have known that a repair was needed; Terraform knew because it has a record and code to compare.

Step 5 — Change the content, read the replacement estimate

In VS Code, modify the content line of main.tf to get:

hcl
resource "local_file" "bonjour" {
  filename = "${path.module}/bonjour.txt"
  content  = "Bonjour Terraform, deuxième version"
}

Save, then:

text
terraform plan
text
local_file.bonjour: Refreshing state... [id=fd9aee5556589b4d797e6d49f8ec894e29e57713]

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

Terraform will perform the following actions:

  # local_file.bonjour must be replaced
-/+ resource "local_file" "bonjour" {
      ~ content              = "Bonjour Terraform" -> "Bonjour Terraform, deuxième version" # forces replacement
      ~ content_base64sha256 = "SvXkP5Iqo1rJKHgTVJnjanxfsNVGhb+r52mzUqZJYyg=" -> (known after apply)
      ~ content_base64sha512 = "r3+PevYNJRFsHMz4H4U1DK+lg89nhO9SLJZMAqqtzPKiIv3ilrmbx5sYh2JCW9o5EF0NXanYmuxMvI2QN4Z1Ag==" -> (known after apply)
      ~ content_md5          = "ff3a967c227a58691a3d34a931d3eeb5" -> (known after apply)
      ~ content_sha1         = "fd9aee5556589b4d797e6d49f8ec894e29e57713" -> (known after apply)
      ~ content_sha256       = "4af5e43f922aa35ac92878135499e36a7c5fb0d54685bfabe769b352a6496328" -> (known after apply)
      ~ content_sha512       = "af7f8f7af60d25116c1cccf81f85350cafa583cf6784ef522c964c02aaadccf2a222fde296b99bc79b188762425bda39105d0d5da9d89aec4cbc8d9037867502" -> (known after apply)
      ~ id                   = "fd9aee5556589b4d797e6d49f8ec894e29e57713" -> (known after apply)
        # (3 unchanged attributes hidden)
    }

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

What the command asks: the same as in step 3. But this time it is the code that changed, not reality.

What to look at, line by line. This is the most important plan of the module:

LineWhat it says
-/+ destroy and then create replacementThe legend: the symbol used is -/+, "destroy then create a replacement".
# local_file.bonjour must be replacedNot will be updated in-place: the resource must be replaced.
-/+ resource "local_file" "bonjour" {The whole block carries the -/+.
~ content = "Bonjour Terraform" -> "Bonjour Terraform, deuxième version" # forces replacementThe ~ says the value changes, from the old one (->) to the new one. The # forces replacement comment says this attribute is the one forcing the replacement.
~ content_md5 = "ff3a967c…" -> (known after apply)The fingerprints will change; Terraform does not know the new ones yet.
~ id = "fd9aee55…" -> (known after apply)The identifier is going to change. It will be another object.
# (3 unchanged attributes hidden)filename, directory_permission, file_permission do not move; Terraform hides them for readability.
Plan: 1 to add, 0 to change, 1 to destroy.One destruction and one creation, zero modification.

This is your first deliverable answer: keep this output.

The essential difference between ~ and -/+. The ~ in front of content says what changes. The -/+ in front of resource says how Terraform is going to go about it: demolish, then rebuild. For a text file, you will not see the difference. For a database, -/+ means the data disappears with the old object. The provider decides, attribute by attribute; the local provider decided that content forces the replacement. You read the three clues (must be replaced, # forces replacement, 1 to destroy) before typing yes.

To understand better
  • Why does the local provider replace instead of modifying? Because it does not know how to "fix" an existing file: it knows how to write it or erase it. The id of a local_file is, by the way, the SHA-1 fingerprint of its content; changing the content changes the identifier, and an object whose identifier changes is, for Terraform, another object. On the course machine, file_permission also forces the replacement. Other providers do otherwise: changing a tag on an AWS resource is a ~, changing its name is often a -/+.
  • Where is this written? In the documentation of each resource on the Registry, some arguments are marked forces new resource (or ForceNew). Before an apply in production, that is what you are going to read.
  • Can you force a replacement deliberately? Yes: terraform apply -replace=local_file.bonjour replaces the resource even without a code change. Useful when an object is corrupted. You will see it in Project 08.

Step 6 — Apply the replacement

text
terraform apply
text
local_file.bonjour: Refreshing state... [id=fd9aee5556589b4d797e6d49f8ec894e29e57713]



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

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

local_file.bonjour: Destroying... [id=fd9aee5556589b4d797e6d49f8ec894e29e57713]
local_file.bonjour: Destruction complete after 0s
local_file.bonjour: Creating...
local_file.bonjour: Creation complete after 0s [id=bd2b5f8ccbb2cbc2b9bea872211549635a76b092]

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

What the command asks: the same as in step 4.

What to look at: four action lines instead of two, in the order of the -/+ symbol: Destroying... then Destruction complete (the old one, fd9aee55…), then Creating... then Creation complete (the new one, bd2b5f8c…). And the last line: Apply complete! Resources: 1 added, 0 changed, 1 destroyed. Not 1 changed: nothing was modified, one object was destroyed and another created.

Step 7 — Check the file and the new identifier

Windows (PowerShell):

powershell
Get-Content .\bonjour.txt

Linux, macOS, WSL 2, Git Bash:

bash
cat bonjour.txt
text
Bonjour Terraform, deuxième version

Then:

text
terraform state show local_file.bonjour
text
# local_file.bonjour:
resource "local_file" "bonjour" {
    content              = "Bonjour Terraform, deuxième version"
    content_base64sha256 = "hg9L75JxH+5SDk+ZOADZinGONkirydetS0TFjN3uZ3o="
    content_base64sha512 = "EjT8mRVQU2JQ+Em/rnbRKYfe7E2UqIWw8yQMMtaatimH5XCKg4ZomW4PI5yO5jhujdSrU04vX17HE2kaqEVSkQ=="
    content_md5          = "9a1a1355b48e28ad727200e450b851e1"
    content_sha1         = "bd2b5f8ccbb2cbc2b9bea872211549635a76b092"
    content_sha256       = "860f4bef92711fee520e4f993800d98a718e3648abc9d7ad4b44c58cddee677a"
    content_sha512       = "1234fc991550536250f849bfae76d12987deec4d94a885b0f3240c32d69ab62987e5708a838668996e0f239c8ee6386e8dd4ab534e2f5f5ec713691aa8455291"
    directory_permission = "0777"
    file_permission      = "0777"
    filename             = "./bonjour.txt"
    id                   = "bd2b5f8ccbb2cbc2b9bea872211549635a76b092"
}

What the commands ask: "Show me the real file, then what the record says about it."

What to look at: the file contains the second version. In the record, content is the new value, all the fingerprints have changed, and the id is bd2b5f8c…, the one displayed by Creation complete in step 6. Compare with the state show of Workshop 1, step 9: same address local_file.bonjour, but another object. The code, the record and reality agree again: a terraform plan now would say No changes.

Step 8 — Destroy everything

text
terraform destroy
text
local_file.bonjour: Refreshing state... [id=bd2b5f8ccbb2cbc2b9bea872211549635a76b092]

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

Terraform will perform the following actions:

  # local_file.bonjour will be destroyed
  - resource "local_file" "bonjour" {
      - content              = "Bonjour Terraform, deuxième version" -> null
      - content_base64sha256 = "hg9L75JxH+5SDk+ZOADZinGONkirydetS0TFjN3uZ3o=" -> null

      - filename             = "./bonjour.txt" -> null
      - id                   = "bd2b5f8ccbb2cbc2b9bea872211549635a76b092" -> null
    }

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

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

local_file.bonjour: Destroying... [id=bd2b5f8ccbb2cbc2b9bea872211549635a76b092]
local_file.bonjour: Destruction complete after 0s

Destroy complete! Resources: 1 destroyed.

What the command asks: "Destroy everything you manage here; show me the estimate and wait for my yes."

What to look at: the third symbol, - on its own, and each attribute going to -> null ("nothing anymore"). The question is more serious than at apply: Do you really want to destroy all resources? and There is no undo. Type yes. The sentence to expect: Destroy complete! Resources: 1 destroyed. The file has disappeared from the folder.

Step 9 — Read an empty record

text
terraform state list
text

Then, to see what Terraform answers when asked about a resource it no longer manages:

text
terraform state show local_file.bonjour
text
No instance found for the given address!

This command requires that the address references one specific instance.
To view the available instances, use "terraform state list". Please modify
the address to reference a specific instance.

What the commands ask: "List what you manage", then "show me local_file.bonjour".

What to look at: state list answers nothing: not a line, no error, an empty record. That is different from the No state file was found! of Workshop 1 before apply: here the terraform.tfstate file exists, but its resource list is empty. And state show answers No instance found for the given address!: the address is correct, but nothing matches it anymore, and Terraform sends you back to state list. This is your second deliverable answer: the empty output of terraform state list.

Step 10 — List the folder afterwards

Windows (PowerShell):

powershell
Get-ChildItem -Force

Linux, macOS, WSL 2, Git Bash:

bash
ls -la
text
total 17
drwxr-xr-x 1 rehou 197609    0 Sep 15 14:57 .
drwxr-xr-x 1 rehou 197609    0 Sep 15 14:57 ..
drwxr-xr-x 1 rehou 197609    0 Sep 15 14:57 .terraform
-rw-r--r-- 1 rehou 197609 1228 Sep 15 14:57 .terraform.lock.hcl
-rw-r--r-- 1 rehou 197609  130 Sep 15 14:57 main.tf
-rw-r--r-- 1 rehou 197609  181 Sep 15 14:57 terraform.tfstate
-rw-r--r-- 1 rehou 197609 1653 Sep 15 14:57 terraform.tfstate.backup

(ls -la listing from the course machine; Get-ChildItem -Force displays the same five names: .terraform, .terraform.lock.hcl, main.tf, terraform.tfstate, terraform.tfstate.backup.)

What the command asks: "Show me the whole folder, hidden files included."

What to look at: bonjour.txt is no longer there. main.tf is intact: destroying the resources never touches the code. terraform.tfstate is still there but tiny (181 bytes), and a terraform.tfstate.backup has appeared: the copy of the record from before the destroy, which Terraform keeps out of caution. .terraform/ and .terraform.lock.hcl also remain: if you rerun apply, no need to redo init.

Open the state to see it empty, without modifying it (Get-Content .\terraform.tfstate or cat terraform.tfstate):

json
{
  "version": 4,
  "terraform_version": "1.12.2",
  "serial": 6,
  "lineage": "69fb219e-a88c-a97f-5aed-a33e7c34ff69",
  "outputs": {},
  "resources": [],
  "check_results": null
}

"resources": []: nothing. The "serial" counts the writes to the state (six here: two apply in Workshop 1, then repair, replacement and destruction); the "lineage" is the identifier of this state, different on your machine.

Final check

Go back up to the root of the kit and run the counter:

Windows (PowerShell):

powershell
cd ..\..
.\labo.ps1 etat

Linux, macOS, WSL 2, Git Bash:

bash
cd ../..
./labo.sh etat
text
travail atelier-1 : aucune ressource
Ressources encore gérées : 0 (0 attendu à la fin d'une séance).

Expected answer: aucune ressource (no resource) and Ressources encore gérées : 0 (resources still managed: 0). This is the clean end of session that every project of the course will demand, once the resources cost money.

  • You deleted bonjour.txt by hand and saw plan offer will be created without the code having changed: a drift, detected at Refreshing state....
  • apply recreated the file with the same id.
  • You changed content and read the three clues of the replacement: must be replaced, # forces replacement, Plan: 1 to add, 0 to change, 1 to destroy.
  • apply displayed Destroying... then Creating..., and Resources: 1 added, 0 changed, 1 destroyed.; the id changed.
  • destroy asked for yes with There is no undo. and answered Destroy complete! Resources: 1 destroyed.
  • state list returns nothing; state show returns No instance found for the given address!; terraform.tfstate contains "resources": [].
  • etat returns Ressources encore gérées : 0.
  • You kept the plan from step 5 and the empty output from step 9 as deliverables.

When things go wrong

Show the frequent cases
  • At step 1, state list answers No state file was found! → You are not in travail/atelier-1, or Workshop 1 was not done up to the apply. Check the folder (Get-ChildItem -Force or ls -la must show terraform.tfstate), otherwise redo Workshop 1.
  • At step 1, state list answers nothing → The resource has already been destroyed (a destroy at the end of Workshop 1). Rerun terraform apply, yes, then resume at step 2.
  • At step 3, the plan says No changes. → The file was not deleted (typo in Remove-Item or rm, or wrong folder). Check with Get-ChildItem or ls, delete it, rerun plan.
  • At step 5, the plan says No changes.main.tf was not saved after the modification (VS Code: white dot in the tab). Ctrl+S then rerun.
  • At step 5, Error: Unterminated template string → The closing quote of the new content value got lost during editing. Put it back.
  • At step 5, the line marked # forces replacement is not content → You modified another argument at the same time (for example file_permission = "0644"). For a local_file, that one also forces the replacement (checked: ~ file_permission = "0777" -> "0644" # forces replacement). Put the file back as in step 5, only the content line must change.
  • Apply cancelled. → Something other than yes was typed. Rerun, type yes in full.
  • At step 8, destroy says No changes. No objects need to be destroyed. then Destroy complete! Resources: 0 destroyed. → There is already nothing left in the state (Either you have not created any objects yet or the existing objects were already deleted outside of Terraform.). Move on to step 9.
  • After destroy, you think the state was not cleaned because terraform.tfstate still exists → That is normal. Open it: "resources": []. The file remains, empty, with a .backup next to it.
  • etat answers Ressources encore gérées : 1 → The destroy was cancelled or done in another folder. Go back into travail/atelier-1, terraform destroy, yes, rerun etat.