Skip to content

Remote State with S3 and DynamoDB: Terraform State Backend Deep Dive

DodaTech 6 min read

In this tutorial, you'll learn about Remote State with S3 and DynamoDB: Terraform State Backend Deep Dive. We cover key concepts, practical examples, and best practices.

Remote state with S3 and DynamoDB is the most widely adopted Terraform state backend for production teams, combining durable encrypted storage with distributed locking for safe concurrent operations.

What You'll Learn

In this tutorial, you will learn how to build a production Terraform remote state backend using S3 for storage and DynamoDB for locking, configure state file isolation per environment and service, implement state versioning and encryption, and handle lock conflicts and state recovery.

Why It Matters

Local state breaks for teams. Without remote state, each team member must share a single state file, leading to corruption, conflicts, and lost changes. S3 plus DynamoDB gives you encrypted, versioned, locked state that scales from solo developers to hundred-person platform teams.

Real-World Use

DodaTech manages 30 state files across three environments and five services in a single S3 bucket. Durga Antivirus Pro's platform team runs concurrent applies across multiple services without conflicts, and S3 versioning enables point-in-time recovery of any previous state.

Architecture Overview

graph TD
    A[Terraform CLI / CI] --> B[S3 Backend]
    B --> C[terraform-state-bucket]
    C --> D[prod/network/terraform.tfstate]
    C --> E[prod/database/terraform.tfstate]
    C --> F[prod/compute/terraform.tfstate]
    C --> G[staging/network/terraform.tfstate]
    B --> H[DynamoDB Lock Table]
    H --> I[LockID: prod/network/terraform.tfstate-md5:abc123]
    H --> J[LockID: prod/database/terraform.tfstate-md5:def456]
    C --> K[S3 Versioning]
    K --> L[Version 1: 2026-06-23 10:00]
    K --> M[Version 2: 2026-06-23 14:30]
    K --> N[Version 3: 2026-06-23 16:45]
    style B fill:#4a90d9,color:#fff
    style C fill:#ff9900,color:#fff
    style H fill:#ff9900,color:#fff
    style K fill:#50c878,color:#fff

State Infrastructure Setup

S3 Bucket for State Storage

# state-infrastructure/state-bucket.tf
resource "aws_s3_bucket" "terraform_state" {
  bucket = "dodatech-terraform-state-${data.aws_caller_identity.current.account_id}"
}

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"
      kms_master_key_id = aws_kms_key.terraform_state.arn
    }
  }
}

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_s3_bucket_lifecycle_configuration" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  rule {
    id     = "expire-old-versions"
    status = "Enabled"

    noncurrent_version_expiration {
      noncurrent_days = 90
    }
  }
}
terraform apply -auto-approve

Expected output:

Apply complete! Resources: 5 added, 0 changed, 0 destroyed.

Outputs:
state_bucket_name = "dodatech-terraform-state-123456789012"
lock_table_name   = "terraform-state-locks"

DynamoDB Lock Table

# state-infrastructure/lock-table.tf
resource "aws_dynamodb_table" "terraform_locks" {
  name         = "terraform-state-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }

  point_in_time_recovery {
    enabled = true
  }

  server_side_encryption {
    enabled = true
  }

  tags = {
    Name        = "Terraform State Locks"
    Environment = "Global"
    ManagedBy   = "Terraform"
  }
}

KMS Key for State Encryption

# state-infrastructure/kms.tf
resource "aws_kms_key" "terraform_state" {
  description             = "KMS key for Terraform state encryption"
  deletion_window_in_days = 30
  enable_key_rotation     = true

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow]
        Principal = {
          AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
        }
        Action   = "kms:*"
        Resource = "*"
      }
    ]
  })
}

resource "aws_kms_alias" "terraform_state" {
  name          = "alias/terraform-state-key"
  target_key_id = aws_kms_key.terraform_state.key_id
}

Backend Configuration Patterns

Environment-Isolated State Files

# production/backend.hcl
bucket         = "dodatech-terraform-state-123456789012"
key            = "production/terraform.tfstate"
region         = "us-east-1"
encrypt        = true
kms_key_id     = "alias/terraform-state-key"
dynamodb_table = "terraform-state-locks"

