Skip to main content
RunBook Academy

TerraformIX · State: The Core Production ConceptProduction Terraform

Why State Exists

Foundation⏱ ~12 minbash

What you'll learn

  • Explain why Terraform needs state when other IaC tools do not
  • Describe the three roles of state: mapping, metadata cache, and lock target
  • Identify the state trust boundary between declared config and real infrastructure
  • Recognise why the state file must live in a remote backend in production

Prerequisites

None — start here.

Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13

Not yet marked complete on this device.

At 03:12 on a Tuesday morning, a senior engineer runs terraform apply in the production account. The plan says “no changes”. An hour later a different engineer — also in the same account, also authenticated, also following the runbook — runs terraform apply from a separate laptop. The plan says “no changes”. Two applies, both reading the same state, both correct, both safe. That is the entire reason state exists.

What state is, and what it is not

Terraform state is a JSON document that Terraform maintains alongside the configuration. It records, for each resource the configuration describes, the real-world identifier Terraform last saw and the attribute values Terraform last read. It is not the source of truth for what exists. The provider API and the cloud control plane are the source of truth. State is Terraform’s working memory.

Configuration (.tf files)        →  Declared intent
Provider API (AWS, GCP, …)      →  Real infrastructure
State (terraform.tfstate)       →  Terraforms working memory

Three distinct things, each with a different role. The configuration is reviewable in Git. The real infrastructure is queryable in the provider. The state is the bridge that lets Terraform compare the two and propose a change.

The three jobs state does

State exists because Terraform needs to do three things the configuration alone cannot do:

1. Map a declared address to a real-world ID. A configuration file says resource "aws_instance" "web". The cloud provider knows nothing about that string. State records the bridge:

{
  "mode": "managed",
  "type": "aws_instance",
  "name": "web",
  "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
  "instances": [
    {
      "schema_version": 1,
      "attributes": {
        "id": "i-0a1b2c3d4e5f6a7b8",
        "ami": "ami-0c55b159cbfafe1f0",
        "instance_type": "t3.medium"
      }
    }
  ]
}

The string aws_instance.web is meaningless to AWS. The string i-0a1b2c3d4e5f6a7b8 is meaningless to Terraform. State holds the two together.

2. Cache the last-known attribute values. During a plan, Terraform does not call the provider API for every attribute of every resource. For a state with 1,000 resources, that would be hundreds of API calls and a slow plan. Terraform reads most attributes from state and only refreshes the ones it needs to verify. This is the cache. It is also why a stale state produces a misleading plan — see the next lesson.

3. Act as the lock target. When two operators attempt an apply at once, they will both propose to create the same resources, both will succeed against the cloud API, and the team will own two of everything. The state backend holds a lock so only one apply can mutate state at a time. The lock table is part of the backend contract; the state itself is the object being protected.

Apply #1 (Alice)  ──►  acquire lock  ──►  mutate state  ──►  release lock
Apply #2 (Bob)    ──►  acquire lock (blocked)  ──►  queue

Why configuration alone is not enough

A reasonable objection: the configuration already says what should exist. Why is a separate file needed?

Because the configuration describes the declared intent. The provider API holds the real world. Terraform needs both to compute the difference. Without state, every plan would require a full read of every resource, which is slow, expensive against rate-limited APIs, and impossible against APIs that do not support attribute reads. State is the local index that makes the comparison tractable.

resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.medium"
}

The configuration does not say which i-0abc… instance Terraform should manage. State does. Without state, Terraform has no idea which cloud object corresponds to aws_instance.web.

Why state must be remote in production

In a single-operator lab, a local terraform.tfstate on the laptop is fine. In a production team it is not. Three reasons:

RiskLocal stateRemote backend
Lost laptopState lost, infra unmanagedState in S3 with versioning
Two operators, one stateLast write wins, lock impossibleBackend enforces lock
Audit trailNoneBackend access logs, version history

The remote backend is the production control. S3 with DynamoDB locking (or the Terraform Cloud / OSS alternatives) gives durable storage, locking, versioning, and an access log. The state lessons later in this part cover each of these in detail.

The trust boundary

State is the trust boundary between declared intent (the configuration, in Git) and real infrastructure (the cloud control plane). Anyone who can read or write the state file has, in effect, the ability to declare what exists in production. That is the threat model the security lessons explore.

   Git (config)          State file          Cloud API
       │                    │                    │
   declared intent  ◄── trust boundary ──►  real infra
       │                    │                    │
   reviewer controls     IAM controls        provider IAM controls

Validation

READ-ONLY

Inspect the state file the configuration expects:

terraform state list

A non-empty list confirms Terraform is reading state. Empty output or an error confirms the state backend is reachable but contains no resources. An authentication error confirms the backend credential is missing or wrong.

Pull the raw state to verify its structure:

terraform state pull | jq '.version, .serial, .lineage, .outputs | length'

Output (illustrative):

4
27
"a1b2c3d4-e5f6-7890-abcd-ef1234567890"
3

Three outputs: the state format version (4 in current Terraform), the serial (incremented on every write), and the lineage (a UUID that uniquely identifies this state lineage from its initial creation). The next lesson covers the full structure.

Production failure modes

Symptom: plan shows no changes, but the real world has drifted. Cause: state is stale. Plan is computed against the state cache, not against a fresh read of the cloud API. Run terraform apply -refresh-only to refresh the cache without making changes, then re-plan.

Symptom: “Error: state lock acquire failed”. Cause: another apply holds the lock. Either a previous apply crashed without releasing, or two operators are running concurrently. Inspect the lock table for the operation ID and decide whether to wait or to break the lock (with team agreement, never alone).

Symptom: “Error: backend reinitialised” or unexpected state serial. Cause: the backend configuration or credentials changed mid-operation. The serial increments on every write; an unexpected jump means two operators wrote concurrently or the lock failed. Investigate before running again.

Symptom: state file on a developer laptop, infrastructure in prod. Cause: an operator ran terraform init against local. The remote backend was never configured, or was bypassed. The fix is to configure the backend before any production apply.

Symptom: state grows without bound; plans slow to minutes. Cause: state is collecting resources from many modules and workspaces. This is not a state problem per se, but a state hygiene problem. The mapping and structure lessons cover the discipline.

Recovery

The recovery procedure for state issues is covered in the recovery part of this course. The short version:

  1. Stop all applies. The state is the trust boundary; do not write to it under pressure.
  2. Identify the cause (lock, drift, corruption, deletion).
  3. Read-only inspect with terraform state list, terraform state pull.
  4. Apply the specific remediation per cause.
  5. Verify with terraform plan (read-only) before any apply.

What comes next

The next lesson covers the JSON structure of the state file: every top-level field, what it means, and how to read it without breaking it.

Verification

  • You can explain why Terraform needs state when other tools (Pulumi, CloudFormation) handle it differently.
  • You can list the three jobs state does (mapping, cache, lock).
  • You can name the production trust boundary between declared intent and real infrastructure.
  • You can run terraform state list and terraform state pull and explain the output.

Knowledge check · 7 questions

  1. Q1. Which is NOT one of the three jobs Terraform state does?

  2. Q2. The state file is the source of truth for what infrastructure exists.

  3. Q3. Where should production state live?

  4. Q4. Which of these are stored in the state file? (Select all that apply.)

  5. Q5. A plan shows no changes but the real world has drifted. The state is:

  6. Q6. An operator runs `terraform init` and Terraform creates a local terraform.tfstate in the working directory. The team uses this operator for production applies. What is the production risk?

  7. Q7. Why does Terraform need state when the configuration already declares what should exist?

Passing score: 75%. Answers are checked in this browser.