Lab — Terraform Plan Review Workflow¶
Lab Overview¶
Purpose: Practise the production Terraform loop used in pull requests: format, validate, save a plan, review it, apply that artefact, then destroy.
Scenario: Your platform team will not merge Infrastructure as Code (IaC) without a reviewed plan file. You must produce one for a small root module and prove apply matches the review.
Expected outcome: A lockfile committed locally, a binary tfplan you can show, successful apply of that plan, and a clean destroy.
This is a lab, not a tutorial
Concepts come from the Terraform track. Here you operate like CI: non-interactive flags and plan artefacts.
Business Scenario¶
A SaaS startup stores environment metadata as versioned files (feature flags, banner text, owners). They use Terraform with the hashicorp/local provider so engineers can rehearse plan review before touching AWS. Policy: no apply -auto-approve without a saved plan that passed review.
Learning Objectives¶
By the end of this lab, you will be able to:
- Author a pinned root module (
required_version,required_providers, variables, outputs) - Run
fmt,init,validate, andplan -outnon-interactively - Read a plan and predict creates/updates before apply
- Apply a saved plan file (not a live re-plan)
- Destroy lab resources and leave no sensitive artefacts in Git
Prerequisites¶
Knowledge¶
- Introduction to Terraform and IaC
- Installing Terraform and the CLI Workflow
- Variables, Locals, and Outputs
- Terraform in CI/CD Pipelines
Software¶
| Tool | Notes |
|---|---|
| Terraform CLI 1.9+ (1.15.x ideal) | terraform version |
| Git | optional but recommended |
| Network once | to download hashicorp/local on first init |
Estimated cost: £0 (local provider only). No cloud account required.
Architecture¶
Environment¶
Any laptop or CI runner with Terraform installed. Work in an empty directory you can delete afterwards.
Initial State¶
Empty workspace. You will create the root module from scratch, then operate it as if a teammate must approve your plan.
Lab Tasks¶
Task 1 — Scaffold the root module¶
Objective: Create a reviewable module layout.
Background: Production roots keep versions, variables, resources, and outputs in separate files for clearer pull request diffs.
Instructions:
mkdir -p ~/rebash-lab-tf-plan && cd ~/rebash-lab-tf-plan
cat > versions.tf <<'EOF'
terraform {
required_version = ">= 1.9.0"
required_providers {
local = {
source = "hashicorp/local"
version = "~> 2.9"
}
}
}
EOF
cat > variables.tf <<'EOF'
variable "project" {
type = string
description = "Short project key used in filenames and tags"
default = "rebash"
}
variable "environment" {
type = string
description = "Environment name"
default = "lab"
validation {
condition = contains(["lab", "dev", "staging"], var.environment)
error_message = "environment must be lab, dev, or staging for this exercise."
}
}
variable "banner" {
type = string
description = "Customer-facing banner text"
default = "Plan review before apply."
}
EOF
cat > main.tf <<'EOF'
locals {
tags = {
project = var.project
environment = var.environment
managed_by = "terraform"
}
artefact_path = "${path.module}/generated/${var.project}-${var.environment}-banner.txt"
}
resource "local_file" "banner" {
filename = local.artefact_path
content = <<-EOT
project = ${var.project}
environment = ${var.environment}
banner = ${var.banner}
tags = ${jsonencode(local.tags)}
EOT
file_permission = "0644"
}
resource "terraform_data" "review_marker" {
input = {
lesson = "lab-terraform-plan-review-workflow"
project = var.project
}
}
EOF
cat > outputs.tf <<'EOF'
output "banner_path" {
description = "Path of the managed banner file"
value = local_file.banner.filename
}
output "content_md5" {
description = "MD5 of banner content"
value = local_file.banner.content_md5
}
output "review_marker" {
description = "Lab marker stored in terraform_data"
value = terraform_data.review_marker.output
}
EOF
cat > terraform.tfvars <<'EOF'
project = "rebash"
environment = "lab"
banner = "Welcome to REBASH Academy labs."
EOF
cat > .gitignore <<'EOF'
.terraform/
*.tfstate
*.tfstate.*
crash.log
tfplan
*.tfplan
EOF
Expected output: Files listed by ls include versions.tf, variables.tf, main.tf, outputs.tf, terraform.tfvars, .gitignore.
Validation:
Task 2 — Format, init, and validate (CI style)¶
Objective: Run the same checks a pipeline runs before planning.
Instructions:
export TF_IN_AUTOMATION=1
terraform fmt
terraform init -input=false
terraform validate
terraform fmt -check
Expected output: Success! The configuration is valid. Lockfile .terraform.lock.hcl appears.
Validation:
Task 3 — Create a saved plan artefact¶
Objective: Produce a binary plan for review — not an immediate apply.
Instructions:
Expected output: Plan shows create for local_file.banner and terraform_data.review_marker. File tfplan exists.
Validation:
Treat plan files as sensitive
Even local plans can embed variable values. Do not commit tfplan (already gitignored).
Task 4 — Simulate pull request review¶
Objective: Practise reading the plan like a reviewer.
Background: Reviewers ask: unexpected destroys? wrong filenames? secrets in content?
Instructions: Answer these in your notes while reading terraform show tfplan:
- How many resources will be created?
- What filesystem path will Terraform write?
- What happens if you change
bannerafter apply — update or replace?
Then introduce a deliberate surprise and re-plan:
# Reviewer asks for a clearer filename — change local only after discussing
sed -i.bak 's/\${var.project}-\${var.environment}-banner.txt/\${var.project}-\${var.environment}-banner.v1.txt/' main.tf
# macOS/BSD sed used -i.bak; on GNU sed you may use sed -i
terraform fmt
terraform plan -input=false -out=tfplan
terraform show -no-color tfplan | head -60
Expected output: Plan still creates (fresh lab) or shows path change intent clearly.
Validation: You can explain whether the filename change is in-place update or replace for local_file.
Hint: local_file replaces when filename changes because the path is ForceNew-style behaviour for that argument.
Task 5 — Apply the reviewed plan only¶
Objective: Apply the artefact, not a new live plan.
Instructions:
terraform apply -input=false tfplan
cat generated/rebash-lab-banner.v1.txt 2>/dev/null || cat generated/rebash-lab-banner.txt
terraform output
If your sed path differs, ls generated/ and cat the file that exists.
Expected output: Apply completes; outputs print banner_path and checksums.
Validation:
test -f "$(terraform output -raw banner_path)"
terraform output -raw content_md5 | grep -E '^[a-f0-9]{32}$'
Task 6 — Drift awareness and destroy¶
Objective: See drift, then tear down cleanly.
Instructions:
# Simulate out-of-band edit
BANNER_FILE="$(terraform output -raw banner_path)"
echo "DRIFT" >> "$BANNER_FILE"
terraform plan -input=false
terraform destroy -input=false -auto-approve
rm -f tfplan
Expected output: Plan after drift proposes an update to restore content; destroy removes managed objects.
Validation:
Validation¶
| Check | Pass criteria |
|---|---|
| Pinning | required_version and local ~> 2.9 present |
| Lockfile | .terraform.lock.hcl exists after init |
| Plan artefact | tfplan created before apply |
| Apply | Outputs resolve; banner file existed post-apply |
| Hygiene | .gitignore excludes state and plan files |
| Cleanup | Destroy removed managed files |
Troubleshooting¶
| Symptoms | Possible causes | Resolution | Verification |
|---|---|---|---|
| Provider download fails | Network/registry blocked | Check access to registry.terraform.io | terraform init |
validate fails before init | Providers missing | Run init first | terraform validate |
| Apply differs from review | Re-planned instead of using -out | Always apply tfplan | Compare SHA of plan file in CI |
sed -i confusion | GNU vs BSD sed | Edit main.tf in your editor instead | Re-run plan |
Permission denied under generated/ | Directory not writable | Ensure workspace is yours | ls -ld . |
Challenge Extensions¶
- Split
labvsdevusingterraform.tfvarsfiles and-var-file - Add a
terraform testfile that asserts output paths - Encode the same workflow in GitHub Actions with
planartefact upload (see Terraform CI tutorial) - Move state to a remote backend once you leave local labs
Cleanup¶
cd ~/rebash-lab-tf-plan
terraform destroy -input=false -auto-approve 2>/dev/null || true
cd ~
rm -rf ~/rebash-lab-tf-plan
No cloud spend — still delete state and plan files so secrets cannot leak from disk backups.
Production Discussion¶
Production teams:
- Store plans as CI artefacts with retention and access control
- Separate plan and apply IAM roles (OIDC)
- Require human approval on destroy and on unexpected replaces
- Commit
.terraform.lock.hcl; never commit*.tfstate* - Prefer remote state with locking and encryption before a second engineer joins
Best Practices¶
TF_IN_AUTOMATION=1and-input=falsein every pipeline job- Reviewers read
terraform show(or JSON plan) before approve - Keep modules small; this lab’s layout scales to cloud providers unchanged
- Validation blocks catch bad environments early
- Destroy labs completely — leftover state confuses the next exercise
Common Mistakes¶
| Mistake | Why it happens | Correct approach |
|---|---|---|
apply -auto-approve without -out | Convenience | Save and apply the reviewed plan |
Committing .terraform/ | Large binaries | Commit only the lockfile |
Putting secrets in banner / tfvars | Demo haste | Use placeholders; mark sensitive later |
| Ignoring drift | “It still works” | Investigate every unexpected plan line |
Success Criteria¶
You can demonstrate:
- A pinned root module with clear outputs
- A saved plan you can explain line-by-line
- Apply of that plan and proof via outputs
- Full destroy and gitignore hygiene
Reflection Questions¶
- Why is applying a saved plan safer than re-running plan in the apply job?
- What belongs in Git for this root module on day one?
- How would remote state change your cleanup and locking story?
- How would you stop a plan that includes an unexpected destroy from reaching apply?
Interview Connection¶
Expect questions on plan vs apply, lockfiles, state sensitivity, and CI gates. Use this lab as a concrete walkthrough. Continue with interview prompts in the Terraform Interview Prep guide and Terraform in CI/CD Pipelines.
Related Tutorials¶
- Track: Terraform
- Installing Terraform and the CLI Workflow
- Terraform in CI/CD Pipelines
- Cheat sheet: Terraform Cheat Sheet
- Learning path: DevOps Engineer
- Previous lab: Linux Production Incident Triage