Skip to main content
Terraform Remote Backends with AWS S3

Terraform Remote Backends with AWS S3

Key Takeaway

Configure Terraform S3 backend for remote state storage with DynamoDB state locking. Complete setup guide with IAM permissions, encryption, and versioning.

Table of Contents

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

  1. terraform plan or apply starts
  2. Terraform writes a lock record to DynamoDB
  3. If another process tries to run, it sees the lock and waits/fails
  4. 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 environmentprod/, staging/, dev/ prefixes
  • prevent_destroy on the S3 bucket — accidental deletion is catastrophic
  • Don’t commit state to git — add *.tfstate to .gitignore

Troubleshooting

ErrorFix
“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

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.

🚀

Level Up Your Terraform Skills

Hands-on courses, books, and resources from Luca Berton

Luca Berton
Written by

Luca Berton

DevOps Engineer, AWS Partner, Terraform expert, and author. Creator of Ansible Pilot, Terraform Pilot, and CopyPasteLearn.