Terraform Fundamentals

State: The Heart of Terraform

18 min Lesson 5 of 30

State: The Heart of Terraform

Every time Terraform creates, updates, or destroys infrastructure, it records the outcome in a single authoritative file called state. Without state, Terraform would have no way to know what it already built, and every plan would try to create everything from scratch. State is therefore not a convenience feature — it is the mechanism that makes idempotent, incremental infrastructure management possible.

What State Actually Is

State is a JSON document (typically named terraform.tfstate) that maps every resource in your configuration to its real-world counterpart in the provider API. For an AWS EC2 instance, the state file holds the instance ID, AMI, private IP, security group associations, tags, and dozens of other computed attributes that Terraform did not know at plan time. These stored attributes are used to:

  • Compute diffs — compare desired config against actual cloud state without making API calls for every attribute.
  • Build the dependency graph — cross-resource references like aws_subnet.main.id are resolved from state, not from live API calls.
  • Detect drift — when the real infrastructure is modified outside Terraform, state reveals the discrepancy.
  • Enable targeted operations — terraform apply -target=aws_instance.web works because Terraform knows which resources exist.
State is the source of truth for what Terraform manages — not the code. Your HCL describes intent; state describes reality as Terraform last observed it. The plan phase reconciles the two.

The Three-Way Reconciliation

Every terraform plan performs a three-way diff:

  1. Desired state — your .tf files.
  2. Recorded stateterraform.tfstate.
  3. Actual state — a fresh read from the provider API (a "refresh").

Terraform compares desired against recorded-plus-refreshed to produce the plan. This is why terraform plan makes real API calls: it needs to refresh the recorded state before diffing. You can skip the refresh with -refresh=false for speed, but only do that when you are certain nothing changed out-of-band.

Terraform Three-Way State Reconciliation HCL Config (.tf files) Desired State State File (tfstate) Recorded State Cloud API (AWS / GCP / Azure) Actual State Plan Engine 3-way diff + dep graph Execution Plan + / ~ / - changes listed desired recorded refresh
Three inputs feed the Plan Engine: desired HCL, recorded state, and a live API refresh — producing the execution plan.

Anatomy of the State File

The state file is not meant to be hand-edited, but reading it teaches you exactly what Terraform tracks. After running terraform apply on a simple VPC and subnet, the file contains a resources array where each entry carries:

  • modemanaged (created by Terraform) or data (read-only data source).
  • type and name — e.g., aws_vpc / main.
  • provider — the provider address that owns this resource.
  • instances — an array (one entry per count or for_each instance) each holding schema_version and a full attributes map.

Inspecting State Safely

Never open terraform.tfstate in a text editor for routine inspection. Use the CLI commands that parse and display state cleanly, and that respect locking when remote state is in use.

# List every resource Terraform knows about terraform state list # Inspect one resource in detail (shows all attributes) terraform state show aws_vpc.main # Pull the entire state as formatted JSON (read-only — safe for scripting) terraform show -json | jq '.values.root_module.resources[] | {address, type, values}' # Check state version and serial number terraform show # Refresh state from the provider without making changes terraform apply -refresh-only

The output of terraform state show aws_vpc.main looks like this when your VPC is live:

# aws_vpc.main: resource "aws_vpc" "main" { arn = "arn:aws:ec2:us-east-1:123456789012:vpc/vpc-0abc123" cidr_block = "10.0.0.0/16" default_route_table_id = "rtb-0def456" dhcp_options_id = "dopt-0ghi789" enable_dns_hostnames = true enable_dns_support = true id = "vpc-0abc123" instance_tenancy = "default" main_route_table_id = "rtb-0def456" owner_id = "123456789012" tags = { "Environment" = "production" "Name" = "main" } tags_all = { "Environment" = "production" "Name" = "main" } }
Use terraform state list as a health-check habit. In a large root module, seeing an unexpected resource in the list — or a missing one — is your first signal that something drifted or that a refactor broke an address. Run it after every apply in CI and pipe the output to a diff against the previous run.

State Operations You Must Know

Beyond inspection, the terraform state subcommand gives you surgical control:

  • terraform state mv — rename or move a resource address in state without destroying and recreating it. Essential when refactoring a flat config into modules.
  • terraform state rm — remove a resource from state without touching the real infrastructure. Used when you want Terraform to "forget" a resource (e.g., you are handing it to another team's state).
  • terraform import — bring an existing cloud resource under Terraform management by writing its attributes into state. Required for brownfield adoption.
State manipulation is permanent and irreversible. A wrong state rm followed by an apply will destroy the real resource. A wrong state mv can cause a replace on the next plan. Always take a state backup first: terraform state pull > backup.tfstate. In production, never run state subcommands without a peer review and a rollback plan.

The State Serial and Version Guard

Each write to state increments a serial integer. If two engineers apply at the same time from different machines — both starting from serial 7 — the second one to finish will find that the remote state is already at serial 8 and refuse to overwrite it. This is how state acts as an optimistic concurrency lock when using a remote backend. Local terraform.tfstate has no such protection, which is exactly why local state is only safe for a single operator.

Sensitive Values in State

Terraform stores all resource attributes in state, including secrets like database passwords, private keys, and access tokens. Even if you mark an output as sensitive = true, the value is still present in plaintext inside the state file. This has direct production security implications:

  • Never commit terraform.tfstate or terraform.tfstate.backup to version control. Add both to .gitignore immediately.
  • Use a remote backend with encryption at rest (S3 + SSE-KMS, or Terraform Cloud) so state is never stored on a developer laptop.
  • Restrict IAM / RBAC access to the state bucket to the CI/CD role and lead engineers only.
State as a security perimeter. At companies like Google, Meta, and Stripe, the Terraform state bucket is treated with the same access controls as a secrets vault. Read access to state == read access to every secret Terraform ever wrote. Remote state with fine-grained IAM and server-side encryption is the baseline; Terraform Cloud's encrypted state with audit logs is the gold standard.