Command Palette

Search for a command to run...

Hectal

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.

25 min Free 4 steps 2 break-it drills

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.

Code, state, and realitydiagram
Rendering diagram…

Part 2

Your project after this mission · 0 files change

shoplite/
  • 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. 1

    List and show — the safe way to read state

    Always start with the state subcommands; they understand the format and never corrupt anything. You should see the four resources from Stage 0.

    terminal
    $ cd infra
    terraform state list
    ── expected output ──
    aws_s3_bucket.assets
    aws_s3_bucket_public_access_block.assets
    aws_s3_bucket_versioning.assets
    random_id.suffix
  2. 2

    Look at the raw JSON once

    Open the file itself to see the structure: top-level version (format), serial, lineage, outputs, then resources. Each resource has a mode (managed for resources, data for data sources), type, name, the provider it belongs to, and instances, a list because count/for_each can create many instances of one resource (Stage 2).

    Notice dependencies: Terraform remembers that the bucket depends on random_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.tfstate
    jq '.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. 3

    Watch the serial change

    Every write bumps serial. Make a harmless change, such as adding Team = "shoplite" to default_tags in providers.tf, apply it, and compare. Terraform also keeps the previous version as terraform.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.tfstate
    terraform apply -auto-approve >/dev/null
    jq .serial terraform.tfstate
    ls terraform.tfstate*
    ── expected output ──
    11
    12
    terraform.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. 4

    Prove it isn't in Git

    git check-ignore explains which rule ignores a file. If this prints nothing, stop and fix .gitignore before 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, and instances are for.
  • ✓You know terraform.tfstate.backup is the previous version.
  • ✓git check-ignore confirms 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.

terminal
$ terraform plan
── what you'll see ──
# aws_s3_bucket.assets will be created
+ resource "aws_s3_bucket" "assets" {
+ bucket = (known after apply)
...
# random_id.suffix will be created
+ resource "random_id" "suffix" {
...
Plan: 4 to add, 0 to change, 0 to destroy.

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.

terminal
$ terraform plan
── what you'll see ──
╷
│ Error: Error acquiring the state lock
│
│ Error message: resource temporarily unavailable
│ Lock Info:
│ ID: 2b6a0d4e-11c3-8f5a-7e21-9c0d3b4a5f61
│ Path: terraform.tfstate
│ Operation: OperationTypeApply
│ Who: priya@laptop
│ Version: 1.13.3
│ Created: 2026-09-26 10:14:03.1234 +0000 UTC
│ Info:
│
│ Terraform acquires a state lock to protect the state from being written
│ by multiple users at the same time. Please resolve the issue above and try
│ again. For most commands, you can disable locking with the "-lock=false"
│ flag, but this is not recommended.
╵

Part 5

Interview questions from this mission

01

Why does Terraform need a state file? Couldn't it just query the cloud?

02

What are serial and lineage in a state file?

03

A colleague suggests committing terraform.tfstate to Git so the team can share it. What do you say?

04

What does terraform force-unlock do, and when is it safe to use?

0/4 · 0%