← All 77 books terraform, one command per page Get the full edition · £10
One command per 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.


Steve Hodgkiss 5 commands

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

Contents


Part 1 · The loop4
Write, init, plan, apply5
plan is the preview6
Part 2 · State7
State is the map8
The lock you never see9
Part 3 · Modules
module blocks10
Part 1 of 3
write, init, plan, apply
1

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.


In this part
  1. 01Write, init, plan, apply
  2. 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).

terraform · No. 01
The loop

Write, init, plan, apply

Four commands, one order

THE CORE LOOP EVERY PROJECT RUNSinitplanapplydestroyyou write .tfvalidate sits between writing and planning; the order is not negotiable

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.

TRY THIS WEEK

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.

terraform · No. 02
The loop

plan is the preview

Refresh, diff, propose

WHAT A PLAN ACTUALLY DOES1 refresh2 compare3 proposereal objects.tfstatetfstatebindings + metadataPlan: 1 to add, 1 to changeno actions needednothing is executedthe preview is free: read the whole output, then decide

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.

TRY THIS WEEK

Before your next apply, read the full plan out loud, including the Plan: summary line at the bottom.

Part 2 of 3
bindings, locks, surgery
2

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.


In this part
  1. 01State is the map
  2. 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.

terraform · No. 03
State

State is the map

Bindings, not a mirror

STATE IS THE BINDING TABLEconfigurationresource "aws_instance""web" { ... }stateterraform.tfstatebindings + metadatai-abcd1234addressbindingnot a copy of the cloud: bindings plus metadata, refreshed before operations

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.

TRY THIS WEEK

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.

terraform · No. 04
State

The lock you never see

Automatic on writes

THE LOCK YOU NEVER SEEstatestatebindings + metadata$ terraform apply$ terraform applywaits for the lockwrites only; silent when healthy

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.

TRY THIS WEEK

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.

terraform · No. 05
Modules

module blocks

Source, then init

MODULE: ANOTHER FOLDER'S TERRAFORMparent (root module)module "network" {source = "./modules/network"}child modulevpcsubnetsmodule.network.vpc_idany source change: initversion: registry onlyoutputs flow backsame source twice: fine, unique labels required

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.

TRY THIS WEEK

Trace one module block you depend on to its source and read the child module's variables and outputs.

Index

Index


module blocks10
plan is the preview6
State is the map8
The lock you never see9
Write, init, plan, apply5