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:
A B l o i b c : e : t t e e r r r r a a f f o o r r m m a a p p p p l l y y # # C C r r e e a a t t e e s s i i n n s s t t a a n n c c e e i i - - a d b e c f ( d o e s n ' t k n o w a b o u t i - a b c ! )
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 └ # t ├ │ ├ │ ├ │ e ─ e ─ ─ ─ B r ─ G r ─ ─ ─ A r O r D a t O a n └ d └ a └ : f e D f e ─ a ─ p ─ o r : o t ─ t ─ p ─ E r r r w a l v m a S m b b b i b e / f p r a a a c a r o l k c s c a c y r i / k e k t k t m t e / e i e h . n n o n i t b d d n d n f y . . / . g s t t t t c f f f i a o n t m e p # # # o o n n k k k e # e e e e n y y y s V t t P / = = = a C e t , n " " " e v p p p E i r r r C r o o o 2 o d d d , n / / / m n d a R e e a p D n t t p S t w a l , o b i r a c L k s a a / e t m t / i b e t o d r e n a r r / , a r t f a e I o f r A r o r M m r a . . m f . t . o . f t r s f m t s . a t t t a f e t s " e t " a t e " 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:
s ├ └ 3 ─ ─ : ─ ─ / / e e m n n y v v - : : t / / e s p r t r r a o a g d f i u o n c r g t m / i - t o s e n t r / a r t t a e e f r / o r r a m f . o t r f m s . t t a f t s e t a t e
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:
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.