Quick Answer
Store Terraform state in an encrypted, versioned S3 bucket with DynamoDB locking. This enables team collaboration, prevents state corruption, and provides automatic backups via S3 versioning.
Why Use Amazon S3 as a Terraform Backend?
Terraform stores infrastructure state locally by default (terraform.tfstate). This breaks when:
- Multiple people run Terraform — who has the latest state?
- CI/CD pipelines need access — where do they read state?
- Accidents happen — local files get deleted, corrupted, or committed to git
S3 solves all three: it’s shared, versioned, and encrypted.
Prerequisites
Create the S3 bucket and DynamoDB table before configuring the backend:
# bootstrap/main.tf — run once manually
provider "aws" {
region = "us-east-1"
}
resource "aws_s3_bucket" "state" {
bucket = "mycompany-terraform-state"
lifecycle { prevent_destroy = true }
}
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 = "aws:kms"
}
}
}
resource "aws_s3_bucket_public_access_block" "state" {
bucket = aws_s3_bucket.state.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_dynamodb_table" "locks" {
name = "terraform-state-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
Backend Configuration
terraform {
backend "s3" {
bucket = "mycompany-terraform-state"
key = "prod/networking/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-state-locks"
encrypt = true
}
}
# Initialize or migrate
terraform init
terraform init -migrate-state # When switching from local to S3
How State Locking Works
terraform planorapplystarts- Terraform writes a lock record to DynamoDB
- If another process tries to run, it sees the lock and waits/fails
- When the operation completes, the lock is released
# If lock gets stuck (crashed process):
terraform force-unlock LOCK_ID
IAM Permissions Required
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::mycompany-terraform-state",
"arn:aws:s3:::mycompany-terraform-state/*"
]
},
{
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:*:table/terraform-state-locks"
}
]
}
Reading Remote State from Other Projects
data "terraform_remote_state" "networking" {
backend = "s3"
config = {
bucket = "mycompany-terraform-state"
key = "prod/networking/terraform.tfstate"
region = "us-east-1"
}
}
# Use outputs from the networking state
resource "aws_instance" "web" {
subnet_id = data.terraform_remote_state.networking.outputs.private_subnet_id
}
Best Practices
- Enable versioning — restore state if it gets corrupted
- Enable encryption — state contains secrets (passwords, keys, ARNs)
- Block public access — state should never be public
- Use PAY_PER_REQUEST for DynamoDB — pennies per month
- Organize by environment —
prod/,staging/,dev/prefixes prevent_destroyon the S3 bucket — accidental deletion is catastrophic- Don’t commit state to git — add
*.tfstateto.gitignore
Troubleshooting
| Error | Fix |
|---|---|
| “Error acquiring state lock” | Wait for other apply, or force-unlock |
| “Access Denied” | Check IAM policy for S3 + DynamoDB |
| “NoSuchBucket” | Create the S3 bucket first |
| “Backend config changed” | Run terraform init -reconfigure |
| “State snapshot newer version” | Upgrade Terraform to match state version |
Related Articles
- Terraform Backend Configuration Guide - S3, Azure, GCS
- Managing State and State Locking
- Terraform Glossary
Conclusion
S3 + DynamoDB is the standard Terraform backend for AWS teams. It takes 10 minutes to set up and prevents every state-related disaster: corruption, conflicts, lost files, and accidental public exposure. Set it up on day one of any project.




