The core model
- Desired state L1
- Your
.tf configuration — the target you declare.
- State / state file L1–2
- Terraform's memory: address → real-object mapping, attribute cache, dependency record, sync point. Can contain plaintext secrets — never commit it.
- Refresh L1–2
- Reading reality to update state before diffing. Only checks resources already in state — never scans the account.
- Drift L2
- Reality no longer matches state (manual change). Plain apply reverts drift — config wins.
- Plan L1
- Refresh → diff desired-vs-state → proposed actions (
+ ~ - -/+). A prediction; computed values are unknown until apply.
- Apply L1, L6
- Execute the plan as a parallel graph walk, recording each success into state. No rollback.
- Address L4
- A resource's identity in state (
module.storage.aws_s3_bucket.logs). Changing the address without a moved block means destroy + create.
State operations
plan/apply -refresh-only L2- Sync state to reality (reviewed), touching no infrastructure. Doesn't change config — drift on config-set attributes still reverts later.
-refresh=false L2- Skip refresh for speed; diff against last-known state. Blind to drift.
- Forget vs destroy L2, L8
- Forget = remove from state, keep the real thing (
state rm / removed block with destroy = false). Destroy = delete the real thing (delete the block, or omit destroy = false).
- Taint L6
- Mark on a half-provisioned resource: exists but untrustworthy → planned as replace. Modern force-replace:
apply -replace=ADDR.
Backends & teams
- Backend L3
- Where state lives and how it locks. Configured in
terraform {}, activated at init; changes need init -migrate-state (move state) or -reconfigure.
use_lockfile L3- S3-native locking via conditional writes (TF ≥1.10). The DynamoDB-table approach is deprecated since 1.11.
- Lock L3
- Per state file; taken at the start of state-writing ops. Fail-fast on contention (
-lock-timeout to wait); stale locks cleared with force-unlock LOCK_ID.
- State isolation L3, L5
- One state = one blast radius, one lock, one pipeline. Partition by environment × component.
terraform_remote_state L5- Data source reading another state's root-level outputs. No lock taken; requires read access to the whole state file.
- CLI workspaces L5
- Multiple state files, one backend, one set of credentials. For near-identical envs only — not an access boundary. (HCP Terraform workspaces are a different concept.)
Modules & structure
- Module L4
- A directory of
.tf files. A function: variables in, outputs out, resources private. The call name namespaces all inner addresses.
- Root module L4
- The directory you run Terraform in. The only place provider configuration belongs; its outputs are what remote states can read.
- Standard module structure L4
main.tf / variables.tf / outputs.tf / README; nested modules under modules/; runnable examples/.
- Provider rule L4
- Modules declare
required_providers; never provider blocks. Violation blocks count/for_each and orphans resources on removal.
- Thin root L5
- An env directory containing only backend + providers + module calls + inputs. Envs differ by inputs and version pins, never by implementation.
- Thin wrapper L4
- Anti-pattern: a module that re-exposes one resource's arguments 1:1. Modules must raise abstraction.
Graph & lifecycle
- DAG / graph walk L6
- References create edges; nodes run when dependencies finish; default concurrency 10 (
-parallelism). File order is irrelevant.
depends_on L6- Explicit edge for dependencies invisible to references (e.g. IAM attachment before boot). Smell if a reference would do.
create_before_destroy L6- Flips replace order for zero-downtime; requires old + new to coexist (unique names break it).
prevent_destroy / ignore_changes L6- Guard against destroy plans / tolerate specific drift. Both live in
lifecycle {}.
-target L6- Apply one address + deps. Emergency scalpel; routine use diverges state from config.
Testing
.tftest.hcl / terraform test L7- Native test framework (GA 1.6). Run blocks execute in order, sharing state; auto-destroy at file end.
command = plan vs apply L7- Default is apply (real infra!). Plan-mode: fast, free, but computed attributes are unknown.
expect_failures L7- Asserts a checkable object (e.g. variable validation) rejects bad input. Pair with plan-mode.
mock_provider / override_resource L7- (≥1.7) Apply against a fake provider: no creds, no infra; computed attrs get placeholders unless pinned. Tests wiring, not AWS reality.
- Testing pyramid L7
- Static (validate/fmt) → unit (plan + mocks, every push) → integration (real apply, sandbox account, scheduled).
Modern workflow
- Config-driven trio L8
moved (1.1) / import (1.5) / removed (1.7) — declarative, plan-previewed, reviewable state surgery.
-generate-config-out L8- Drafts resource blocks from reality for pending import blocks.
- Plan-as-artifact L8
plan -out=tfplan → review → apply tfplan: execute exactly what was approved, or error if the world moved.
-detailed-exitcode L8- 0 clean · 1 error · 2 changes pending. Exit 2 + clean refresh-only = unapplied config; dirty refresh-only = drift.
.terraform.lock.hcl L8- Provider version lock file. Commit it — the one state-adjacent file that belongs in git.
- OpenTofu L8
- Linux Foundation fork from the 2023 BUSL license change. Largely drop-in today; divergence grows.