Terraform State — Local vs Remote, Locking, and Why It Matters
You just ran terraform apply, everything deployed perfectly, and then your teammate ran it too — from their laptop. Suddenly, resources are duplicated, outputs are wrong, and nobody knows what is actually deployed. Welcome to the world of Terraform state mismanagement.
What Is Terraform State?
Every time you run terraform apply, Terraform records what it created in a file called terraform.tfstate. This JSON file is the single source of truth that maps your HCL configuration to real-world infrastructure.
Without state, Terraform would have no idea what it already created. It would try to create everything from scratch on every run.
{
"version": 4,
"terraform_version": "1.7.0",
"resources": [
{
"mode": "managed",
"type": "aws_instance",
"name": "web",
"provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
"instances": [
{
"attributes": {
"id": "i-0abc123def456789",
"ami": "ami-0c55b159cbfafe1f0",
"instance_type": "t3.micro",
"tags": { "Name": "web-server" }
}
}
]
}
]
}
Why State Is Critical
State serves three essential purposes:
- Mapping — It maps resource blocks in your
.tffiles to real infrastructure IDs. - Performance — Terraform queries state instead of calling cloud APIs for every resource during
plan. - Dependency tracking — It stores the dependency graph so Terraform knows the correct order for create/update/destroy.
Delete the state file and Terraform forgets everything. Your infrastructure still exists in the cloud, but Terraform thinks there is nothing deployed.
Local State — The Default (and the Problem)
By default, Terraform writes state to terraform.tfstate in your working directory.
# This is what happens with no backend config — local state
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
This works fine when you are learning or working solo. But the moment a second person touches the project, local state breaks down:
- No shared access — each person has their own copy
- No locking — two people can run
applysimultaneously and corrupt the state - Risk of data loss — a deleted laptop means deleted state
Remote State with S3 and DynamoDB
The industry standard for AWS teams is storing state in S3 with DynamoDB for locking.
First, create the backend infrastructure (this is a one-time setup you typically do manually or with a separate Terraform config):
# backend-setup/main.tf — Run this ONCE to create the backend resources
resource "aws_s3_bucket" "terraform_state" {
bucket = "my-company-terraform-state"
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_versioning" "enabled" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "default" {
bucket = aws_s3_bucket.terraform_state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
}
}
}
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-state-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
Now configure your actual project to use this remote backend:
terraform {
backend "s3" {
bucket = "my-company-terraform-state"
key = "prod/networking/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-state-locks"
encrypt = true
}
}
With this setup, every terraform apply acquires a lock in DynamoDB first. If someone else is already running an apply, you get an error instead of corruption.
State Locking in Action
When locking is enabled, here is what happens behind the scenes:
| Step | Action |
|---|---|
| 1 | terraform apply writes a lock entry to DynamoDB |
| 2 | Terraform reads current state from S3 |
| 3 | Plan is calculated and changes are applied |
| 4 | Updated state is written back to S3 |
| 5 | Lock is released from DynamoDB |
If a second user tries to run apply during steps 1-5, they see:
Error: Error acquiring the state lock
Lock Info:
ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Path: my-company-terraform-state/prod/networking/terraform.tfstate
Operation: OperationTypeApply
Who: alice@dev-laptop
Created: 2025-02-25 14:30:00 UTC
If a lock gets stuck (crashed process), you can force-unlock:
terraform force-unlock a1b2c3d4-e5f6-7890-abcd-ef1234567890
Sensitive Data in State
Here is something that catches people off guard: Terraform state stores secrets in plain text. Database passwords, API keys, private keys — all visible in the state file.
resource "aws_db_instance" "main" {
engine = "postgres"
engine_version = "15.4"
instance_class = "db.t3.micro"
username = "admin"
password = var.db_password # This ends up in state as plain text!
}
This is why you must:
- Never commit
terraform.tfstateto Git - Always encrypt state at rest (S3 SSE, Azure Storage encryption)
- Restrict access to the state bucket with IAM policies
Add this to your .gitignore immediately:
# Terraform state — NEVER commit these
*.tfstate
*.tfstate.backup
*.tfstate.*.backup
.terraform/
.terraform.lock.hcl
crash.log
override.tf
override.tf.json
*_override.tf
*_override.tf.json
*.tfvars
!example.tfvars
Essential State Commands
Terraform provides several commands to inspect and manipulate state directly.
# List all resources in state
terraform state list
# Output: aws_instance.web, aws_s3_bucket.assets, aws_vpc.main
# Show details of a specific resource
terraform state show aws_instance.web
# Rename a resource (refactoring without destroy/recreate)
terraform state mv aws_instance.web aws_instance.app_server
# Remove a resource from state (Terraform "forgets" it, but it still exists)
terraform state rm aws_instance.legacy
# Pull remote state to stdout (useful for debugging)
terraform state pull > state-backup.json
# Push a local state file to the remote backend
terraform state push state-backup.json
The state mv command is particularly valuable during refactoring. If you rename a resource block in your code, Terraform thinks the old one should be destroyed and a new one created. Using state mv tells Terraform it is the same resource with a new name.
State Backup Strategies
| Strategy | How | When |
|---|---|---|
| S3 versioning | Enable versioning on the state bucket | Always — this is your undo button |
| Pre-apply backup | terraform state pull > backup-$(date +%s).json | Before risky changes |
| State snapshots | Terraform Cloud/Enterprise auto-snapshots | If using TFC/TFE |
| Cross-region replication | S3 CRR on the state bucket | Disaster recovery |
Common Pitfalls
- Running
terraform initwith a new backend without migrating — Terraform asks if you want to copy state. Always say yes unless you know what you are doing. - Manually editing state — Use
terraform statecommands instead. Manual JSON edits can corrupt the state. - Sharing state files via Slack/email — Use remote backends, not file sharing.
- Forgetting to set
encrypt = trueon the S3 backend — state is stored unencrypted by default.
Wrapping Up
Terraform state is not optional — it is the backbone of how Terraform manages your infrastructure. Treat the state file like a database: back it up, encrypt it, lock it, and never let it get out of sync.
In the next post, we will explore Terraform variables and outputs — making your configurations reusable and maintainable instead of hardcoding everything.
