Terraform state is the source of truth for your infrastructure. Mess it up and you’ll be manually reconciling resources at 2 AM. Here’s how to manage state properly from day one.

What Is State?

Terraform state maps your configuration to real resources:

1
2
3
4
5
# main.tf
resource "aws_instance" "web" {
  ami           = "ami-12345"
  instance_type = "t3.micro"
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// terraform.tfstate (simplified)
{
  "resources": [{
    "type": "aws_instance",
    "name": "web",
    "instances": [{
      "attributes": {
        "id": "i-0abc123def456",
        "ami": "ami-12345",
        "instance_type": "t3.micro"
      }
    }]
  }]
}

Without state, Terraform doesn’t know aws_instance.web corresponds to i-0abc123def456. It would try to create a new instance every time.

Never Use Local State in Teams

The default is local state in terraform.tfstate. This breaks immediately with multiple people:

ABloibc:e:tteerrrraaffoorrmmaappppllyy##CCrreeaatteessiinnssttaanncceeii--adbecf(doesn'tknowabouti-abc!)

Both have different state files. Neither knows about the other’s resources. Chaos.

Remote State: S3 + DynamoDB

The standard for AWS environments:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# backend.tf
terraform {
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "prod/network/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}

Set up the backend resources first:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# bootstrap/main.tf - Run this once manually
resource "aws_s3_bucket" "state" {
  bucket = "my-terraform-state"
}

resource "aws_s3_bucket_versioning" "state" {
  bucket = aws_s3_bucket.state.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "state" {
  bucket = aws_s3_bucket.state.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

resource "aws_dynamodb_table" "locks" {
  name         = "terraform-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"
  
  attribute {
    name = "LockID"
    type = "S"
  }
}

Why DynamoDB? State locking. Without it, two terraform apply commands can corrupt state.

State File Organization

Don’t put everything in one state file:

#t#teeBrGrArOrDatOanda:feDfeapor:ottpErrrwalvmaSmbbbibe/fpraaacarolkcscacyri/kektktmte/eieh.nnonitbddndnfy../.gsttttcfffiaontmep###oonnkkke#eeeenyyysVttP/===aCet,n"""evpppEirrrCrooo2oddd,n///mndaReeapDnttpStwal,obiracLksaa/etmt/ibetodrenarr/,artfaeIofrArorMmra..mf.t.o.ftrsfmts.atttafets"et"ate"

Benefits:

  • Faster plans (less to check)
  • Smaller blast radius
  • Independent team ownership
  • Easier troubleshooting

Data Sources for Cross-State References

Components need to reference each other:

1
2
3
4
5
6
7
8
# network/outputs.tf
output "vpc_id" {
  value = aws_vpc.main.id
}

output "private_subnet_ids" {
  value = aws_subnet.private[*].id
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# application/main.tf
data "terraform_remote_state" "network" {
  backend = "s3"
  config = {
    bucket = "my-terraform-state"
    key    = "prod/network/terraform.tfstate"
    region = "us-east-1"
  }
}

resource "aws_instance" "app" {
  subnet_id = data.terraform_remote_state.network.outputs.private_subnet_ids[0]
  # ...
}

Workspaces for Environments

Same config, different environments:

1
2
3
4
5
6
7
8
terraform workspace new staging
terraform workspace new production

terraform workspace select staging
terraform apply  # Creates staging resources

terraform workspace select production  
terraform apply  # Creates production resources

State files are stored separately:

s3://eemnnyvv-::t//esprtrraoagdfiuoncrgtm/i-tosentr/arttaeefr/orramf.otrfms.ttaftsetate

Use workspace in your config:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
locals {
  environment = terraform.workspace
  
  instance_type = {
    staging    = "t3.small"
    production = "t3.large"
  }
}

resource "aws_instance" "app" {
  instance_type = local.instance_type[local.environment]
  # ...
}

State Operations

Viewing State

1
2
3
4
5
6
7
8
# List all resources
terraform state list

# Show specific resource
terraform state show aws_instance.web

# Pull remote state locally (for inspection)
terraform state pull > state.json

Moving Resources

Refactoring without destroying:

1
2
3
4
5
# Rename a resource
terraform state mv aws_instance.web aws_instance.application

# Move to a module
terraform state mv aws_instance.web module.app.aws_instance.web

Removing from State

Resource exists but shouldn’t be managed:

1
2
3
4
# Remove from state (doesn't destroy the resource)
terraform state rm aws_instance.legacy

# Next apply won't touch it

Importing Existing Resources

Bring unmanaged resources under Terraform:

1
2
3
4
# Add to config first
# resource "aws_instance" "imported" { ... }

terraform import aws_instance.imported i-0abc123def456

State Recovery

When things go wrong:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# S3 versioning lets you recover previous state
aws s3api list-object-versions \
  --bucket my-terraform-state \
  --prefix prod/network/terraform.tfstate

# Restore previous version
aws s3api get-object \
  --bucket my-terraform-state \
  --key prod/network/terraform.tfstate \
  --version-id "abc123" \
  recovered.tfstate

# Push recovered state
terraform state push recovered.tfstate

Security Considerations

State contains sensitive data (passwords, keys):

1
2
3
4
# This password ends up in state!
resource "aws_db_instance" "main" {
  password = var.db_password
}

Mitigations:

  • Enable S3 encryption (AES256 or KMS)
  • Restrict bucket access via IAM
  • Enable S3 access logging
  • Use sensitive = true on outputs
  • Consider Vault or Secrets Manager for secrets

Quick Checklist

Before going to production:

  • Remote backend configured (S3 + DynamoDB)
  • State bucket has versioning enabled
  • State bucket has encryption enabled
  • DynamoDB table for locking
  • State split by component/environment
  • IAM policies restrict state access
  • S3 access logging enabled
  • Documented recovery procedures

Terraform state is infrastructure for your infrastructure. Treat it with the same care you’d give a production database—because that’s essentially what it is.