State & drift decision tree
Need to reconcile infrastructure?
│
├─ Infrastructure changed manually (outside Terraform)?
│ └─ YES → That is DRIFT
│ ├─ Preview drift only → terraform plan -refresh-only
│ └─ Accept drift → terraform apply -refresh-only
│ (updates state to match reality, no config changes)
│
├─ Remove resource from config, but resource was manually destroyed?
│ └─ Remove .tf definition + terraform apply -refresh-only
│
├─ Resource removed from .tf config, existing resource still alive?
│ └─ terraform apply → Terraform DESTROYS the real resource
│
├─ Force recreate a resource?
│ ├─ Old (deprecated) → terraform taint <resource>
│ └─ Modern (v0.15.2+) → terraform apply -replace=<resource>
│
└─ State file stale/locked?
├─ Automatic unlock failed → terraform force-unlock <lock-id>
└─ legacy refresh (avoid) → terraform refresh
Backend decision tree
Need to store/share state?
│
├─ Local only (default)?
│ └─ terraform.tfstate in working dir
│
├─ Remote shared state (team)?
│ ├─ S3 → needs DynamoDB for locking
│ ├─ Consul → has built-in locking
│ └─ HCP Terraform → built-in locking + remote execution
│
├─ Prevent concurrent state changes?
│ └─ Configure STATE LOCKING on backend (S3+DynamoDB, Consul, HCP)
│
├─ Multiple environments from ONE backend config?
│ ├─ workspaces.name → maps to ONE workspace
│ └─ workspaces.prefix → maps to MULTIPLE workspaces
│
├─ Migrate backend (local → S3)?
│ └─ Update backend.tf + terraform init (will prompt to migrate)
│ Modern: terraform init --migrate-state
│
└─ Sensitive data in state?
└─ Use encrypted backend (NOT local plaintext)
Provider & module decision tree
Working with providers?
│
├─ First time / new provider added?
│ └─ terraform init (downloads to .terraform/providers/)
│
├─ Upgrade to latest provider versions?
│ └─ terraform init -upgrade
│ (respects version constraints, updates .terraform.lock.hcl)
│
├─ Lock file (.terraform.lock.hcl) exists?
│ └─ Terraform uses version FROM lock file (not latest)
│
├─ Provider from internet vs offline?
│ ├─ Default → registry.terraform.io
│ └─ Air-gapped → filesystem/network mirror
│
├─ Multiple configs for same provider?
│ └─ Use provider ALIAS
│
├─ What's stored in .terraform/ dir?
│ └─ Providers AND Modules (not lock file, not state)
│
└─ Plugin-based architecture?
├─ Providers are plugins (separate binaries)
├─ Can write custom provider for any API
├─ Provider types: Official / Partner / Community
└─ Provider = first part of resource type (aws_vpc → aws)
Workspace decision tree
Need multiple environments?
│
├─ CLI Workspaces?
│ ├─ terraform workspace new <name>
│ ├─ terraform workspace list
│ ├─ terraform workspace select <name>
│ └─ Each workspace has its own state file
│
├─ HCP Terraform Workspaces vs CLI Workspaces?
│ ├─ CLI workspace → local directory state isolation
│ └─ HCP workspace → full remote env with variables, policies, VCS
│
├─ Feature ONLY in HCP (not CLI)?
│ └─ Secure variable storage
│
└─ One cloud block = one org. Can define MULTIPLE workspaces
└─ One cloud block per configuration (cannot configure multiple)
Lifecycle decision tree
Need to control resource lifecycle?
│
├─ Prevent accidental deletion?
│ └─ lifecycle { prevent_destroy = true }
│
├─ Create before destroy (zero-downtime)?
│ └─ lifecycle { create_before_destroy = true }
│
├─ Ignore changes (config drift tolerated)?
│ └─ lifecycle { ignore_changes = [<attr>] }
│
├─ Replace resource on next apply?
│ └─ lifecycle { replace_triggered_by = [...] }
│
└─ Check block assertion fails?
└─ WARNING only (non-blocking) – operation continues
Dependency decision tree
Managing resource dependencies?
│
├─ Terraform detects dependencies automatically?
│ └─ YES – via attribute references (implicit)
│
├─ Explicit dependency needed (no attribute reference)?
│ └─ depends_on = [<resource>]
│
├─ count vs for_each?
│ ├─ count → creates LIST → reference with [index], use splat [*]
│ └─ for_each → creates MAP → reference with ["key"], NO splat [*]
│
└─ Dynamic nested blocks?
└─ Use dynamic blocks (not count arguments)
Commands quick reference
Core Workflow
| Command |
Purpose |
terraform init |
FIRST command always – downloads providers/modules, init backend |
terraform plan |
Preview changes (read-only, does not change state) |
terraform apply |
Execute changes (refreshes state by default) |
terraform destroy |
Destroy all managed resources |
Validate & Format
| Command |
Purpose |
terraform validate |
Syntax + internal consistency check (needs init, no API calls) |
terraform fmt |
Format code (tabs vs spaces – validate does NOT catch this) |
State Operations
| Command |
Purpose |
terraform state list |
List all resources in state |
terraform state show <res> |
Show attributes of a resource |
terraform state rm <res> |
Remove resource from state (keeps real infra) |
terraform state mv |
Move resource in state |
Refresh / Drift
| Command |
Purpose |
terraform plan -refresh-only |
Preview state refresh (no infra changes) |
terraform apply -refresh-only |
Apply state refresh (updates state file) |
terraform refresh |
Legacy – deprecated behavior |
Taint / Replace
| Command |
Purpose |
terraform taint <res> |
Mark for recreate – DEPRECATED |
terraform apply -replace=<res> |
Modern replace (v0.15.2+) |
Import
| Command |
Purpose |
terraform import <address> <id> |
Import existing resource into state |
terraform import requires: resource address + resource ID
You must ALSO write the .tf config manually (import doesn’t generate it)
Destroy Preview
| Command |
Purpose |
terraform plan -destroy |
Preview destroy plan |
terraform destroy |
Shows resources to delete before prompting |
Locking
| Command |
Purpose |
terraform force-unlock <id> |
Force unlock state – use ONLY when auto-unlock fails |
Debugging
| Command |
Purpose |
TF_LOG=DEBUG |
Enable debug logging |
TF_LOG=TRACE |
Most verbose – shows provider load paths |
terraform console |
Interactive expression evaluation |
terraform providers |
Show provider requirements |
terraform show |
Show current state or plan |
terraform output |
Show output values |
terraform output <name> |
Show specific output |
Workspace
| Command |
Purpose |
terraform workspace new <name> |
Create workspace |
terraform workspace list |
List workspaces |
terraform workspace select <name> |
Switch workspace |
HCP Terraform
| Command |
Purpose |
terraform login |
Authenticate with HCP Terraform |
Exam Traps
State & Drift
❌ TRAP: terraform.tfstate ALWAYS matches current infrastructure
→ FALSE – drift occurs when changes happen outside Terraform
State = snapshot of LAST KNOWN state, not real-time
❌ TRAP: terraform plan always == terraform apply execution plan
→ FALSE – teammate manual change between plan & apply = different result
Apply refreshes state again before executing
❌ TRAP: terraform plan -refresh-only UPDATES the state file
→ FALSE – plan is preview only; state updated only with apply -refresh-only
❌ TRAP: Changing resource in Azure Console updates state file
→ FALSE – Console changes do NOT update state file
State updates only during next plan/apply/refresh
❌ TRAP: Remove resource from .tf config = Terraform removes from state only
→ FALSE – Terraform DESTROYS the real resource on next apply
Backend & Workspaces
❌ TRAP: One remote backend config = one remote workspace
→ FALSE – use workspaces.prefix for MULTIPLE workspaces
❌ TRAP: All standard backends support state locking + remote operations
→ FALSE – only some backends support locking; only HCP Terraform is "enhanced"
❌ TRAP: State file is secure by default
→ FALSE – local state = plaintext JSON; use encrypted backend
❌ TRAP: One cloud block maps to exactly one HCP workspace
→ FALSE – cloud block → one org, can configure multiple workspaces
❌ TRAP: You cannot change backend after first terraform apply
→ FALSE – it is OPTIONAL but possible; run terraform init to migrate
Providers & Plugins
❌ TRAP: Provider configuration block required in every config
→ FALSE – block can be omitted; Terraform assumes empty default
Provider is required; provider BLOCK is not always required
❌ TRAP: Providers are always installed from the internet
→ FALSE – air-gapped installs use filesystem/network mirrors
❌ TRAP: Provider versions from state file
→ FALSE – Terraform uses .terraform.lock.hcl (lock file), NOT state
❌ TRAP: terraform init -upgrade uses ANY latest version
→ FALSE – still respects version constraints in configuration
Variables & Expressions
❌ TRAP: Variable declarations require type, default, or description
→ FALSE – all are optional; variable "x" {} is valid (empty block)
❌ TRAP: for_each resources can use splat [*] expression
→ FALSE – splat works with count (list); for_each = map → use for expressions
count → list → splat [*] ✅
for_each → map → splat [*] ❌ (use for expression instead)
Terraform validate
❌ TRAP: terraform validate uses provider APIs
→ FALSE – validate is offline; checks syntax only, no API calls
❌ TRAP: terraform validate catches tab indentation errors
→ FALSE – tabs are allowed; terraform fmt fixes indentation, not validate
✅ validate DOES catch: missing variable block declarations
Outputs
❌ TRAP: terraform apply prints outputs from child modules
→ FALSE – only ROOT module outputs are printed; child module outputs are not
Provisioners
local-exec → runs on machine running Terraform (NOT on resource)
remote-exec → runs on the resource created by Terraform
file → copies files to the resource
Immutable Infrastructure
Immutable infra advantage = Less complex upgrades
(replace entirely instead of modifying in place)
NOT: automatic, in-place, or quicker
Dependency
❌ TRAP: Terraform ONLY manages dependencies via depends_on
→ FALSE – Terraform handles most dependencies implicitly via references
depends_on needed only when no attribute reference exists
Keyword pattern recognition
“If question mentions…” → “Answer is…”
| Keyword in Question |
→ Answer |
| “manually changed infrastructure” |
→ Drift |
| “sensitive values / secrets in state” |
→ Use encrypted backend |
| “up-to-date / latest provider versions” |
→ terraform init -upgrade |
| “multiple remote workspaces from one config” |
→ workspaces.prefix |
| “single remote workspace” |
→ workspaces.name |
| “resource recreated next apply” |
→ taint / -replace |
| “rerun local-exec provisioner” |
→ taint or -replace (taint if old exam) |
| “force recreate modern way” |
→ terraform apply -replace= |
| “sync state without infrastructure change” |
→ terraform apply -refresh-only |
| “preview state sync” |
→ terraform plan -refresh-only |
| “prevent two runs modifying same state” |
→ State locking |
| “state lock stuck / automatic unlock failed” |
→ force-unlock |
| “migrate state to new backend” |
→ terraform init |
| “access denied error debugging” |
→ TF_LOG=DEBUG |
| “trace provider load paths” |
→ TF_LOG=TRACE |
| “experiment with expressions” |
→ terraform console |
| “first command in new directory” |
→ terraform init |
| “import existing resources” |
→ terraform import + write config |
| “no outputs defined, find IP” |
→ terraform state list + state show |
| “check configuration syntax” |
→ terraform validate |
| “format code” |
→ terraform fmt |
| “show resources to be deleted” |
→ terraform plan -destroy or terraform destroy |
| “run on resource (not local)” |
→ remote-exec provisioner |
| “run on Terraform machine” |
→ local-exec provisioner |
| “copy file to resource” |
→ file provisioner |
| “private module source” |
→ Private GitHub repo OR internal VCS |
| “multiple nested blocks from variable” |
→ dynamic block |
| “second instance of count resource” |
→ resource[1] (0-indexed) |
| “provider for aws_vpc” |
→ aws (prefix before first _) |
| “lock file created when” |
→ terraform init |
| “what’s stored in .terraform/” |
→ Providers and modules |
| “health assessment in HCP” |
→ Drift detection + Check block validation |
| “HCP Terraform vs S3/Consul difference” |
→ HCP can EXECUTE runs (enhanced backend) |
| “available only in HCP, not CLI” |
→ Secure variable storage |
| “check block assertion fails” |
→ Warning only (non-blocking) |
| “can only have one cloud block” |
→ TRUE (only one per config) |
| “write Terraform config first time workflow” |
→ write → init → plan → apply |
| “advantage of immutable infra” |
→ Less complex upgrades |
Topic sections
State
State file = snapshot of last known infrastructure
Location: terraform.tfstate (local) or remote backend
terraform.tfstate = current state
terraform.tfstate.backup = previous state
State stores:
- Resource metadata
- Attribute values (including SECRETS in plaintext if local!)
- Resource dependencies
State does NOT store:
- Provider version (that's in lock file)
- Real-time infra status (drift not auto-detected)
Backend
Standard backends: S3, Consul, GCS, Azure Blob
→ store state only
→ some support locking (S3+DynamoDB, Consul)
Enhanced backend: HCP Terraform / Terraform Cloud
→ stores state + EXECUTES operations remotely
→ has built-in locking
→ has Sentinel policy checks
→ secure variable storage
→ VCS integration
Local backend (default):
→ terraform.tfstate in working dir
→ no locking
→ plaintext (insecure for secrets)
Workspace
CLI workspace:
- Separate state per workspace
- Same config, different state
- terraform.workspace = current workspace name
- terraform workspace <new|list|select|delete>
Remote workspace (HCP):
- workspaces.name → SINGLE remote workspace
- workspaces.prefix → MULTIPLE remote workspaces
e.g., prefix = "networking-" → networking-dev, networking-prod
cloud block (modern) vs remote block (legacy):
- cloud block = connects to HCP Terraform
- One cloud block per config
- cloud block → one org, multiple workspaces possible
Providers
Types:
- Official → maintained by HashiCorp
- Partner → maintained by cloud vendors
- Community → maintained by individuals/community
ALL of the above are TRUE (exam trick: "which is NOT true" → None of above)
Provider architecture:
- Plugins (separate binaries from Terraform core)
- Communicate via RPC
- Downloaded to .terraform/providers/
- Can write custom providers for any API
Version pinning:
.terraform.lock.hcl = dependency lock file
Created/updated by: terraform init
Priority: lock file > version constraints
To upgrade: terraform init -upgrade
Provider reference in code:
resource "aws_vpc" "main" {} → provider = aws
Outside required_providers → refer by LOCAL name only
Modules
Module sources (private):
- Private GitHub repo ✅
- Internal VCS platform ✅
- Public Terraform Registry ❌ (not private)
- Public GitHub ❌ (not private)
terraform import:
→ syntax: terraform import <address> <resource_id>
→ requires: resource address + resource ID
→ does NOT generate config (write manually!)
→ use case: bring existing resource under TF management
Module outputs:
→ terraform apply only prints ROOT module outputs
→ child module outputs not displayed automatically
Command
terraform show → show state or plan file content
terraform output → show defined output values
terraform state show → show state for specific resource
terraform validate → syntax check (no API, needs init)
terraform fmt → format HCL code
terraform providers → show provider requirements
terraform console → interactive expression REPL
NOT REAL commands:
terraform push → deprecated
terraform env → deprecated (use workspace)
terraform import-gcp → does not exist
terraform show -destroy → does not exist
TF_VAR_log=TRACE → wrong; use TF_LOG=TRACE
TF_LOG=PATH → wrong; use TF_LOG_PATH for log file path
HCP Terraform
Key features:
- Remote execution (plan/apply on HCP VMs)
- Secure variable storage (ONLY in HCP, not CLI)
- State storage + locking
- Sentinel policy checks
- VCS integration (GitHub, GitLab, etc.)
- Team management
- Health assessments:
✅ Resource drift detection
✅ Check block validation
❌ NOT: Sentinel policy checks during health assessment
❌ NOT: Estimate infrastructure cost
HCP free tier exists (NOT only for paying customers)
Security
⚠️ State file stores secrets in PLAINTEXT by default
⚠️ Sensitive outputs still stored in state
Solutions:
- Encrypted backend (S3+SSE, Azure Blob encryption, etc.)
- HCP Terraform (encrypted state at rest)
- DO NOT: delete state, edit state manually, store in secrets.tfvars
Best practice → Protect state = Encrypt backend
Drift
Drift = difference between real infra and state file
Cause = changes made outside Terraform (manual, other tools)
Detection:
terraform plan → shows drift as changes to apply
terraform plan -refresh-only → shows drift only (no config changes)
Resolution options:
1. Correct the drift: terraform apply (overwrite manual change with config)
2. Accept the drift: terraform apply -refresh-only (update state to match reality)
KEY: State file does NOT auto-update on external changes
Updates happen on next: plan, apply, or refresh
Lifecycle
lifecycle {
create_before_destroy = true # zero-downtime replacement
prevent_destroy = true # block destroy
ignore_changes = [attr] # tolerate external changes
replace_triggered_by = [...] # force replace when dep changes
}
Taint (deprecated in 0.15.2):
terraform taint <resource>
→ replaced by: terraform apply -replace=<resource>
check block:
→ health assertion (non-blocking)
→ failure = warning, NOT error
→ operation continues
Variables
Variable declaration:
variable "name" {} → ALL arguments optional
Required args: NONE
Optional args: type, default, description, validation, sensitive
Variable precedence (highest → lowest):
1. -var or -var-file CLI flags
2. *.auto.tfvars / *.auto.tfvars.json
3. terraform.tfvars
4. Environment variables TF_VAR_*
5. Default value in variable block
Splat expressions:
count → list → aws_instance.web[*].id ✅
for_each → map → aws_instance.web[*].id ❌ (use for expression)
For expressions:
List: [ for o in var.list : o.id ]
Map: { for o in var.list : o.name => o.id }
Outputs
output "name" {
value = resource.type.name.attr
description = "..."
sensitive = true # hides from CLI output, still in state
}
terraform output → show all root outputs
terraform output <name> → show specific output
terraform state show → show raw state values
Child module outputs → NOT printed by terraform apply
Functions
Common exam functions:
length() → count items
toset() → convert list to set
flatten() → flatten nested lists
merge() → merge maps
lookup() → map lookup with default
element() → get item by index
join() → join list to string
split() → split string to list
format() → string formatting
coalesce() → first non-null value
Dependencies
Implicit dependency (automatic):
resource "A" depends on resource "B".attr → TF detects it
Explicit dependency (manual):
resource "A" {
depends_on = [resource.B]
}
Use when: dependency cannot be expressed via attribute reference
Provider dependency:
depends_on in module blocks also works
Provisioner
local-exec:
→ runs command on Terraform machine (where TF is running)
→ use for: Ansible, scripts triggered locally
remote-exec:
→ runs command ON the resource (SSH/WinRM)
→ use for: bootstrapping remote server
file:
→ copies files from local to remote resource
⚠️ Provisioners are last resort – prefer provider resources
⚠️ Terraform recommends AVOIDING provisioners when possible
To re-run provisioner:
Modern: terraform apply -replace=<resource>
Legacy: terraform taint <resource> → terraform apply
Immutable infrastructure
Immutable = replace entire resource instead of modifying
Advantage = Less complex upgrades
✅ Less complex
❌ NOT: Automatic / In-place / Quicker
Terraform encourages immutable via:
create_before_destroy lifecycle
-replace flag
Terraform workflow (canonical)
New infrastructure workflow:
1. Write Terraform configuration (.tf files)
2. terraform init (setup providers, modules, backend)
3. terraform plan (preview – optional but recommended)
4. terraform apply (execute)
5. terraform destroy (teardown when done)
Import existing infrastructure:
1. Write matching .tf configuration
2. terraform import <address> <id>
3. Verify with terraform plan (should show no changes)
Migrate backend:
1. Update/add backend block in configuration
2. terraform init (prompts to copy existing state)
Quick memory
init → initialize (providers, modules, backend, lock file)
plan → preview (read-only, refreshes state)
apply → execute (refreshes state, makes changes)
destroy → remove all
fmt → formatting (tabs→spaces etc.)
validate → syntax check (no API, needs init, not validate tabs)
show → display state or plan
output → display outputs
console → expression REPL
state list → list resources
state show → show resource attributes
state rm → remove from state (not from cloud)
taint → DEPRECATED → use -replace
refresh → DEPRECATED behavior → use -refresh-only
import → bring existing into state (write config first!)
workspace → manage multiple state environments
Reference syntax
# Backend (S3 with locking)
terraform {
backend "s3" {
bucket = "my-bucket"
key = "terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks" # locking
}
}
# Remote backend workspace modes
terraform {
backend "remote" {
organization = "my-org"
workspaces {
name = "prod" # single workspace
# prefix = "app-" # multiple workspaces
}
}
}
# HCP Terraform (cloud block)
terraform {
cloud {
organization = "my-org"
workspaces {
name = "prod"
}
}
}
# Lifecycle
resource "aws_instance" "web" {
lifecycle {
create_before_destroy = true
prevent_destroy = true
ignore_changes = [tags]
}
}
# Dynamic block
resource "aws_security_group" "sg" {
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.port
to_port = ingress.value.port
protocol = ingress.value.protocol
}
}
}
# for_each vs count
resource "aws_instance" "count_ex" {
count = 3
# reference: aws_instance.count_ex[0], [1], [2]
# splat: aws_instance.count_ex[*].id ✅
}
resource "aws_instance" "fe_ex" {
for_each = { "web" = "t2.micro", "api" = "t2.small" }
instance_type = each.value
# reference: aws_instance.fe_ex["web"]
# splat: ❌ use for expression instead
}
# Import (required: address + ID)
# terraform import aws_instance.web i-1234567890abcdef0
# Variable (no required args)
variable "region" {}
variable "full" {
type = string
default = "us-east-1"
description = "AWS region"
}