Skip to main content

Terraform State — Local vs Remote, Locking, and Why It Matters

· 6 min read
Goel Academy
DevOps & Cloud Learning Hub

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:

  1. Mapping — It maps resource blocks in your .tf files to real infrastructure IDs.
  2. Performance — Terraform queries state instead of calling cloud APIs for every resource during plan.
  3. 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 apply simultaneously 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:

StepAction
1terraform apply writes a lock entry to DynamoDB
2Terraform reads current state from S3
3Plan is calculated and changes are applied
4Updated state is written back to S3
5Lock 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.tfstate to 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

StrategyHowWhen
S3 versioningEnable versioning on the state bucketAlways — this is your undo button
Pre-apply backupterraform state pull > backup-$(date +%s).jsonBefore risky changes
State snapshotsTerraform Cloud/Enterprise auto-snapshotsIf using TFC/TFE
Cross-region replicationS3 CRR on the state bucketDisaster recovery

Common Pitfalls

  1. Running terraform init with a new backend without migrating — Terraform asks if you want to copy state. Always say yes unless you know what you are doing.
  2. Manually editing state — Use terraform state commands instead. Manual JSON edits can corrupt the state.
  3. Sharing state files via Slack/email — Use remote backends, not file sharing.
  4. Forgetting to set encrypt = true on 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.