terraform, one command per page
Thirty commands for the person who runs plan every week and was never shown the machinery underneath: the core loop of write, init, plan and apply, the state file that binds configuration to real objects, locking and force-unlock, rm that forgets without destroying, import that adopts what already exists, -replace instead of taint, required_providers and the lock file that pins it, variables against locals, modules and where they end, workspaces as a second state, and graph drawing the wiring.
A diagram, the trap, and one thing to go run this week. That's a page.
terraform, one command per page
Thirty commands for the person who runs plan every week and was never shown the machinery underneath: the core loop of write, init, plan and apply, the state file that binds configuration to real objects, locking and force-unlock, rm that forgets without destroying, import that adopts what already exists, -replace instead of taint, required_providers and the lock file that pins it, variables against locals, modules and where they end, workspaces as a second state, and graph drawing the wiring.
Set in Space Grotesk, Inter and JetBrains Mono (SIL Open Font License).
Every fact, flag, default and number in this book is as the official Terraform documentation states it, fetched and read during this build: developer.hashicorp.com/terraform (CLI command reference for init, validate, fmt, plan, apply, destroy, import, taint, force-unlock, state and its subcommands, output, show, graph, workspace; the configuration language pages for state, state locking, backends, provider requirements, variables, locals, outputs, modules and workspaces; the plan symbols table and apply options). Version-specific notes (moved blocks, -replace, -refresh-only, import blocks, removed blocks) are stated with the versions the docs name. Teaching conventions (one command a page) are named as conventions. An independent guide, not affiliated with or endorsed by HashiCorp.
General information only. Not professional advice; check flags against your own Terraform version, which may differ.
© 2026 Steve Hodgkiss. All rights reserved. Personal use only; no redistribution rights.
Edition 1.0 · stevehodgkiss.net
Contents
The loop
The five commands every project runs, in the order they run: where Terraform reads, what init installs, what the lock file pins, and the two cheap checks before any plan.
- 01Write, init, plan, apply
- 02plan is the preview
Per the Terraform CLI overview at developer.hashicorp.com/terraform/cli/commands: the command line interface to Terraform is the terraform command, which accepts a variety of subcommands; Usage: terraform [global options] [args]; main commands init (prepare your working directory for other commands), validate (check whether the configuration is valid), plan (show changes required by the current configuration), apply (create or update infrastructure), destroy (destroy previously-created infrastructure).
Write, init, plan, apply
The whole tool turns on five words. You write .tf files, then init prepares the directory, plan previews the changes, apply makes them, and destroy takes everything away when the environment was temporary. validate is the cheap syntax check between writing and planning.
The order is not negotiable: a fresh clone that skips init fails at plan, and an apply without a plan reviewed is how accidents happen. The docs list init, validate, plan, apply, destroy as the main commands; everything else is support.
Learn the loop once. Every project you will ever touch runs it.
Start one throwaway directory today: write one .tf file, run init, plan, apply, destroy, in that order, and watch each one's output.
Per the terraform plan command reference at developer.hashicorp.com/terraform/cli/commands/plan: by default, when it creates a plan, Terraform reads the current state of any already-existing remote objects to make sure the state is up-to-date, compares the current configuration to the prior state and noting any differences, and proposes a set of change actions that should, if applied, make the remote objects match the configuration; the plan command alone does not actually carry out the proposed changes; if Terraform detects that no changes are needed it reports that no actions need to be taken.
plan is the preview
A plan is three moves in one. First it refreshes: reads the real objects so the state is up to date. Then it compares configuration to state, and proposes the actions that would make reality match the configuration.
The sentence that makes it safe to run anywhere: the plan command alone does not carry out the changes. A plan with nothing to do says so, plainly. Read the whole output before every apply; the numbers at the bottom are a contract.
The preview is free. Skipping it is the expensive choice.
Before your next apply, read the full plan out loud, including the Plan: summary line at the bottom.
State
The file that maps configuration to real objects: where it lives, how it locks, how to read it, forget from it, adopt into it, and how destroy is really just apply.
- 01State is the map
- 02The lock you never see
Per the Terraform state documentation at developer.hashicorp.com/terraform/language/state: Terraform must store state about your workspace's managed infrastructure and configuration; the primary purpose of Terraform state is to store bindings between objects in a remote system and resource instances declared in your configuration; by default Terraform stores each workspace's state in a local file named terraform.tfstate, and a backup of the previous state in terraform.tfstate.backup; Terraform uses state to determine which changes to make to your infrastructure; prior to any operation, Terraform does a refresh to update the state with the real infrastructure; do not directly edit this file.
State is the map
State is not a copy of your infrastructure; it is the binding table between resource addresses and real remote objects, plus metadata. When Terraform created that instance, it recorded the identity here, and every later plan uses that binding.
Default home: a local terraform.tfstate, previous copy in terraform.tfstate.backup. Operations refresh it against reality first. One blunt rule from the docs: do not directly edit this file; the state commands exist for that.
Configuration says what should exist. State says what it belongs to.
Run terraform state list in one project and count the bindings. Match each to a block in your .tf files.
Per the State Locking documentation at developer.hashicorp.com/terraform/language/state/locking: if supported by your backend, Terraform will lock your state for all operations that could write state; this prevents others from acquiring the lock and potentially corrupting your state; state locking happens automatically on all operations that could write state and you do not see any message that it happens; if acquiring the lock takes longer than expected, Terraform outputs a status message; not all backends support locking; you can disable locking for most commands with -lock=false, but this is not recommended.
The lock you never see
On any operation that could write state, and if the backend supports it, Terraform takes a lock first, silently. That is the whole protection: two applies cannot interleave their writes and corrupt the file. You never see a message unless acquiring takes a while.
Not every backend locks; the per-backend docs say which. Most commands accept -lock=false, and the docs do not recommend it; -lock-timeout=DURATION is the patient alternative, retrying for a period instead of failing fast.
The lock is invisible when healthy. Its error is the first sign two writers met.
Open two terminals against one workspace and run plan in both. Watch the second one wait for the lock.
Per the module block reference at developer.hashicorp.com/terraform/language/block/module: a module is a collection of multiple resources that Terraform manages together; the module block instructs Terraform to create resources defined in a local or remote module; source is required and specifies where Terraform retrieves the module source code, as a local file or directory or a remote module source such as the public Terraform registry; version is only available for modules listed in a registry; you must run terraform init after modifying the source argument; you can specify the same source address in two or more separate module blocks but must use unique labels; when the child module exposes output values, you can use the module.LABEL.OUTPUT syntax to reference them; module blocks also support count, for_each, depends_on and providers meta-arguments.
module blocks
A module block runs another folder's Terraform as part of yours. source is the required argument: a local path like ./modules/network, or a registry address, where version pins it. Any source change means a fresh init.
Same source, several blocks? Allowed, with unique labels each. Values come back through outputs, read as module.network.vpc_id. The block also takes count, for_each, depends_on and providers.
A module is a function call with a shopping trolley. Source is its address.
Trace one module block you depend on to its source and read the child module's variables and outputs.