Skip to main content
RunBook Academy

← All labs in Terraform

Lab · intermediate · ~30 min

Lab: Plan Review and Interpretation

C · Simulation

Objectives

  • Read every action symbol in a Terraform plan
  • Identify the change types (create, update, replace, destroy)
  • Recognise known-after-apply values
  • Investigate unexpected plans before applying
  • Apply the production plan-review workflow

Prerequisites

Objective

By the end of this lab, you will have:

  • Written a configuration that produces several different plan outputs.
  • Interpreted each plan correctly.
  • Identified the action types (create, update, replace, destroy).
  • Recognised known-after-apply values.
  • Investigated an unexpected plan.

The lab produces several plans; the reader must interpret each correctly before applying.

Architecture

A single Terraform configuration with three resources that have different change types:

+------------------------+
| Terraform              |
|       ↓                |
| random_pet.name        |
| local_file.greeting    |
| local_file.tags        |
|       ↓                |
| ~/rb-plan-review-lab/  |
|   ├── greeting.txt     |
|   └── tags.txt         |
+------------------------+

The local_file.tags resource is the focus: it has a custom lifecycle that triggers replacement on tag changes.

Requirements

  • A Linux or macOS workstation with shell access.
  • The Terraform CLI 1.9.x or later installed.

Scenario

You maintain a small Terraform configuration. The configuration manages two files. You are about to make a change. The change is supposed to be a simple content update. The plan tells a different story.

The lab walks through several scenarios. Each scenario asks you to interpret the plan and decide what to do.

Tasks

Task 1: Create the working directory and configuration

mkdir -p ~/rb-plan-review-lab
cd ~/rb-plan-review-lab

Create main.tf:

terraform {
  required_version = ">= 1.9.0"
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.5"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.6"
    }
  }
}

resource "random_pet" "name" {
  length = 1
}

resource "local_file" "greeting" {
  filename = "${path.module}/greeting.txt"
  content  = "Hello, ${random_pet.name.id}!\n"
}

resource "local_file" "tags" {
  filename       = "${path.module}/tags.txt"
  content        = "Initial content.\n"
  file_permission = "0644"

  lifecycle {
    create_before_destroy = true
  }
}

Task 2: Initialise and apply

terraform init
terraform apply

Verify the files were created:

ls -la ~/rb-plan-review-lab
cat ~/rb-plan-review-lab/greeting.txt
cat ~/rb-plan-review-lab/tags.txt

Task 3: Verify the empty plan

terraform plan

Expected output:

No changes. Your infrastructure matches the configuration.

The plan is empty. The configuration matches the state.

Task 4: Scenario A — A simple in-place update

Edit main.tf to change the greeting content:

resource "local_file" "greeting" {
  filename = "${path.module}/greeting.txt"
  content  = "Hello, ${random_pet.name.id}! Welcome.\n"
}

Run the plan:

terraform plan

Question A.1: What is the action?

Answer

The action is update in-place. The content attribute changes; the other attributes are unchanged.

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

Apply:

terraform apply

Verify:

cat ~/rb-plan-review-lab/greeting.txt

Task 5: Scenario B — A no-op due to random_pet

The random_pet resource generates a name. The name is in state. Editing the length does not affect the name.

Edit main.tf to change the random_pet length:

resource "random_pet" "name" {
  length = 2
}

Run the plan:

terraform plan

Question B.1: What is the action?

Answer

The plan proposes to update the random_pet resource in place. The length attribute changes from 1 to 2. The id attribute (the generated name) is unchanged because the already-generated name is preserved.

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

The random_pet provider is sensitive to the length change but does not change the resulting name until a new resource is created. The length change is mostly cosmetic.

Task 6: Scenario C — A replace due to replacement-triggering attributes

The tags.txt resource has a file_permission of 0644. The local_file providers schema determines whether changing file_permission triggers replacement.

Edit main.tf to change the file permission:

resource "local_file" "tags" {
  filename        = "${path.module}/tags.txt"
  content         = "Initial content.\n"
  file_permission = "0600"

  lifecycle {
    create_before_destroy = true
  }
}

Run the plan:

terraform plan

Question C.1: What is the action?

Answer

