Centralize Terraform State in S3 with DynamoDB Locking for Team Safety
Move Terraform state to a versioned S3 bucket and protect concurrent writes with a DynamoDB lock table. This guide covers backend configuration, init, verification checks, and recovery options.
20 Aug 2026, 12:10 UTC

The problem with local state in a team
When multiple engineers run terraform plan and terraform apply against the same infrastructure, a local terraform.tfstate file on each laptop leads to overwrites and lost resource tracking. The useful takeaway is to move state to a single S3 object and protect concurrent writes with a DynamoDB lock table. That gives one source of truth and prevents two applies from corrupting state.
Desired outcome
A centralized state file stored in an S3 bucket with server-side encryption, addressed by a stable key path per workspace or environment. A DynamoDB table provides advisory locking so Terraform acquires a lock before reading or writing state and releases it afterwards. Team members can run plan and apply without manual coordination.
Prerequisites
- AWS account with permission to create and read an S3 bucket and a DynamoDB table.
- S3 bucket with versioning enabled. Versioning allows recovery from accidental overwrites.
- DynamoDB table named for locking with a single partition key LockID of type String. Terraform expects this exact key name for its locking protocol.
- Credentials available to the Terraform execution environment, e.g., AWS CLI profile or assumed role, with minimum permissions: s3:GetObject, s3:PutObject, s3:DeleteObject, s3:ListBucket on the bucket; dynamodb:GetItem, dynamodb:PutItem, dynamodb:DeleteItem on the lock table.
- Terraform 1.x installed locally. Backend block syntax is stable in 1.x; earlier versions used different configuration patterns.
Configure the S3 backend with DynamoDB locking
Add a backend block to the root module configuration. The backend is configured separately from provider blocks and is read during terraform init.
terraform {
backend "s3" {
bucket = "my-tf-state-bucket"
key = "prod/network/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}Replace bucket with your state bucket name, key with the desired path inside the bucket, region with the bucket region, and dynamodb_table with the lock table name. encrypt=true enables SSE-S3. Restrict bucket access with a bucket policy and consider a KMS CMK for additional control.
Create the DynamoDB table with LockID as the partition key. Do not add additional attributes; Terraform writes specific items for locking.
Initialize and migrate state
Run initialization from the directory containing the configuration. Required permissions are read/write on the bucket and lock table.
terraform initIf this is the first remote backend use, Terraform prompts to copy existing local state to the remote backend. Confirm the migration. For an existing remote state, init configures the CLI to read from the specified S3 key.
Risks: incorrect bucket or key values create a new empty state and can lead to resource recreation. Verify the key path before initializing.
Verify locking and state location
- Run a plan in a terminal with AWS credentials that match the IAM policy.
- Check the S3 bucket console for an object at the configured key path. The object is terraform.tfstate.
- Open the DynamoDB console and inspect the Items tab for the lock table during plan or apply. A lock item appears while the operation is in progress and disappears after completion.
- Simulate concurrency by starting terraform apply in one terminal and terraform plan in another. The second command should wait for the lock to be released rather than proceeding immediately.
State file contains sensitive values. Ensure bucket access is limited to the Terraform execution role and enable encryption.
Recovery options
Stuck lock
If a process terminates without releasing the lock, Terraform reports that the state is locked. Identify the lock ID from the error output and run force-unlock from the same working directory.
terraform force-unlock <LOCK_ID>Use force-unlock only after confirming the original operation is not running. Required permission is dynamodb:DeleteItem on the lock table.
Corrupted or overwritten state
With S3 versioning enabled, open the bucket version history for the state key and restore a prior version. After restoring, run terraform init to ensure the backend points to the restored object.
Inaccessible backend
If init or plan fails with access errors, verify IAM policy for the required S3 and DynamoDB actions, bucket policy, and network path to AWS. Check that the bucket name and region match the backend configuration.
Limitations: DynamoDB locking is advisory and relies on correct IAM permissions. S3 eventual consistency can affect immediate visibility of state updates across regions. Keep state per environment to avoid accidental cross-environment changes.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.