Mission 1.1 · Stage 1 — State
Open the State File
Goal: Know exactly what terraform.tfstate contains, how Terraform uses it on every run, and what goes wrong when it's lost, stale, or edited by two people at once.
By the end of this mission
- Read the structure of a state file: serial, lineage, resources, instances
- Explain why state exists instead of Terraform re-discovering everything
- Know why state is sensitive and must never be committed
- See what happens when state is lost or locked
Part 1
Understand it first
Why Terraform needs state at all
Your code says aws_s3_bucket.assets exists. Your AWS account contains dozens of buckets. Which one is 'assets'? Tags could be changed, names could collide, and many AWS resources have no name at all, only a generated ID like vpc-0a1b2c3d. Terraform needs a reliable, explicit record: 'the thing I call aws_s3_bucket.assets is the real bucket with ID shoplite-dev-assets-3f9a'. That mapping is STATE.
State also stores the last-known attributes of every resource (so plans are fast and diffs are precise), dependency information (so destroys happen in the right order even after you delete code), and output values. On each run Terraform loads state, refreshes it against reality, and then compares it with your code.
Serial and lineage: how Terraform protects state
lineage is a UUID assigned when a state is first created; it never changes. It lets Terraform refuse to overwrite one environment's state with a different environment's. serial increments every time state is written. When pushing state, Terraform rejects a lower serial or a different lineage. That's what stops an old copy on someone's laptop from overwriting newer state.
State is a secret
State stores every attribute of every resource in plain JSON. That includes things you'd never commit: database passwords if you passed one as an argument, private keys generated by tls_private_key, random passwords, connection strings. sensitive = true hides values from terminal output, not from state.
So state must be kept out of Git, stored encrypted, readable only by people and pipelines that run Terraform, and backed up with version history. Mission 1.2 does all of that. Stage 4 shows how to keep secrets out of state in the first place.
Locking: one writer at a time
If two applies run at once, both read serial 9, both change infrastructure, and both try to write serial 10. One set of changes is lost from state even though it happened in AWS. To prevent this, every operation that may write state first takes a LOCK. A second operation fails immediately with 'Error acquiring the state lock' instead of racing.
The local backend locks with an OS file lock, which only protects you from yourself on one machine. Real teams need a lock everyone shares, which the S3 backend provides in Mission 1.2.
Part 2
Your project after this mission · 0 files change
- infra/
- .terraform.lock.hcl
- locals.tf
- main.tf
- outputs.tf
- providers.tf
- terraform.tfstate
- terraform.tfvars
- variables.tf
- versions.tf
- .gitignore
Part 3
Build it, step by step
- 1
List and show — the safe way to read state
Always start with the
statesubcommands; they understand the format and never corrupt anything. You should see the four resources from Stage 0.terminal$ cd infraterraform state list── expected output ──aws_s3_bucket.assetsaws_s3_bucket_public_access_block.assetsaws_s3_bucket_versioning.assetsrandom_id.suffix - 2
Look at the raw JSON once
Open the file itself to see the structure: top-level
version(format),serial,lineage,outputs, thenresources. Each resource has amode(managedfor resources,datafor data sources),type,name, theproviderit belongs to, andinstances, a list becausecount/for_eachcan create many instances of one resource (Stage 2).Notice
dependencies: Terraform remembers that the bucket depends onrandom_id.suffix, so it can still destroy things in the correct order after you delete them from code.terminal$ jq '{version, serial, lineage, resources: [.resources[] | {mode, type, name}]}' terraform.tfstatejq '.resources[] | select(.type=="aws_s3_bucket") | .instances[0] | {id: .attributes.id, arn: .attributes.arn, dependencies}' terraform.tfstate── expected output ──{"version": 4,"serial": 11,"lineage": "7d2f1c3a-9b4e-4f0a-8c21-5e6d7f8a9b0c","resources": [{ "mode": "managed", "type": "aws_s3_bucket", "name": "assets" },{ "mode": "managed", "type": "aws_s3_bucket_public_access_block", "name": "assets" },{ "mode": "managed", "type": "aws_s3_bucket_versioning", "name": "assets" },{ "mode": "managed", "type": "random_id", "name": "suffix" }]}{"id": "shoplite-dev-assets-3f9a","arn": "arn:aws:s3:::shoplite-dev-assets-3f9a","dependencies": [ "random_id.suffix" ]}Install jq with your package manager if you don't have it. `terraform show -json` gives the same data in a stable, documented format for scripts. - 3
Watch the serial change
Every write bumps
serial. Make a harmless change, such as addingTeam = "shoplite"todefault_tagsinproviders.tf, apply it, and compare. Terraform also keeps the previous version asterraform.tfstate.backup, which is one level of undo for a local backend. Remove the tag again and apply once more afterwards, so the code stays as Stage 0 left it.terminal$ jq .serial terraform.tfstateterraform apply -auto-approve >/dev/nulljq .serial terraform.tfstatels terraform.tfstate*── expected output ──1112terraform.tfstate terraform.tfstate.backup`-auto-approve` skips the prompt. Only use it for changes you've already reviewed; in this course, that's CI in Stage 7. - 4
Prove it isn't in Git
git check-ignoreexplains which rule ignores a file. If this prints nothing, stop and fix.gitignorebefore anything else, because state must never be committed.terminal$ git check-ignore -v terraform.tfstate terraform.tfstate.backup── expected output ──.gitignore:5:*.tfstate terraform.tfstate.gitignore:6:*.tfstate.* terraform.tfstate.backup
Checkpoint — you should now have
- ✓You can explain what
serial,lineage,resources, andinstancesare for. - ✓You know
terraform.tfstate.backupis the previous version. - ✓
git check-ignoreconfirms both state files are ignored. - ✓You can explain why state must be treated as a secret.
Part 4
Break it on purpose
Make each change, run the command, and read the error before revealing the diagnosis. Recognising these messages on sight is what makes you fast on a real team. Undo the change afterwards.
Break #1
Lose the state file
Move state aside to simulate a lost laptop: mv terraform.tfstate /tmp/lost.tfstate, then run terraform plan. Do NOT apply.
Break #2
Run two applies at the same time
Open two terminals in infra/. In the first, run terraform apply and leave it waiting at the Enter a value: prompt. In the second, run terraform plan.
Part 5
Interview questions from this mission
Why does Terraform need a state file? Couldn't it just query the cloud?
What are serial and lineage in a state file?
A colleague suggests committing terraform.tfstate to Git so the team can share it. What do you say?
What does terraform force-unlock do, and when is it safe to use?