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
3.4 How Terraform Uses and Manages State
Quick Answer: State is how Terraform remembers that
aws_instance.webis EC2 instancei-0abc123def456. The default local file isterraform.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
| Contents | Why Terraform keeps it | 004 phrasing |
|---|---|---|
| Bindings (resource address → remote id) | Know which real object a block already manages | Source of truth for config-to-object mapping |
| Dependency metadata | Destroy in a safe order after you delete blocks from HCL | Graph edges survive after the configuration is gone |
| Pointer to the provider configuration last used | Aliased providers stay attached to the right resources | Which aws alias owns this instance |
| Cached attribute values | Plans can compare desired vs last-known without extra API calls | Performance 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 directoryterraform.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:
| File | Role | Commit? |
|---|---|---|
terraform.tfstate | Bindings, metadata, cached attributes | No, if it can contain secrets (the usual case) |
terraform.tfstate.backup | Previous local snapshot | No |
.terraform.lock.hcl | Pinned provider versions and checksums (3.1) | Yes |
.terraform/ | Plugin cache and local backend metadata | No |
*.tf / *.tfvars | Configuration 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.hclis not state.terraform.tfvarsis not state.- Refresh does not create or destroy remote objects.
sensitive = truedoes not strip a value out ofterraform.tfstate.- Default local state is a file named
terraform.tfstatein the working directory, not under.terraform/providers. - One remote object, one resource instance — never two bindings to the same id.
What is the primary purpose of Terraform state?
What is the default local filename for Terraform workspace state?
Why should a team avoid committing terraform.tfstate to Git?