Remote State + Locking, Done Right
Put state in a versioned S3 bucket and let native locking (or DynamoDB) stop two applies from colliding.
The single biggest jump from hobby Terraform to team Terraform is getting state out of your laptop. Local state means one person owns the truth, backups are an accident, and a lost file is a lost afternoon.
The pattern that has never let me down
- Versioned S3 bucket — every write is a new object version, so a bad apply is one
aws s3apirestore away. - Encryption on — state holds secrets in plaintext, so
encrypt = trueis non-negotiable. - Locking — Terraform 1.10+ supports native S3 lockfiles with
use_lockfile = true. Older setups pair S3 with a DynamoDB table.
One backend per environment
The key is what separates dev from prod inside the same bucket. Never share a key between two environments — that is how a dev apply nukes prod.
Backend config cannot use variables. Keep it static, or pass it at init time with -backend-config=prod.s3.tfbackend.Example
terraform {
backend "s3" {
bucket = "acme-tf-state"
key = "prod/network/terraform.tfstate"
region = "us-east-1"
encrypt = true
# Terraform 1.10+: native S3 locking, no DynamoDB needed
use_lockfile = true
# Pre-1.10 alternative:
# dynamodb_table = "tf-locks"
}
}When to use it
- A team migrates from a shared local state file to S3 with DynamoDB locking after two engineers corrupted state by running simultaneous applies.
- A CI/CD system runs nightly applies across three environments knowing that DynamoDB locking will serialize them even if pipelines trigger at the same time.
- An SRE enables S3 versioning on the state bucket so any corrupted state can be recovered by restoring the previous version from the bucket history.
More examples
S3 bucket with versioning
Creates the S3 state bucket with versioning and AES256 encryption enabled so every state revision is recoverable and encrypted at rest.
resource "aws_s3_bucket" "tfstate" {
bucket = "my-org-terraform-state"
}
resource "aws_s3_bucket_versioning" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
versioning_configuration { status = "Enabled" }
}
resource "aws_s3_bucket_server_side_encryption_configuration" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
rule {
apply_server_side_encryption_by_default { sse_algorithm = "AES256" }
}
}DynamoDB lock table
Creates the DynamoDB table required for native S3 backend state locking, preventing concurrent apply corruption.
resource "aws_dynamodb_table" "tf_lock" {
name = "terraform-state-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
tags = { ManagedBy = "terraform" }
}Backend wired to both resources
Ties the S3 backend to the versioned bucket and DynamoDB lock table so state is remote, encrypted, and locking-protected.
terraform {
backend "s3" {
bucket = "my-org-terraform-state"
key = "prod/app/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}
Discussion