Showing posts with label Terraform. Show all posts
Showing posts with label Terraform. Show all posts

Tuesday, October 27, 2026

Terraform Remote State on S3 with Locking: A Practical Setup

The fastest way to break Terraform for a whole team is to keep terraform.tfstate on someone's laptop. Two people apply at the same time, one state file wins, and the other half of your infrastructure is now “unknown” to Terraform. A remote backend with locking fixes this.

1. The state bucket

Create the bucket once (by hand or in a tiny bootstrap Terraform project with local state):

  • Versioning on – every state write is recoverable.
  • Block all public access and default encryption (SSE-S3 or SSE-KMS).
  • A bucket policy that only allows your CI role and a small admin group.

2. The backend block

terraform {
  required_version = ">= 1.10"

  backend "s3" {
    bucket       = "shailapps-tfstate"
    key          = "envs/dev/network/terraform.tfstate"
    region       = "us-east-1"
    encrypt      = true
    use_lockfile = true
  }
}

use_lockfile = true uses S3's native conditional writes to create a .tflock object next to the state, available from Terraform 1.10. On older versions you need a DynamoDB table with a LockID string key and dynamodb_table = "..." in the backend instead; that option is now deprecated, so plan the upgrade.

3. Move existing state

terraform init -migrate-state

Terraform copies the local state into S3. Check the object exists, then delete the local file so nobody applies from it by accident.

4. Split state by blast radius

envs/
  dev/
    network/   -> key = envs/dev/network/terraform.tfstate
    eks/       -> key = envs/dev/eks/terraform.tfstate
    apps/
  prod/
    network/
    eks/

One giant state means every plan touches everything and a mistake can destroy unrelated resources. Separate states per environment and per layer keep plans fast and failures small. Read outputs across layers with the terraform_remote_state data source or, better, by looking resources up with data sources.

5. Running it from CI

  • Authenticate with an IAM role (OIDC from your CI system), never long-lived keys in variables.
  • Run terraform plan -out=tfplan on the pull request and terraform apply tfplan only after approval, so what you reviewed is exactly what gets applied.
  • If a run dies and leaves a lock behind, use terraform force-unlock LOCK_ID only after you are sure no other apply is running.

With this in place, concurrent applies fail fast with a clear lock error instead of silently corrupting state.