The action depends on the local_file providers schema. In the current version of the provider, changing file_permission does trigger replacement because the provider does not have an in-place update API for file permissions.

The plan shows:

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

But the change is actually a replace. The replacement is visible in the will be updated in-place or must be replaced annotation. Look for the attribute-level annotation.

In the actual plan output, the change is annotated with # forces replacement. The provider has marked the file_permission change as a replacement.

The lifecycle.create_before_destroy = true ensures the new file is created before the old file is destroyed.

Task 7: Scenario D — A destroy for a removed resource

Edit main.tf to remove the tags resource:

# Delete the local_file.tags block entirely

Run the plan:

terraform plan

Question D.1: What is the action?

Answer

The plan proposes to destroy the tags.txt file.

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

The destroyed resource is the local_file.tags resource. The file is removed from disk during the apply.

The destroy is recoverable in this lab (the file is disposable) but is not recoverable in production. The plan is the opportunity to question the destroy.

Task 8: Scenario E — The unexpected plan

The “production plan test” scenario. The plan shows changes that were not intended.

Edit main.tf to add a new resource:

resource "local_file" "extra" {
  filename = "${path.module}/extra.txt"
  content  = "Extra file.\n"
}

Run the plan:

terraform plan

The plan should show the new resource as 1 to add, the greeting as 0 to change (no changes were made), and the tags as 0 to destroy (wait — the tags block was removed in Task 7!).

Question E.1: What is the actual plan?

Answer

The plan shows:

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

The plan creates the extra resource. The greeting and name resources are unchanged. The tags resource was already removed in Task 7; the state was updated then.

The plan is the expected plan. The configuration matches the state matches the real world.

Task 9: The unexpected plan (advanced)

Now the test: introduce a drift. Modify the greeting.txt file manually:

echo "Manual change." > ~/rb-plan-review-lab/greeting.txt

Run the plan:

terraform plan

Question E.2: What does the plan show?

Answer

The plan refreshes the state and detects the drift. The plan output reports:

# local_file.greeting will be updated in-place
~ resource "local_file" "greeting" {
    ~ content = "Hello, ...! Welcome.\n" -> "Manual change.\n"

The provider refresh updates the state to reflect the manual change. The plan now proposes to reconcile the configuration back to the configuration. The change is from Manual change.\n to the configured value.

This is drift. The drift is detected. The plan is the recovery procedure.

Task 10: Accept the drift

terraform apply

The apply restores the file content to the configuration value.

Verify:

cat ~/rb-plan-review-lab/greeting.txt

Validation

The lab is successful if:

  • The plans in each scenario were interpreted correctly.
  • The plan output matched the expectation.
  • The apply did not produce unexpected changes.

Expected Outcome

At the end of the lab:

+---------------------------------+
| ~/rb-plan-review-lab/             |
|   .terraform/                    |
|   .terraform.lock.hcl            |
|   extra.txt                      |
|   greeting.txt                   |
|   main.tf                        |
|   tags.txt (deleted)             |
+---------------------------------+

The greeting.txt content is the configured value. The tags.txt is deleted. The extra.txt is created. The state is consistent with the configuration.

Cleanup

cd ~/rb-plan-review-lab
terraform destroy
rm -rf .terraform .terraform.lock.hcl terraform.tfstate*

What You Learned

You learned the plan-review workflow:

  1. Read the summary line. The summary is the operational artefact.
  2. For each change, identify the action type. Create, update, replace, destroy. Each has different operational implications.
  3. For each replace, find the trigger. The plan shows # forces replacement for the attribute that triggers the replacement.
  4. Investigate unexpected plans. A plan that differs from your expectation is evidence of a problem.
  5. Drift is detected by the plan. A manually-modified resource appears in the plan as update in-place to restore the configuration.

Deliverables

  • · Plan interpretations for each scenario
  • · A documented decision for each scenario
  • · A running configuration that produces the expected plans

Verification status

Last reviewed
2026-08-12
Executed end to end
not yet run on hardware

The commands and configuration here have been reviewed against the verified software versions, but nobody has run this lab start to finish on a system meeting its prerequisites. Treat the Expected Outcome as the intended result rather than an observed one, and keep the Cleanup section to hand.