TerraformPilot

DevOps

Terraform Remote State - S3 Backend with DynamoDB Locking

Configure Terraform remote state with AWS S3 and DynamoDB locking. Complete setup with encryption, versioning, IAM permissions, and team access patterns.

LLuca Berton2 min read

Quick Answer

#

Store Terraform state in S3 with DynamoDB locking for team collaboration. Create an encrypted, versioned S3 bucket and a DynamoDB table, then configure the backend "s3" block. This prevents state corruption from concurrent runs.

Why Remote State?

#
FeatureLocal StateRemote State (S3)
Team collaboration❌ One person only✅ Shared access
State locking❌ No protection✅ DynamoDB locking
Encryption❌ Plaintext on disk✅ AES-256 at rest
Versioning❌ No history✅ S3 versioning
Backup❌ Manual✅ Automatic
CI/CD support❌ Needs file sharing✅ Native access

Step 1: Create the Backend Resources

#

Create these resources before configuring the backend (use a separate Terraform config or create manually):

# backend-setup/main.tf — run this first
provider "aws" {
  region = "us-east-1"
}
 
resource "aws_s3_bucket" "terraform_state" {
  bucket = "mycompany-terraform-state"
 
  lifecycle {
    prevent_destroy = true
  }
}
 
resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  versioning_configuration {
    status = "Enabled"
  }
}
 
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "aws:kms"
    }
  }
}
 
resource "aws_s3_bucket_public_access_block" "terraform_state" {
  bucket                  = aws_s3_bucket.terraform_state.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}
 
resource "aws_dynamodb_table" "terraform_locks" {
  name         = "terraform-state-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"
 
  attribute {
    name = "LockID"
    type = "S"
  }
}

Step 2: Configure the Backend

#
# main.tf — your actual project
terraform {
  backend "s3" {
    bucket         = "mycompany-terraform-state"
    key            = "prod/networking/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-state-locks"
    encrypt        = true
  }
}
# Initialize with the new backend
terraform init
 
# If migrating from local state:
terraform init -migrate-state

Step 3: State Key Organization

#

Organize state files by environment and component:

s3://mycompany-terraform-state/
├── prod/
│   ├── networking/terraform.tfstate
│   ├── eks/terraform.tfstate
│   ├── rds/terraform.tfstate
│   └── app/terraform.tfstate
├── staging/
│   ├── networking/terraform.tfstate
│   └── app/terraform.tfstate
└── shared/
    ├── iam/terraform.tfstate
    └── dns/terraform.tfstate

Step 4: Access State from Other Projects

#
# Read another project's state outputs
data "terraform_remote_state" "networking" {
  backend = "s3"
  config = {
    bucket = "mycompany-terraform-state"
    key    = "prod/networking/terraform.tfstate"
    region = "us-east-1"
  }
}
 
resource "aws_instance" "web" {
  subnet_id = data.terraform_remote_state.networking.outputs.private_subnet_id
}

IAM Policy for State Access

#
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
      "Resource": "arn:aws:s3:::mycompany-terraform-state/*"
    },
    {
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": "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"
    }
  ]
}

Force Unlock (Emergency)

#

If a lock is stuck (crashed apply, lost connection):

# Show the lock info
terraform force-unlock LOCK_ID
 
# Get the lock ID from the error message:
# "Error locking state: Error acquiring the state lock"
# "Lock Info: ID: xxxx-xxxx-xxxx"

Troubleshooting

#
IssueFix
"Error acquiring state lock"Another apply is running, or lock is stuck — force-unlock
"Access Denied"Check IAM permissions for S3 and DynamoDB
"No such bucket"Create the S3 bucket first
"Backend configuration changed"Run terraform init -reconfigure

Best Practices

#
  • Enable versioning — recover from corrupt state by restoring previous version
  • Enable encryption — state contains sensitive data (passwords, keys)
  • Block public access — state files should never be public
  • Use PAY_PER_REQUEST for DynamoDB — minimal cost, no capacity planning
  • Separate state per component — don't put everything in one state file
  • Use prevent_destroy on the S3 bucket — accidental deletion loses all state
#

Conclusion

#

S3 + DynamoDB is the gold standard for Terraform remote state on AWS. It gives you encryption, versioning, locking, and team collaboration out of the box. Set it up once, organize state files by environment and component, and never worry about state corruption from concurrent runs again.

#Terraform#AWS#State Management#DevOps#Best Practices

Share this article