Teams and CI must share one authoritative state, and concurrent applies corrupt it.
A backend decides where state lives. The default local backend writes a file next to your code, fine for experiments only. For anything shared, use a remote backend:
s3(AWS): state in a bucket; locking with a lock file in the bucket viause_lockfile = true(the older DynamoDB table locking is deprecated).gcs(Google Cloud) andazurerm(Azure Storage): built-in locking.remote/cloudblock for HCP Terraform or Terraform Enterprise, which also run plans for you.- Others:
pg,consul,kubernetes,http.
Locking: an operation that may write state takes a lock first. If someone else holds it, you get "Error acquiring the state lock" with the holder's details. Wait; only use terraform force-unlock <ID> when you are sure the holder crashed.
Good practice for the storage itself: enable bucket versioning (to recover from a bad write), encryption, tight access control and block public access. Create the state bucket once, outside the configuration that uses it (a small bootstrap configuration or by hand).
Backend blocks can't use variables. Pass environment-specific settings with partial configuration: terraform init -backend-config=prod.s3.tfbackend.
Changing backends: update the block and run terraform init -migrate-state, which copies the state across.
Example
terraform {
backend "s3" {
bucket = "acme-terraform-state"
key = "network/prod/terraform.tfstate"
region = "eu-west-1"
encrypt = true
use_lockfile = true # S3-native locking (Terraform 1.10+)
}
}
# terraform init -migrate-state # move existing local state into S3
# A second concurrent apply:
# Error: Error acquiring the state lock
# Lock Info:
# ID: 6f1c9a7e-...
# Who: ci@runner-42
# Operation: OperationTypeApplyRemember
- Remote backend + locking for any shared state
- S3 uses use_lockfile; GCS/Azure lock natively
- Version and encrypt the state bucket; force-unlock only when sure
Watch out for
- Using force-unlock while another apply is genuinely still running.