HashiCorp Terraform Associate 004 — Intensive Crash Course
3-Part Exam Crash Course
Everything the 004 exam specifically tests — what changed from 003, command gotchas, five high-difficulty scenario questions, and the exam-day quick-reference table. Designed for exam takers who are short on time and need only the exam-relevant content.
Part 1 — The 004 Specifics (What's New/Emphasised)
1.1 create_before_destroy — Blue/Green Deployments
resource "aws_instance" "web" {
lifecycle {
create_before_destroy = true
}
}
- Creates the new resource first
- Updates dependencies to point to the new resource
- Destroys the old resource last
When using create_before_destroy on a launch template with an auto-scaling group, add it to both resources — not just the launch template.
1.2 prevent_destroy — Guardrail Against Accidents
resource "aws_db_instance" "prod" {
lifecycle {
prevent_destroy = true
}
}
prevent_destroy = true stops the entire destroy operation with an error. No partial destroys — the whole apply fails. Use case: databases, persistent volumes, DNS records.
To actually destroy a resource with prevent_destroy, you must set it to false and re-apply first. You cannot override it at runtime.
prevent_destroy and ignore_changes are different: ignore_changes = don't revert manual drift on those attributes. prevent_destroy = cannot be deleted at all.
1.3 ignore_changes and replace_triggered_by
| Rule | What It Does |
|---|---|
ignore_changes = [tag, label] | Ignores drift on the listed attributes. Does NOT prevent the resource from being destroyed via other means. |
replace_triggered_by = [resource.x] | Forces replacement of this resource whenever the referenced resource or attribute changes. |
| Type | Where Defined | When Evaluated | Can use self? | Blocks apply? |
|---|---|---|---|---|
validation | Inside variable block | Plan — input time | No | Yes |
precondition | Inside lifecycle block | Plan — before resource creation | No | Yes |
postcondition | Inside lifecycle block | After apply — resource exists | Yes (self.xxx) | Yes |
check | Top-level HCL block | After apply | Yes | No — warning only |
check blocks are non-blocking — they warn and continue. postconditions are gates — they fail the apply. This distinction appears on every practice exam.
validation= checks variable INPUT (before plan even starts)precondition= checks BEFORE resource is created (during plan)postcondition= checks AFTER resource is created (blocks apply if failed)checkblock = independent assertion, NEVER blocks apply
2.1 Code Examples
Variable Validation
variable "environment" {
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
Precondition
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = var.instance_type
lifecycle {
precondition {
condition = can(regex("^ami-", var.ami_id))
error_message = "AMI ID must start with 'ami-'"
}
}
}
Postcondition (uses self)
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = var.instance_type
lifecycle {
postcondition {
condition = self.public_ip != ""
error_message = "Instance must have a public IP."
}
}
}
Check Block (non-blocking)
check "database_encryption" {
assert {
condition = aws_db_instance.prod.storage_encrypted == true
error_message = "Production database MUST be encrypted."
}
}
HCP Terraform Organisation
│
├── Project: "Platform Team"
│ ├── Workspace: networking-prod
│ ├── Workspace: networking-staging
│ └── Workspace: networking-dev
│
├── Project: "App Team A"
│ ├── Workspace: app-prod
│ └── Workspace: app-dev
│
└── Project: "App Team B"
├── Workspace: services-prod
└── Workspace: services-dev
| Aspect | Workspaces | Projects |
|---|---|---|
| Purpose | Maps to an individual Terraform config (state, vars, runs) | Groups related workspaces — org/team structure |
| Level | Individual environment (dev, staging, prod) | Team or application level |
| Permission control | Org-wide only (older model) | Per-project RBAC (finer control) |
| New in 004? | No (existed in 003) | Yes — new emphasis |
"Your org has 50 teams. How do you organise workspaces for proper access control?" → Create a Project per team, with that team's workspaces inside.
3.1 Ephemeral Values and Write-Only Arguments
terraform.tfstate.
secret_string on aws_secretsmanager_secret_version — Terraform writes the value but does not store it in state. You cannot reference a write-only attribute in an output — Terraform errors immediately.
The exam tests this: sensitive data should be kept out of state. Options: ephemeral values, write-only arguments, or external secrets managers. NOT sensitive = true which only hides from output — the value is still in the state file in plaintext.
Part 2 — Command Gotchas (Trap Answers)
4.1 state mv vs moved block
| Approach | Method | Traceable? | Preferred? |
|---|---|---|---|
| Old (003 era) | terraform state mv old.name new.name | No — CLI command only | No |
| New (004 emphasis) | moved {} block in code | Yes — in version control | Yes |
Exam question: "Safest way to refactor (rename) a resource?" → Answer: moved {} block in code, not terraform state mv.
moved {
from = aws_instance.web
to = aws_instance.server
}
4.2 -replace vs -target vs -refresh-only
| Flag | What It Does | Use When |
|---|---|---|
terraform apply -replace="res" | Forces destroy + recreate of that specific resource | Resource is corrupted or needs a forced replacement. Replaces deprecated terraform taint. |
terraform apply -target="res" | Applies ONLY that resource, skips others | Emergency only — can break dependencies. NOT for routine use. |
terraform apply -refresh-only | Updates state to match real infrastructure. Does NOT change infrastructure. | Accept drift into state without modifying infrastructure. |
terraform refresh is deprecated. The modern equivalent is terraform apply -refresh-only.
The exam asks: "Force recreate a resource" → Answer: -replace. Not -target (selective apply), not taint (deprecated).
4.3 CI/CD Best Practice — Plan File
# BEST PRACTICE for CI/CD terraform plan -out=plan.tfplan terraform apply plan.tfplan # No -auto-approve needed; plan file prevents accidental changes
Using a plan file means terraform apply plan.tfplan applies exactly what was reviewed — no new plan, no prompt. This is the exam-preferred answer for production CI/CD, NOT terraform apply -auto-approve.
Exam: "Best practice for production CI/CD?" → Plan file (terraform plan -out + terraform apply plan.tffile). NOT terraform apply -auto-approve which skips any review.
4.4 init Flags
| Flag | What It Does |
|---|---|
terraform init -upgrade | Upgrades provider versions to the latest compatible version. Updates .terraform.lock.hcl. |
terraform init -reconfigure | Re-initialises without migrating state. Use when changing backend config. Does NOT move existing state. |
terraform init -migrate-state | Changes backend AND migrates existing state to the new backend. |
Use -migrate-state when you want to move state to a new backend. Use -reconfigure when you want to reconfigure the backend without migrating existing state (dangerous if you have state!).
4.5 terraform import — Hidden Gotcha
terraform import adds an existing real-world resource to Terraform state. But:
- You MUST have the resource block defined in code first — import only writes to state, not code.
- Import does NOT generate code — you must write the resource configuration manually after importing.
- After import, if the code doesn't match the real resource,
terraform planwill show a diff.
4.6 Quick Command Reference
| Command | Purpose | Safe for Prod? |
|---|---|---|
| terraform plan | Preview changes | Yes — read-only |
| terraform apply | Apply changes | Only with plan file |
| terraform destroy | Delete all managed resources | Never without prevent_destroy on critical resources |
| terraform state list | List tracked resources | Yes |
| terraform state show | Show resource details from state | Yes |
| terraform state rm | Remove resource from state (DANGEROUS) | No — use code removal instead |
| terraform import | Add existing resource to state | Requires matching code first |
| terraform validate | Check syntax against provider schemas | Yes |
| terraform fmt | Auto-format .tf files | Yes |
| terraform console | Interactive expression evaluator / REPL | Yes — read-only |
| terraform graph | Output dependency graph in DOT format | Yes |
| terraform apply -refresh-only | Sync state with reality without changing infrastructure | Yes (replaces deprecated terraform refresh) |
Part 3 — High-Difficulty Scenario Questions
A developer accidentally runs: terraform state rm aws_db_instance.prod The database is deleted from the state file but still exists in AWS. What are the THREE most dangerous consequences? A) Terraform will automatically recreate the database B) Terraform will ignore the database on next apply C) If someone adds a new resource with the same config, Terraform may try to create a duplicate D) The database is now totally unmanaged E) The state file size increases due to the missing reference F) If the database is deleted manually, Terraform will not know about it
Your org uses HCP Terraform. You need: - Platform team: manage networking across all environments - App Team A: only manage their app resources - App Team B: only manage their services All workspaces are currently flat under one organisation. Best way to achieve team isolation without giving everyone admin access? A) Use IAM roles in HCP Terraform for each team member B) Create a Project per team, put their workspaces inside C) Create separate HCP Terraform organisations per team D) Use workspace naming conventions and enforce via policy-as-code E) Use team tokens with different permissions per workspace
You manage an auto-scaling group with a launch template. Tomorrow you will update the AMI to patch a security vulnerability. You CANNOT have any downtime. What's the minimum configuration you must add? A) lifecycle { create_before_destroy = true } on the launch template only B) lifecycle { create_before_destroy = true } on the ASG only C) lifecycle { prevent_destroy = true } on both D) lifecycle { create_before_destroy = true } on BOTH resources E) Add force_delete = true to the ASG F) Nothing — it's automatic with "$Latest"
create_before_destroy = true. Adding it to only one resource (A or B) is insufficient — the ASG still tries to destroy the old launch template before the new ASG instances are healthy. Option C (prevent_destroy) protects against deletion, not against downtime during updates. Option F is wrong — $Latest tracks the latest version but does not control destruction order.You want to move resources from the root module into module.storage. Current state: aws_s3_bucket.storage aws_s3_bucket_versioning.storage What's the SAFEST way to perform this refactor without destroying resources? A) terraform state mv aws_s3_bucket.storage module.storage.aws_s3_bucket.storage B) Add moved {} blocks in the new module and apply C) terraform destroy followed by terraform apply D) Use terraform import to re-import after moving the code E) Backup state, run state mv commands, test carefully F) Both A and E (run state mv commands and verify)
moved {} blocks in code is the 004-preferred safe refactoring approach. It is traceable in version control, repeatable, and safe. terraform state mv (A, E, F) is the old approach — it works but is not tracked in code, easy to run twice accidentally, and harder to audit. Option C destroys real infrastructure. Option D (import) is a workaround that leaves the code disconnected from state.You have variable validation (allows only t3/t4 micro/small/medium), a precondition (AMI must be x86_64), and a postcondition (instance must get a public IP). Someone runs: terraform apply -var="instance_type=t3.micro" -var="ami_id=ami-windows-arm64" The ami-windows-arm64 AMI is ARM64 architecture, not x86_64. What will happen? A) Apply succeeds; postcondition fails because no public IP B) Apply fails at precondition; wrong architecture C) Apply fails at variable validation; wrong architecture D) Apply succeeds for variable validation but fails at postcondition E) Terraform crashes due to architecture mismatch F) Apply succeeds for everything
6.1 The Four Shifts from 003 to 004
| Topic | 003 Era | 004 Now |
|---|---|---|
| Organisation structure | Flat workspaces | Projects > Workspaces (hierarchy) |
| Resource refactoring | terraform state mv CLI | moved {} blocks in code (traceable) |
| Validation | Variables only | Preconditions + Postconditions + Check blocks |
| Security / secrets | Store in state (sensitive = true) | Ephemeral values + write-only arguments (NOT in state) |
6.2 Scenario Pattern Quick Answers
Create a Project per team in HCP Terraform. Put each team's workspaces inside their project.
Add a moved {} block in code. Apply. Do NOT use terraform state mv.
lifecycle { prevent_destroy = true }. Stops the entire apply if destroy is attempted.
lifecycle { create_before_destroy = true } on BOTH the launch template AND the ASG.
Use postcondition block (blocks apply if failed) or check block (warning only).
Use validation {} in the variable block (runs before plan). Use precondition inside lifecycle to check at plan time against data sources.
terraform apply -replace="resource.address". Replaces deprecated terraform taint.
terraform plan -out=plan.tfplan then terraform apply plan.tfplan. NOT -auto-approve.
check block = warning only, never blocks apply. postcondition = hard gate, fails the apply if condition is false.
Only hides value from plan output and terraform output. State file STILL stores value in plaintext. Use ephemeral values or write-only args to keep secrets out of state entirely.