Short answer: Terraform state is a JSON file (terraform.tfstate) that maps the resources in your code to the real resources in your cloud account. Terraform reads it on every plan and apply to work out what to create, change or destroy. For any team, store state in a remote backend such as Amazon S3, Azure Blob Storage, Google Cloud Storage or HCP Terraform, turn on state locking, encrypt it and never edit it by hand.
What Is Terraform State?
When you run terraform apply, Terraform creates real infrastructure: a VPC, an EC2 instance, a database. It then writes down what it created, including IDs, attributes and dependencies, in a state file. That file is Terraform's memory.
Without state, Terraform would have no way to know that aws_instance.web in your code is the instance i-0abc123 running in your account. Every plan would try to create everything again.
If you are new to the tool, start with our guide on what Terraform is and how Infrastructure as Code works.
Why Does Terraform Need a State File?
State does four jobs:
- Mapping code to reality. It links each resource block to a real cloud object ID.
- Tracking metadata. It stores dependency order, so Terraform knows to delete a server before the subnet it lives in.
- Performance. It caches attribute values, so large projects do not have to query every resource from the cloud API on every run.
- Detecting drift. On each plan, Terraform refreshes state and compares it with your code and the real world, so you can spot manual changes made in the console.
What Is Inside a terraform.tfstate File?
A state file is plain JSON. A trimmed example looks like this:
{
"version": 4,
"terraform_version": "1.9.8",
"serial": 12,
"lineage": "3f2a9c1e-...",
"resources": [
{
"mode": "managed",
"type": "aws_s3_bucket",
"name": "logs",
"instances": [
{ "attributes": { "id": "acme-prod-logs", "arn": "arn:aws:s3:::acme-prod-logs" } }
]
}
]
}
- serial goes up by one every time state changes.
- lineage is a unique ID that stops you from accidentally overwriting one project's state with another's.
- resources holds every managed object and all of its attributes.
Important: state stores attribute values in plain text, including database passwords, private keys and tokens if a resource returns them. Treat the state file as a secret.
Local State vs Remote State
By default Terraform writes state to a local file in your working directory. That is fine for learning, and risky for anything else.
- Where it lives: local state sits on your laptop; remote state lives in S3, Azure Blob, GCS or HCP Terraform.
- Team sharing: local state cannot be shared safely; remote state can.
- State locking: local state locks only on one machine; remote backends lock across every user and CI job.
- Encryption at rest: local depends on your disk; remote uses KMS or provider-managed keys.
- Versioning and recovery: local needs manual backups; remote gets bucket versioning and state history.
- Best for: local for tutorials and quick tests; remote for any shared or production project.
Rule of thumb: if more than one person or one CI pipeline touches the code, use remote state.
What Is a Terraform Backend?
A backend decides where Terraform stores state and how it locks it. You configure it inside the terraform block. Popular backends include:
- s3 for AWS
- azurerm for Azure Blob Storage
- gcs for Google Cloud Storage
- HCP Terraform (the
cloudblock), which also gives you a UI, run history and policies. See Terraform Cloud vs Terraform OSS. - kubernetes, pg (PostgreSQL), consul and http for special cases
How to Set Up a Remote Backend on AWS S3 (2026 Way)
Since Terraform 1.10, the S3 backend supports native state locking using a lock file in the same bucket. You no longer need a DynamoDB table. In Terraform 1.11 and later, the dynamodb_table argument is deprecated, so new projects should use use_lockfile.
Step 1: Create a secure state bucket
Create the bucket once, in a separate bootstrap project, with versioning, encryption and public access blocked:
resource "aws_s3_bucket" "tf_state" {
bucket = "acme-terraform-state-prod"
}
resource "aws_s3_bucket_versioning" "tf_state" {
bucket = aws_s3_bucket.tf_state.id
versioning_configuration { status = "Enabled" }
}
resource "aws_s3_bucket_server_side_encryption_configuration" "tf_state" {
bucket = aws_s3_bucket.tf_state.id
rule {
apply_server_side_encryption_by_default { sse_algorithm = "aws:kms" }
}
}
resource "aws_s3_bucket_public_access_block" "tf_state" {
bucket = aws_s3_bucket.tf_state.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
Step 2: Point your project at the backend
terraform {
required_version = ">= 1.10"
backend "s3" {
bucket = "acme-terraform-state-prod"
key = "network/prod/terraform.tfstate"
region = "ap-south-1"
encrypt = true
use_lockfile = true
}
}
Step 3: Initialise and migrate
terraform init -migrate-state
Terraform will copy your existing local state into S3. Confirm with yes, then delete the local terraform.tfstate and its .backup file from your machine.
Migrating from DynamoDB locking? Add use_lockfile = true while keeping dynamodb_table for one release cycle, run terraform init -reconfigure, confirm locks work, then remove the DynamoDB argument and table.
What Is Terraform State Locking?
State locking stops two people, or two pipeline runs, from changing the same state at the same time. When apply starts, Terraform takes a lock. Anyone else who runs a write operation sees an error like "Error acquiring the state lock" and has to wait.
Without locking, two concurrent applies can both read the same old state and write conflicting results. That is how teams end up with orphaned resources and corrupted state.
If a run crashes and leaves a stale lock, you can release it, but only after you are sure no other run is active:
terraform force-unlock <LOCK_ID>
Essential Terraform State Commands
terraform state listlists every resource tracked in state.terraform state show <address>shows all attributes of one resource.terraform state mv <from> <to>renames or moves a resource address without destroying it.terraform state rm <address>stops tracking a resource but leaves it running in the cloud.terraform state pulldownloads the current remote state to stdout.terraform importbrings an existing cloud resource under Terraform management.terraform plan -refresh-onlyshows drift between state and the real world without changing anything.
Prefer config blocks over CLI surgery
Modern Terraform lets you make most state changes in code, where they are reviewed in a pull request:
movedblocks (1.1+) replaceterraform state mvwhen you rename resources or refactor into reusable modules.importblocks (1.5+) replace theterraform importcommand and can even generate config for you.removedblocks (1.7+) replaceterraform state rm.
moved {
from = aws_instance.web
to = module.app.aws_instance.web
}
import {
to = aws_s3_bucket.legacy
id = "acme-legacy-bucket"
}
How to Structure State: One File or Many?
Putting your whole company in a single state file is a common mistake. Plans get slow, the blast radius of a bad apply is huge and everyone fights for the same lock.
Instead, split state by environment and component:
acme-terraform-state-prod/ network/prod/terraform.tfstate database/prod/terraform.tfstate app/prod/terraform.tfstate acme-terraform-state-dev/ network/dev/terraform.tfstate app/dev/terraform.tfstate
Use separate buckets, or at least separate keys and IAM permissions, for production and non-production. A developer who can change dev should not be able to read prod state.
Workspaces vs separate directories
Terraform CLI workspaces let one configuration keep several state files (for example default, staging, prod). They are handy for short-lived copies of the same stack. For long-lived environments with different settings and access rules, most teams prefer separate directories or separate root modules, because it is harder to apply to prod by mistake.
Sharing outputs between stacks
Use the terraform_remote_state data source, or better, publish values to a parameter store such as AWS SSM Parameter Store, so one stack can read another stack's outputs without full access to its state.
Terraform State Best Practices
- Always use a remote backend for shared or production code.
- Enable state locking (
use_lockfileon S3; built in on Azure Blob, GCS and HCP Terraform). - Encrypt state at rest with KMS or provider-managed keys, and enforce HTTPS.
- Turn on bucket versioning so you can roll back a corrupted or deleted state file.
- Lock down access with IAM. Only CI and a small admin group should read or write prod state.
- Never commit state to Git. Add
*.tfstate,*.tfstate.*and.terraform/to.gitignore. - Never edit state by hand. Use
moved,importandremovedblocks, or theterraform statecommands. - Keep secrets out of state where you can. Use ephemeral resources and write-only arguments in newer Terraform versions, or fetch secrets at runtime from a vault. Read our guide to secrets management in DevOps.
- Split state by environment and component to cut blast radius and plan time.
- Run applies from CI/CD, not laptops. One pipeline with one identity makes locking and auditing simple. See CI/CD pipeline security best practices.
- Check for drift on a schedule with
terraform plan -refresh-onlyorplan -detailed-exitcode, and alert when it finds changes. - Pin your Terraform and provider versions so a newer CLI does not upgrade the state format unexpectedly.
Common Terraform State Problems and How to Fix Them
"Error acquiring the state lock"
Another run is active, or a crashed run left a lock. Check your CI for running jobs first. Only if nothing is running, use terraform force-unlock <LOCK_ID>.
Resource exists in the cloud but not in state
Someone created it manually, or it was removed from state. Bring it back with an import block, then run plan and confirm Terraform shows no changes.
Terraform wants to destroy and recreate after a rename
You renamed a resource or moved it into a module. Add a moved block from the old address to the new one and the plan will show a move instead of a replace.
State file was deleted or corrupted
Restore the previous version from bucket versioning (or HCP Terraform state history), then run terraform plan to check it matches reality. This is why versioning is not optional.
Drift from manual console changes
Run terraform plan -refresh-only to see what changed. Either update your code to match, or run apply to put the infrastructure back to what the code says.
Terraform State vs OpenTofu State
OpenTofu, the open-source fork of Terraform, uses a compatible state format and the same backends, so migration is usually simple. One notable extra: OpenTofu supports client-side state encryption, so state is encrypted before it ever leaves your machine. Compare the two in our Terraform vs OpenTofu guide.
Key Takeaways
- Terraform state is the record that links your code to real infrastructure.
- Use a remote backend with locking, encryption and versioning for every shared project.
- On AWS, use the S3 backend with
use_lockfile = true; DynamoDB locking is deprecated. - Change state through
moved,importandremovedblocks, not by hand. - Split state by environment and component, and run applies from CI.
Conclusion
State is the part of Terraform that beginners ignore and experienced engineers obsess over, for good reason. Get it right on day one with a locked, encrypted, versioned remote backend and a sensible split by environment, and most of the scary Terraform stories never happen to you.
Preparing for an interview? State management comes up in almost every round. Practise with our Terraform interview questions, and if you want hands-on practice with Terraform, AWS and CI/CD on real projects, explore the DevOps courses at PaperLive Learning.


