Overview and What You Will Learn
Terraform state is the most important concept to understand after you know how to write HCL. Without it, Terraform would have no way to know what infrastructure it created last time — it would try to create everything from scratch on every apply, hit naming conflicts, and leave your cloud account in chaos.
In this guide you will learn exactly what the state file contains, why it is Terraform's source of truth, how to use state commands to inspect and surgically modify state, and what happens when state gets out of sync with reality.
By the end you will be able to:
- Explain what the terraform.tfstate file contains and why Terraform needs it
- Use
terraform state listandterraform state showto inspect any resource - Move, rename, and remove resources from state without destroying them
- Understand what drift is and how to detect it
- Know exactly what to do when state gets corrupted or out of sync
Why This Matters in Production
At Zerodha, a junior engineer was cleaning up their local terraform directory and deleted the terraform.tfstate file thinking it was a build artifact. They ran terraform apply to recreate it — Terraform had no memory of what it had created, so it tried to create everything again. The S3 bucket names conflicted. The security group names conflicted. The apply failed halfway through, leaving the environment in a broken state that took three hours to untangle.
Understanding state — what it is, where it lives, and how to protect it — is the difference between Terraform being a reliable tool and a liability.
Core Principles
State as the Bridge Between Code and Reality
+------------------------------------------+| Your .tf files (desired state) || resource "aws_instance" "app" { || instance_type = "t3.large" || } |+------------------------------------------+ | v+------------------------------------------+| terraform.tfstate (Terraform's memory) || { || "resource": "aws_instance.app", || "id": "i-0a1b2c3d4e5f", || "instance_type": "t3.large", || "public_ip": "13.126.x.x" || } |+------------------------------------------+ | v+------------------------------------------+| Real AWS Infrastructure || EC2 instance i-0a1b2c3d4e5f || running in ap-south-1 |+------------------------------------------+Terraform uses state to answer three questions on every plan:
- What resources did I create before? (state file)
- What does the configuration say they should look like now? (.tf files)
- What does the cloud say they actually look like right now? (API refresh)
The diff between those three things is what you see in terraform plan.
What Is Actually Inside the State File
The state file is a JSON document. Here is a simplified version of what one resource entry looks like:
{ "version": 4, "terraform_version": "1.6.3", "resources": [ { "mode": "managed", "type": "aws_instance", "name": "web", "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", "instances": [ { "schema_version": 1, "attributes": { "id": "i-0a1b2c3d4e5f6789", "ami": "ami-0f5ee92e2d63afc18", "instance_type": "t3.large", "public_ip": "13.126.45.67", "private_ip": "10.0.1.50", "tags": { "Environment": "prod", "ManagedBy": "terraform", "Name": "razorpay-api-prod-web-server" }, "vpc_security_group_ids": [ "sg-0a1b2c3d4e5f" ] } } ] } ]}Every attribute of every resource is stored here — IDs, IP addresses, ARNs, tags, and any sensitive values like database passwords. This is why the state file must be protected: it is a complete inventory of your infrastructure, including secrets.
Detailed Step-by-Step Practical Lab
Step 1 — Inspect Your State File
After running terraform apply, explore what Terraform recorded:
# List every resource and data source Terraform is trackingterraform state list # Example output:# data.aws_ami.amazon_linux# data.aws_caller_identity.current# aws_instance.web# aws_s3_bucket.app_assets# aws_s3_bucket_public_access_block.app_assets# aws_s3_bucket_server_side_encryption_configuration.app_assets# aws_s3_bucket_versioning.app_assets# aws_security_group.web # Show every stored attribute of a specific resourceterraform state show aws_instance.web # Output will show every attribute Terraform knows about:# resource "aws_instance" "web" {# ami = "ami-0f5ee92e2d63afc18"# arn = "arn:aws:ec2:ap-south-1::instance/i-0a1b2c3d4e5f"# id = "i-0a1b2c3d4e5f6789"# instance_type = "t3.large"# private_ip = "10.0.1.50"# public_ip = "13.126.45.67"# tags = {# "Environment" = "prod"# "ManagedBy" = "terraform"# "Name" = "razorpay-api-prod-web-server"# }# vpc_security_group_ids = [# "sg-0a1b2c3d4e5f",# ]# ...many more attributes...# }PLACEMENT PRO TIP**Tip:** `terraform state show` is your best debugging tool. When a plan shows a change you did not expect, run `terraform state show` to see exactly what Terraform currently believes is in the cloud — then compare it to what is really there.
Step 2 — Move a Resource in State (Rename Without Destroy)
You renamed a resource block in your .tf files — from aws_instance.web to aws_instance.app_server. Without updating state, Terraform will try to destroy the old one and create a new one. Use terraform state mv to rename it in state without touching the real infrastructure:
# Rename a resource in state — NO real infrastructure changesterraform state mv aws_instance.web aws_instance.app_server # Move: aws_instance.web => aws_instance.app_server# Successfully moved 1 object(s). # Verify the moveterraform state list | grep instance# aws_instance.app_server <- renamed in state # Now update your .tf file to match:# resource "aws_instance" "app_server" { <- was "web"# ...# } # Confirm plan shows no changesterraform plan# No changes. Your infrastructure matches the configuration.Step 3 — Remove a Resource From State (Without Destroying It)
Sometimes you want to stop managing a resource with Terraform — without deleting it. terraform state rm removes it from state, leaving the real resource untouched:
# Remove a resource from Terraform state — does NOT delete the real resourceterraform state rm aws_s3_bucket.legacy_logs # Removed aws_s3_bucket.legacy_logs# Successfully removed 1 resource instance(s). # The S3 bucket still exists in AWS — Terraform just no longer tracks it# Verify:terraform state list | grep legacy# (nothing — it is gone from state but still in AWS)Use cases for terraform state rm:
- Moving a resource to a different Terraform configuration or module
- Handing off a resource to another team's configuration
- Stopping management of a resource without deleting it
Step 4 — Pull and Push State Manually
When using a remote backend, you can pull the current state to inspect it locally and push a modified state back:
# Pull the current remote state to stdoutterraform state pull # Pull and save to a local file for inspectionterraform state pull > state-backup-$(date +%Y%m%d).json # Push a modified state file back to the remote backend# Only do this when explicitly fixing state corruption — dangerous operationterraform state push state-backup-20240115.jsonCOMMON MISTAKE / WARNING**Security:** Never edit the state file by hand with a text editor. The state file contains checksums and internal references. Manual editing almost always corrupts it. Use `terraform state mv`, `terraform state rm`, and `terraform import` instead — these are the safe surgery tools.
Step 5 — Understand What Happens When State is Lost
If the state file is deleted or becomes unavailable, Terraform loses all memory of what it created. The next terraform plan will show every resource as needing to be created fresh — because from Terraform's perspective, nothing exists.
# Simulate state loss (do NOT do this in production)mv terraform.tfstate terraform.tfstate.deleted terraform plan# aws_instance.web will be created <- Terraform thinks it does not exist# aws_s3_bucket.app_assets will be created <- Terraform thinks it does not exist# Plan: 6 to add, 0 to change, 0 to destroy.# The real resources still exist in AWS — Terraform just forgot about them # Recovery: import every resource back into stateterraform import aws_instance.web i-0a1b2c3d4e5f6789terraform import aws_s3_bucket.app_assets razorpay-api-dev-assets-123456789012# Repeat for every resource...This is why remote state with versioning is non-negotiable for any team. With S3 versioning enabled, any previous version of the state file is recoverable from the S3 console.
Step 6 — Detect and Understand Drift
Drift is what happens when someone changes infrastructure outside of Terraform. Every terraform plan detects it automatically:
# Scenario: someone increased the instance type in the AWS consoleterraform plan # ~ aws_instance.web will be updated in-place# ~ instance_type = "t3.large" -> "t3.medium"# <- Terraform will REVERT this to match your .tf file## This "change" is Terraform noticing the drift and planning to fix it # Three options:# 1. Apply normally — Terraform reverts the console change (enforces IaC)# 2. Update your .tf file to accept the new instance type (adopt the change)# 3. Apply -refresh-only — update state to record the change without revertingterraform apply -refresh-onlyProduction Best Practices and Common Pitfalls
Never edit the state file by hand. Use
terraform state mv,terraform state rm,terraform import, andterraform force-unlock. These commands are safe — manual edits are not.Always use remote state for teams. Local state is only acceptable for solo learning projects. The moment a second engineer exists, use S3 + DynamoDB. Set it up before the second engineer joins — migrating state later is extra work.
Enable S3 versioning on your state bucket. A versioned state bucket means any corrupt or accidentally modified state file is recoverable. Without versioning, a corrupted state file may be unrecoverable.
Restrict access to the state file. The state file contains plaintext sensitive values. Only the Terraform apply role and trusted administrators should have
s3:GetObjecton the state bucket. Developers who only need to read plan output do not need direct state access.Add
prevent_destroyto your state bucket. The state S3 bucket is the most critical resource in your infrastructure setup. A lifecycle rule withprevent_destroy = trueprevents it from being accidentally deleted by a Terraform run.Back up state before major operations. Before running
terraform state mv,terraform state rm, or any large refactor, pull the current state and save it:terraform state pull > state-backup-$(date +%Y%m%d).json.
Quick Reference and Troubleshooting Commands
| Command | What It Does |
|---|---|
terraform state list |
List all resources tracked in state |
terraform state list module.vpc |
List resources within a specific module |
terraform state show <addr> |
Show all stored attributes of one resource |
terraform state mv <src> <dst> |
Rename or move a resource in state — no infrastructure change |
terraform state rm <addr> |
Remove a resource from state — does NOT destroy real resource |
terraform state pull |
Download remote state to stdout |
terraform state push <file> |
Upload a local state file to the remote backend |
terraform plan -refresh-only |
Preview what drift exists without making changes |
terraform apply -refresh-only |
Update state to match reality — no infrastructure changes |
terraform force-unlock <id> |
Break a stuck state lock — confirm no apply is running first |
| Error | Root Cause | Fix |
|---|---|---|
No state file found |
State file deleted or wrong directory | Check directory, restore from backup or re-import resources |
Error: state snapshot was created by Terraform v1.x |
State version mismatch | Upgrade Terraform CLI to match the version that wrote the state |
Resource already exists in state |
Trying to import a resource that is already tracked | Check terraform state list first |
Error: Cycle |
Circular dependency between resources | Remove circular reference — use depends_on instead of direct references |
| Plan shows all resources as new | State file is empty or lost | Re-import resources or restore state from S3 versioned backup |
Error acquiring the state lock |
Another apply is running | Wait for it to complete, or terraform force-unlock if it is stuck |