# staging/backend.hcl
bucket         = "dodatech-terraform-state-123456789012"
key            = "staging/terraform.tfstate"
region         = "us-east-1"
encrypt        = true
kms_key_id     = "alias/terraform-state-key"
dynamodb_table = "terraform-state-locks"
# Initialize production with its backend
cd terraform/environments/production
terraform init -backend-config=backend.hcl -reconfigure

Service-Level State Isolation

# ----- network/backend.hcl -----
key = "production/network/terraform.tfstate"

# ----- database/backend.hcl -----
key = "production/database/terraform.tfstate"

# ----- compute/backend.hcl -----
key = "production/compute/terraform.tfstate"
# Terraform can read outputs from other state files
terraform init -backend-config=backend.hcl

State Data Source for Cross-State References

# database/main.tf
data "terraform_remote_state" "network" {
  backend = "s3"
  config = {
    bucket = "dodatech-terraform-state-123456789012"
    key    = "production/network/terraform.tfstate"
    region = "us-east-1"
  }
}

resource "aws_db_subnet_group" "main" {
  name       = "production"
  subnet_ids = data.terraform_remote_state.network.outputs.private_subnet_ids
}
terraform plan

Expected output:

data.terraform_remote_state.network: Reading...
data.terraform_remote_state.network: Read complete after 1s

No changes. Your infrastructure matches the configuration.

Locking Behavior and Troubleshooting

Testing State Locking

# Terminal 1: Start a long-running apply
terraform apply -auto-approve

# Terminal 2: Attempt concurrent apply
terraform apply -auto-approve

Expected output for Terminal 2:

Acquiring state lock. This may take a few moments...
Error: Error acquiring the state lock

Error message: ConditionalCheckFailedException: The conditional request failed
Lock Info:
  ID:        2f8a1b3c-4d5e-6f78-9abc-def012345678
  Path:      production/terraform.tfstate
  Operation: OperationTypeApply
  Who:       ci-runner-47@dodatech.com
  Version:   1.9.5
  Created:   2026-06-23 14:30:21.123 +0000 UTC
  Info:      ci-runner-47

Force Unlock Procedure

# Step 1: Verify no apply is running
aws dynamodb get-item \
  --table-name terraform-state-locks \
  --key '{"LockID": {"S": "production/terraform.tfstate-md5"}}'

# Step 2: Force unlock
terraform force-unlock 2f8a1b3c-4d5e-6f78-9abc-def012345678

Common Mistakes

1. Bucket Deletion Protection

Without a bucket policy denying deletion, someone can accidentally delete the S3 bucket and all state files.

2. Missing Versioning

Without S3 versioning, a corrupted state overwrite is unrecoverable. Always enable versioning on the state bucket.

3. Using One State for Everything

A monolithic state file creates a single point of failure. Split state by environment and service.

4. Exposing State Outputs

State outputs are readable by anyone with state access. Do not store secrets in Terraform outputs.

5. Ignoring Lock Table Region

The DynamoDB lock table must be in the same region as the S3 bucket. Cross-region locking fails silently.

Practice Questions

1. What problem does DynamoDB locking solve for Terraform teams? It prevents concurrent apply operations from corrupting the state file by acquiring a distributed lock.

2. How do you reference outputs from one state file in another Terraform configuration? Using the data "<a href="/devops/terraform/">Terraform</a>_remote_state" data source with the backend configuration pointing to the other state file.

3. Why should KMS encryption be used for the state bucket? KMS provides envelope encryption with key rotation, audit logging, and integration with AWS CloudTrail for compliance.

4. Challenge: Set up a complete state infrastructure with S3, DynamoDB, and KMS. Configure three environment-separated state files. Create a service that uses terraform_remote_state to reference VPC IDs from the network state.

Mini Project: Multi-Service State Infrastructure

Provision the complete state backend infrastructure: S3 bucket with versioning, KMS key with rotation, DynamoDB lock table with PITR. Configure separate state files for network, database, and compute in production. Wire the database service to read VPC and subnet IDs from the network state using data.terraform_remote_state.

State Management
Terraform on AWS

What's Next

Configure Terraform remote state with S3 and DynamoDB for your production infrastructure, then build complete AWS environments with Terraform. Study Docker containerization patterns for application deployment.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro