Remote State + Locking, Done Right

Put state in a versioned S3 bucket and let native locking (or DynamoDB) stop two applies from colliding.

The single biggest jump from hobby Terraform to team Terraform is getting state out of your laptop. Local state means one person owns the truth, backups are an accident, and a lost file is a lost afternoon.

The pattern that has never let me down

  • Versioned S3 bucket — every write is a new object version, so a bad apply is one aws s3api restore away.
  • Encryption on — state holds secrets in plaintext, so encrypt = true is non-negotiable.
  • Locking — Terraform 1.10+ supports native S3 lockfiles with use_lockfile = true. Older setups pair S3 with a DynamoDB table.

One backend per environment

The key is what separates dev from prod inside the same bucket. Never share a key between two environments — that is how a dev apply nukes prod.

Backend config cannot use variables. Keep it static, or pass it at init time with -backend-config=prod.s3.tfbackend.

Example

Example Β· hcl
terraform {
  backend "s3" {
    bucket       = "acme-tf-state"
    key          = "prod/network/terraform.tfstate"
    region       = "us-east-1"
    encrypt      = true

    # Terraform 1.10+: native S3 locking, no DynamoDB needed
    use_lockfile = true

    # Pre-1.10 alternative:
    # dynamodb_table = "tf-locks"
  }
}

When to use it

  • A team migrates from a shared local state file to S3 with DynamoDB locking after two engineers corrupted state by running simultaneous applies.
  • A CI/CD system runs nightly applies across three environments knowing that DynamoDB locking will serialize them even if pipelines trigger at the same time.
  • An SRE enables S3 versioning on the state bucket so any corrupted state can be recovered by restoring the previous version from the bucket history.

More examples

S3 bucket with versioning

Creates the S3 state bucket with versioning and AES256 encryption enabled so every state revision is recoverable and encrypted at rest.

Example Β· hcl
resource "aws_s3_bucket" "tfstate" {
  bucket = "my-org-terraform-state"
}

resource "aws_s3_bucket_versioning" "tfstate" {
  bucket = aws_s3_bucket.tfstate.id
  versioning_configuration { status = "Enabled" }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "tfstate" {
  bucket = aws_s3_bucket.tfstate.id
  rule {
    apply_server_side_encryption_by_default { sse_algorithm = "AES256" }
  }
}

DynamoDB lock table

Creates the DynamoDB table required for native S3 backend state locking, preventing concurrent apply corruption.

Example Β· hcl
resource "aws_dynamodb_table" "tf_lock" {
  name         = "terraform-state-lock"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

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

  tags = { ManagedBy = "terraform" }
}

Backend wired to both resources

Ties the S3 backend to the versioned bucket and DynamoDB lock table so state is remote, encrypted, and locking-protected.

Example Β· hcl
terraform {
  backend "s3" {
    bucket         = "my-org-terraform-state"
    key            = "prod/app/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-state-lock"
  }
}

Discussion

  • Be the first to comment on this lesson.
Remote State + Locking, Done Right β€” Terraform | SoundsCode