State
Terraform is declarative: you describe the infrastructure you want, and Terraform figures out how to make reality match. To do that, it has to remember what it created last time. That memory lives in state — a snapshot that maps every resource in your configuration to a concrete object in your cloud provider. Without it, Terraform would have no way to know whether a resource already exists, what its current attributes are, or what needs to change on the next run.
What state is
State is a JSON document (by default a file named terraform.tfstate) that records the bindings between the resources declared in your .tf files and the real objects they correspond to. When you write this configuration:
resource "aws_s3_bucket" "assets" {
bucket = "devcraftly-assets-prod"
}
Terraform creates the bucket and then writes an entry into state linking the resource address aws_s3_bucket.assets to the real-world ID devcraftly-assets-prod, along with every attribute the provider returned. On the next terraform apply, Terraform reads state, refreshes those attributes against the live API, compares them to your configuration, and computes the difference.
You rarely read the raw file directly. Instead, inspect it with the CLI:
terraform state list
terraform state show aws_s3_bucket.assets
Output:
# aws_s3_bucket.assets:
resource "aws_s3_bucket" "assets" {
arn = "arn:aws:s3:::devcraftly-assets-prod"
bucket = "devcraftly-assets-prod"
id = "devcraftly-assets-prod"
region = "us-east-1"
tags_all = {}
}
OpenTofu uses the exact same state format and
tofu state ...subcommands, so everything in this page applies equally to both tools.
Why Terraform needs it
You might wonder why Terraform doesn’t just query the provider for the current infrastructure every time. There are three reasons state is essential.
| Purpose | What it gives you |
|---|---|
| Mapping | Links the logical name aws_s3_bucket.assets to a specific physical object, so renames and re-applies target the right resource. |
| Metadata | Stores resource dependencies and provider info that aren’t always recoverable from the live API, enabling correct create/update/destroy ordering. |
| Performance | Caches attribute values so large configurations can plan quickly instead of making thousands of API calls (you can still force a refresh). |
The mapping problem is the deepest one. Many resources have no field that maps cleanly back to your configuration name. State is the only authoritative record that this security group in AWS is the one you call aws_security_group.web.
What gets stored
State contains far more than IDs. For every managed resource it records the full set of attributes the provider returned after creation or update — instance types, IP addresses, ARNs, computed values, and the dependency relationships Terraform inferred. It also stores output values and the state format version.
Because attributes are stored verbatim, state can contain sensitive data. A database password set via aws_db_instance.password, a generated TLS private key, or an access token returned by a provider will all appear in plain text inside the state file — even if you mark the variable as sensitive in your configuration (that flag only hides values from CLI output, not from state).
resource "aws_db_instance" "primary" {
identifier = "devcraftly-db"
engine = "postgres"
instance_class = "db.t3.medium"
username = "appuser"
password = var.db_password # this lands in state as plaintext
}
Never commit
terraform.tfstateto version control. It frequently holds secrets, and Git history makes them permanent. Add*.tfstateand*.tfstate.backupto.gitignore.
Sharing state across a team
The default local state file works for a single person on a single machine, but it falls apart the moment a teammate runs terraform apply from their own laptop or a CI pipeline. Each copy diverges, and two people applying at once can corrupt the infrastructure.
The solution is a remote backend: state is stored in a shared, durable location, and Terraform coordinates access through locking so only one apply can run at a time. A common AWS setup uses an S3 bucket with native locking:
terraform {
backend "s3" {
bucket = "devcraftly-tfstate"
key = "prod/network/terraform.tfstate"
region = "us-east-1"
encrypt = true
use_lockfile = true
}
}
With this in place, every collaborator and pipeline reads and writes the same state, the file is encrypted at rest, and the lock prevents concurrent runs from clobbering each other. Other backends (Terraform Cloud, Azure Blob Storage, Google Cloud Storage) offer the same guarantees.
Best practices
- Use a remote backend with locking and encryption for anything beyond a personal experiment — never share state by copying files around.
- Treat the state file as sensitive: restrict who can read the backend, and keep it out of version control.
- Split large infrastructures into multiple states (per environment or component) so blast radius and plan times stay small.
- Avoid hand-editing the JSON; use
terraform state mv,terraform import, andterraform state rmto make targeted changes safely. - Enable versioning on your state storage (e.g. S3 object versioning) so you can recover from accidental corruption or deletion.
- Run
terraform planbefore everyapplyto see how the proposed changes compare against the recorded state.