TerraformPilot

Cloud Computing

Terraform Remote Backends with AWS S3

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

LLuca Berton2 min read

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.

#Terraform#HashiCorp#AWS#DevOps#Infrastructure as Code

Share this article