State #
State is the most crucial — and most often misunderstood — concept in Terraform. Everything that goes well or badly in a Terraform workflow comes down to state. Without a proper understanding of state, you’ll struggle to debug problems, fear running apply, and not know where to start when something goes wrong. This section will break down what state actually is, how Terraform uses it, and how to manage it safely.
What Is State #
State is a JSON file storing the mapping between the resources you define in your Terraform configuration and the resources that actually exist in the cloud provider. This file is called terraform.tfstate and by default is stored in the same directory as your configuration.
// terraform.tfstate — example snippet of its contents
{
"version": 4,
"terraform_version": "1.6.0",
"serial": 5,
"resources": [
{
"type": "aws_instance",
"name": "web",
"provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
"mode": "managed",
"instances": [
{
"attributes": {
"id": "i-0abcdef1234567890",
"ami": "ami-0abcdef1234567890",
"instance_type": "t3.micro",
"public_ip": "54.123.45.67",
"private_ip": "10.0.1.42",
"tags": {
"Name": "web-server"
}
}
}
]
}
]
}
State stores all resource attributes — including attributes only available after the resource is created (like public_ip and id generated by AWS). This means state isn’t just a record — it’s a complete representation of what Terraform manages.
Information Stored in State #
| Information | Example | Function |
|---|---|---|
| Resource identity | aws_instance.web → i-0abcdef1234567890 | Mapping config ↔ real resource |
| Full attributes | public_ip, arn, id | Cross-resource references |
| Dependency | aws_subnet.public depends on aws_vpc.main | Dependency graph |
| Provider config | hashicorp/aws version 5.31.0 | Determines the managing provider |
| Serial number | 5 | Optimistic locking for concurrency |
Why State Is Needed #
Without state, Terraform has no way to know which resources already exist, what their current configuration is, and what needs to change. State solves three fundamental problems.
flowchart TD
A["Three problems without state"] --> B["1. Know what resources exist"]
A --> C["2. Know what attributes they have"]
A --> D["3. Fast plan performance"]
B --> E["State stores the mapping\nbetween config and real resources\n→ Terraform knows which\nresources to manage"]
C --> F["State stores the attributes\nof existing resources\n→ Cross-resource references\ncan work"]
D --> G["State caches the\nlatest attributes\n→ No need to query APIs\nfor every plan"]
style A fill:#ffebee,stroke:#c62828
style E fill:#e8f5e9,stroke:#2e7d32
style F fill:#e8f5e9,stroke:#2e7d32
style G fill:#e8f5e9,stroke:#2e7d32Without state — Terraform would have to query all resources from the cloud provider APIs every time it runs plan. For infrastructure with hundreds of resources, this could take dozens of minutes. And Terraform still wouldn’t know which resources were intentionally created by Terraform versus manually.
With state — Terraform knows exactly: aws_instance.web exists with ID i-0abcdef1234567890, instance_type t3.micro, public_ip 54.123.45.67. Plans can be calculated in seconds because it only needs to compare state with configuration.
# No state → Terraform can't answer:
# "Does aws_instance.web already exist?"
# → Must query AWS for all resources
# → Slow for large infrastructures
# → Doesn't know which attributes you set vs which AWS set
# With state → Terraform knows:
# "aws_instance.web exists with ID i-0abcdef1234567890"
# → Fast plans
# → Knows exactly what changed
# → Can calculate the dependency graph accurately
Local State vs Remote State #
By default, state is stored locally in terraform.tfstate. This is fine for experiments and learning, but it’s not safe and doesn’t work for team collaboration.
Local State #
# No configuration needed — this is the default
# The terraform.tfstate file lives in the same directory as the .tf files
# PROBLEMS:
# 1. The file can contain secrets (passwords, API keys) → must never be committed to Git
# 2. Can't be accessed by other team members
# 3. No locking — two people applying at once = corrupted state
# 4. No backup — a dead hard drive = lost state
Remote State #
Remote state stores the state file in a central backend accessible to all team members. All team members read and write the same state.
# Example: AWS S3 as the remote backend
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "production/terraform.tfstate"
region = "ap-southeast-1"
encrypt = true # Encryption at rest
dynamodb_table = "terraform-lock" # For state locking
}
}
# Example: Google Cloud Storage
terraform {
backend "gcs" {
bucket = "my-terraform-state"
prefix = "production"
}
}
# Example: Terraform Cloud (managed backend)
terraform {
cloud {
organization = "my-company"
workspaces {
name = "production"
}
}
}
flowchart TD
A["Choose a backend"] --> B{"Environment?"}
B -->|"Learning / experimenting"| C["Local state\n(default)"]
B -->|"Small team"| D["S3 + DynamoDB\nor GCS"]
B -->|"Large team / enterprise"| E["Terraform Cloud\nor Spacelift"]
C --> F["No config needed\nmanual backups"]
D --> G["Versioning + locking\nrequires maintenance"]
E --> H["Managed + RBAC + UI\npaid"]
style C fill:#fff3e0,stroke:#e65100
style D fill:#e3f2fd,stroke:#1565c0
style E fill:#e8f5e9,stroke:#2e7d32Backend Comparison #
| Backend | Locking | Versioning | Encryption | Best for |
|---|---|---|---|---|
| Local | No | No | No | Learning, experiments |
| S3 | DynamoDB | S3 versioning | AES-256 | AWS production |
| GCS | Built-in | GCS versioning | Built-in | GCP production |
| Terraform Cloud | Built-in | Built-in | Built-in | Enterprise, managed |
| Consul | Built-in | KV store | TLS | On-premise |
State Locking #
State locking prevents two Terraform processes from modifying the state at the same time. Without locking, two people running terraform apply simultaneously can produce corrupted state.
sequenceDiagram
participant A as Developer A
participant S3 as S3 Backend
participant B as Developer B
A->>S3: terraform apply → acquire lock
S3-->>A: Lock acquired ✓
B->>S3: terraform apply → try acquire lock
S3-->>B: Lock denied ✗ (already locked)
Note over B: Error: state is locked
A->>S3: Apply complete → release lock
S3-->>A: Lock released ✓
B->>S3: terraform apply → acquire lock
S3-->>B: Lock acquired ✓ (now it can)# Normal operation — locking happens automatically
$ terraform apply
# Lock acquired automatically...
# ... apply ...
# Lock released automatically.
# Force unlock — ONLY if the lock wasn't released due to a crash
$ terraform force-unlock LOCK_ID
# Get the LOCK_ID from the error message
# USE WITH CAUTION — make sure no other process is running
Never run terraform force-unlock unless you’re sure no other Terraform process is running. Force unlocking an active lock can corrupt state if two processes write state simultaneously.State Is Not for Manual Editing #
State is a file managed entirely by Terraform. Editing it manually — even for “small fixes” — almost always ends badly because state has a specific internal format, checksums that must stay consistent, and a serial number that must remain sequential.
# ANTI-PATTERN: Editing terraform.tfstate directly with a text editor
# This can cause:
# - Corrupted state that can't be recovered
# - Orphaned resources (exist in cloud, missing from state)
# - Broken dependencies
# - Serial number conflicts
# CORRECT: Use Terraform commands to manipulate state
terraform state list # View all resources in state
terraform state show aws_instance.web # View details of one resource
terraform state rm aws_instance.web # Remove resource from state (without destroying infra)
terraform state mv \ # Rename / move a resource in state
aws_instance.web \
aws_instance.app_server
terraform state pull # Download state from the remote backend
terraform state push backup.tfstate # Upload state to the remote backend
Why Use terraform state Commands
#
Every terraform state command ensures:
- The serial number is incremented correctly
- The checksum is updated
- The dependency graph stays valid
- Changes are propagated to the remote backend (if any)
Manual editing violates all of these validations — similar to editing a database directly without going through an ORM.
The Risk of Losing State #
Losing the state file is one of the most dangerous scenarios in Terraform. Without state, Terraform loses the map connecting the configuration to real resources.
flowchart TD
A["State lost!"] --> B["terraform plan"]
B --> C["Terraform doesn't know\nwhich resources exist"]
C --> D["Plan shows:\n+47 resources to create"]
D --> E{"terraform apply?"}
E -->|"Yes"| F["ERROR: resource already exists\nin the cloud, can't duplicate"]
E -->|"No"| G["Must re-import\nall resources manually"]
F --> H["New state contains\nthe failed resources"]
G --> I["terraform import aws_instance.web i-0abc...\n(for EACH resource)"]
style A fill:#ffebee,stroke:#c62828
style F fill:#ffebee,stroke:#c62828
style G fill:#fff3e0,stroke:#e65100
style I fill:#fff3e0,stroke:#e65100Prevention #
# 1. Use a remote backend with versioning
# S3 versioning can be restored to a previous version
# 2. Back up state before risky operations
$ terraform state pull > backup-$(date +%Y%m%d).tfstate
# 3. Enable S3 versioning
# (make sure the bucket policy allows restore)
# 4. Monitor the state file — don't let it get deleted
# 5. Never delete state unless you know exactly what the consequences are
Recovering from Lost State #
# Option 1: Restore from S3 versioning
$ aws s3api list-object-versions \
--bucket my-terraform-state \
--prefix production/terraform.tfstate
# Then restore the last valid version
# Option 2: Re-import all resources
# (only if no backup is available)
$ terraform import aws_vpc.main vpc-0abc1234
$ terraform import aws_subnet.public subnet-0def5678
$ terraform import aws_instance.web i-0abcdef1234567890
# ... for every resource — extremely tedious
# Option 3: Terraform Cloud (if you use it)
# Terraform Cloud has built-in state history and rollback
terraform state Subcommands
#
Here’s the complete list of terraform state commands you need to know.
| Command | Function | When to use |
|---|---|---|
state list | List resources in state | Audit, debugging |
state show <addr> | View details of one resource | Debugging, checking values |
state mv <src> <dst> | Move/rename a resource in state | Refactoring, moving modules |
state rm <addr> | Remove a resource from state | Unmanaging a resource without destroying it |
state pull | Download state from the remote backend | Backup, debugging |
state push <file> | Upload state to the remote backend | Restoring from backup |
import <addr> <id> | Add an existing resource to state | Managing resources that already exist |
# Common usage examples:
# View all resources managed by Terraform
$ terraform state list
aws_vpc.main
aws_subnet.public[0]
aws_subnet.public[1]
aws_instance.web
aws_s3_bucket.data
# View details of one resource
$ terraform state show aws_instance.web
# resource "aws_instance" "web" {
# ami = "ami-0abcdef1234567890"
# instance_type = "t3.micro"
# public_ip = "54.123.45.67"
# ...
# }
# Remove a resource from state without removing it from the cloud
# Useful when you want to "stop managing" a specific resource
$ terraform state rm aws_instance.legacy_server
# The resource still exists in AWS, but Terraform no longer manages it
Summary #
- State is the map between configuration and reality — Terraform compares state with configuration to determine what needs to change.
- State stores all resource attributes including those generated by the cloud provider — this is what makes cross-resource references work.
- Without state, Terraform is blind — it can’t know what resources exist without querying every provider API, which is very slow.
- Local state is fine for learning — but remote state (S3, GCS, Terraform Cloud) is a must for teams and production.
- State locking prevents two processes from writing state simultaneously — make sure your backend supports locking (DynamoDB for S3, built-in for GCS/Terraform Cloud).
- Never edit state manually — use the
terraform statesubcommands for all state manipulation so checksums and serial numbers stay valid.- Losing state is an emergency — protect it with a versioned remote backend, regular backups, and never delete state without knowing the consequences.