3.4 How Terraform Uses and Manages State

Key Takeaways

  • State stores bindings between configuration addresses and real remote object IDs, plus dependency metadata and a cache of attributes
  • The default local file is terraform.tfstate, with a previous snapshot in terraform.tfstate.backup
  • Do not commit state when it can contain secrets; local state is plaintext JSON and is the source of truth for those bindings
  • Terraform refreshes state from the real system during plan so the next apply starts from current remote objects
  • Each remote object should map to exactly one resource instance, for example aws_instance.web bound to i-abcd1234
Last updated: August 2026

3.4 How Terraform Uses and Manages State

Quick Answer: State is how Terraform remembers that aws_instance.web is EC2 instance i-0abc123def456. The default local file is terraform.tfstate. It also stores dependency metadata and a cache of attributes. Treat it as secret-bearing source-of-truth data: do not commit it to Git. Refresh during plan updates those bindings from the real API.

Objective 2d on Terraform Associate (004) asks you to explain how Terraform uses and manages state. This section is the fundamentals view. Remote backends, state locking, terraform state surgery, and drift workflows are later objectives (6a–6d, 7b). If a 004 item only asks why state exists or what the default file is, answer from this page. Official reference: State and Purpose of Terraform State.

Why Terraform cannot skip state

Terraform must map objects in your configuration to objects in the real world. When you write resource "aws_instance" "web", the AWS API returns an instance id such as i-0abc123def456. The next plan has to know that this resource address already owns that instance. Otherwise Terraform would create a second instance every run, or it would have no idea which instance to destroy when you remove the block.

HashiCorp's state docs call those records bindings between resource instances and remote objects. That is the primary purpose of state. Early Terraform prototypes tried to skip a state file and recover mappings from cloud tags. Not every resource type supports tags, and not every provider is a tag-friendly cloud, so Terraform keeps its own database.

State is required even when a provider could theoretically list every object. Without a binding, "list all instances and guess" is ambiguous the moment two aws_instance blocks look similar.

What state stores

ContentsWhy Terraform keeps it004 phrasing
Bindings (resource address → remote id)Know which real object a block already managesSource of truth for config-to-object mapping
Dependency metadataDestroy in a safe order after you delete blocks from HCLGraph edges survive after the configuration is gone
Pointer to the provider configuration last usedAliased providers stay attached to the right resourcesWhich aws alias owns this instance
Cached attribute valuesPlans can compare desired vs last-known without extra API callsPerformance cache; refresh updates it

Bindings — a concrete mapping

Configuration:

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

resource "aws_s3_bucket" "logs" {
  bucket = "app-logs-prod"
}

After the first successful apply, local state conceptually records:

aws_instance.web  →  i-0abc123def456   (us-east-1, t3.micro, ami-0c55b159cbfafe1f0)
aws_s3_bucket.logs →  app-logs-prod    (arn:aws:s3:::app-logs-prod)

If you later change instance_type to t3.small, Terraform does not create a second instance. It looks up aws_instance.web, finds i-0abc123def456, and asks the AWS provider to update that id. If you delete the aws_instance.web block, Terraform still sees the binding in state, plans a destroy of i-0abc123def456, and only then removes the binding.

Terraform expects a one-to-one mapping: each remote object is bound to exactly one resource instance. Importing the same instance id into two addresses makes the mapping ambiguous. Creating an object outside Terraform and then applying a new resource block that does not import it produces a second object — the first one is not in state, so Terraform does not know it exists.

Metadata and dependencies

Terraform normally builds the dependency graph from references in configuration (aws_instance.web.subnet_id = aws_subnet.app.id). When you delete those blocks, the HCL references disappear. State still holds the last known dependency set, so destroy can tear down the instance before the subnet. State also remembers which provider configuration (including aliases from 3.3) last managed the object.

Performance cache

State stores a copy of each resource's attributes. For small infrastructures Terraform still refreshes everything on every plan by default. For large infrastructures those Read calls are slow and can hit API rate limits; teams then use -refresh=false or -target and treat the cached attributes as truth. You do not need the performance-tuning flags for 2d, but you should know the cache exists so you do not claim "state is only ids."

Default local files

With no backend block, Terraform writes:

  • terraform.tfstate — the current snapshot, JSON text, in the working directory
  • terraform.tfstate.backup — the previous snapshot, rewritten on the next successful write

No extra configuration is required. That is the entire default. Local state is fine for a personal lab and a poor fit for a team: only one laptop has the file, and losing the laptop loses the bindings. HashiCorp recommends HCP Terraform or another remote backend for collaboration. How you configure those backends, and how locking works, is not this section.

Do not confuse state with other files in the same directory:

FileRoleCommit?
terraform.tfstateBindings, metadata, cached attributesNo, if it can contain secrets (the usual case)
terraform.tfstate.backupPrevious local snapshotNo
.terraform.lock.hclPinned provider versions and checksums (3.1)Yes
.terraform/Plugin cache and local backend metadataNo
*.tf / *.tfvarsConfiguration and variable values.tf yes; secret .tfvars no

Do not commit state that can hold secrets

Local state is plaintext JSON. Providers persist resource attributes there, including values that were sensitive in plan output. Database passwords, private keys, and initial access tokens routinely appear. HashiCorp's state page tells you to avoid storing state in a version-control system or any other store that lacks locking and access control, because you can lose data or expose secrets.

The sensitive argument on a variable or output redacts CLI display. It does not remove the value from terraform.tfstate. (Ephemeral values and write-only arguments, available by Terraform 1.12, can omit some secrets from state — that is objective 4h, not 2d.) For this objective, the exam answer is: treat terraform.tfstate as secret-bearing, add it to .gitignore, and use access-controlled remote storage when you work as a team.

Do not hand-edit the JSON. Use terraform state subcommands when you must change bindings. The on-disk format can change between Terraform versions; the CLI is the supported interface.

Refresh during plan

Prior to an operation, Terraform refreshes state: each provider reads the remote object currently bound to each resource instance and updates the cached attributes. That refresh is why plan can say "the instance type in AWS is still t3.micro but your configuration now says t3.small." It is also why a console-deleted instance becomes a planned create.

Refresh does not change the remote system by itself. It updates Terraform's record of the remote system. Apply is what calls create, update, or delete.

State remains the source of truth for bindings. The remote API is the source of truth for current attribute values after a refresh. If you skip refresh, the cached attributes play that role until the next successful read.

004 traps for objective 2d

  • .terraform.lock.hcl is not state.
  • terraform.tfvars is not state.
  • Refresh does not create or destroy remote objects.
  • sensitive = true does not strip a value out of terraform.tfstate.
  • Default local state is a file named terraform.tfstate in the working directory, not under .terraform/providers.
  • One remote object, one resource instance — never two bindings to the same id.
Loading diagram...
State binds a resource address to a real object
Test Your Knowledge

What is the primary purpose of Terraform state?

A
B
C
D
Test Your Knowledge

What is the default local filename for Terraform workspace state?

A
B
C
D
Test Your Knowledge

Why should a team avoid committing terraform.tfstate to Git?

A
B
C
D