What's Actually Happening
Terraform operations fail due to state lock. Another operation holds the lock, or a stale lock exists from a crashed operation.
The Error You'll See
```bash $ terraform apply
Error: Error acquiring the state lock Error message: ConditionalCheckFailedException: The conditional request failed ```
Lock info error:
```bash $ terraform apply
Error: State locked by another operation Lock Info: ID: abc123 Path: terraform.tfstate Operation: terraform apply Who: user@host ```
Timeout error:
```bash $ terraform apply
Error: timeout while waiting for state lock ```
S3 backend error:
Error: Error acquiring the state lock: operation error S3: PutObject, forbiddenWhy This Happens
- 1.Another apply running - Concurrent Terraform operation
- 2.Crashed operation - Previous run left stale lock
- 3.Network issue - Lock acquisition failed mid-operation
- 4.Permission denied - Cannot acquire lock in backend
- 5.Backend misconfigured - Lock table not configured
- 6.CI/CD timeout - Pipeline cancelled leaving lock
Step 1: Check State Lock Status
```bash # Force unlock to see lock info: terraform force-unlock -help
# Check lock status (if supported by backend): terraform state list
# For S3 backend, check DynamoDB: aws dynamodb get-item --table-name terraform-locks \ --key '{"LockID":{"S":"bucket-name/key-name"}}'
# For local state, check .terraform.tfstate.lock.info: cat .terraform.tfstate.lock.info
# Check for running Terraform processes: ps aux | grep terraform
# Check if another terminal running Terraform ```
Step 2: Check Lock Details
```bash # Terraform shows lock details in error: # Lock Info: # ID: abc-123-def # Path: terraform.tfstate # Operation: terraform apply # Who: user@hostname # Version: 1.5.0 # Created: 2024-01-01T10:00:00Z # Info: optional info
# For S3 + DynamoDB backend: aws dynamodb scan --table-name terraform-locks
# For Consul backend: consul kv get -recurse terraform/
# For TFC/TFE: # Check runs in Terraform Cloud UI
# For Azure backend: az storage entity show --table-name terraformlocks \ --entity PartitionKey state RowKey state ```
Step 3: Wait for Lock Release
```bash # If another operation is legitimately running:
# Check operation progress: # In another terminal, the running terraform apply/plan
# Wait for completion (can take minutes) # Lock auto-releases on completion
# Check periodically: watch -n 10 'terraform state list 2>&1'
# If using CI/CD: # Check pipeline status # Wait for completion or timeout ```
Step 4: Force Unlock
```bash # Force unlock (DANGEROUS - only if sure no other operation running):
terraform force-unlock LOCK_ID
# Example: terraform force-unlock abc-123-def
# Confirm when prompted: # Do you really want to force-unlock? (yes/no): yes
# Force unlock without confirmation: terraform force-unlock -force LOCK_ID
# For S3 backend, delete from DynamoDB: aws dynamodb delete-item --table-name terraform-locks \ --key '{"LockID":{"S":"mybucket/path/terraform.tfstate"}}'
# For local state: rm -f .terraform.tfstate.lock.info
# After force unlock: terraform apply ```
Step 5: Configure S3 Backend Locking
```hcl # Proper S3 backend configuration with locking:
terraform { backend "s3" { bucket = "my-terraform-state" key = "prod/terraform.tfstate" region = "us-east-1"
# DynamoDB table for locking: dynamodb_table = "terraform-locks"
# Enable encryption: encrypt = true } }
# Create DynamoDB table for locks: aws dynamodb create-table \ --table-name terraform-locks \ --attribute-definitions AttributeName=LockID,AttributeType=S \ --key-schema AttributeName=LockID,KeyType=HASH \ --billing-mode PAY_PER_REQUEST ```
Step 6: Configure Other Backend Locks
```hcl # Consul backend: terraform { backend "consul" { address = "consul.example.com" path = "terraform/state/prod" lock = true } }
# Azure backend: terraform { backend "azurerm" { resource_group_name = "terraform" storage_account_name = "tfstate" container_name = "tfstate" key = "prod.terraform.tfstate" } }
# GCS backend: terraform { backend "gcs" { bucket = "terraform-state" prefix = "prod" } }
# HTTP backend with locking: terraform { backend "http" { address = "https://state.example.com/state" lock_address = "https://state.example.com/lock" unlock_address = "https://state.example.com/unlock" } } ```
Step 7: Fix Permission Issues
```bash # Check S3 permissions: aws s3 ls s3://my-terraform-state/
# Check DynamoDB permissions: aws dynamodb describe-table --table-name terraform-locks
# IAM policy for S3 + DynamoDB: { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:ListBucket", "s3:GetObject", "s3:PutObject", "s3:DeleteObject" ], "Resource": [ "arn:aws:s3:::my-terraform-state", "arn:aws:s3:::my-terraform-state/*" ] }, { "Effect": "Allow", "Action": [ "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem" ], "Resource": "arn:aws:dynamodb:*:*:table/terraform-locks" } ] }
# Test permissions: aws s3 cp test.txt s3://my-terraform-state/test.txt aws dynamodb put-item --table-name terraform-locks \ --item '{"LockID":{"S":"test"}}' ```
Step 8: Handle CI/CD Locks
```bash # In CI/CD pipeline:
# Add timeout to prevent stale locks: terraform apply -lock-timeout=10m
# Use auto-unlock on failure: # In pipeline, add cleanup step:
cleanup: script: - terraform force-unlock -force $(cat .terraform/lock.id) || true when: always
# For GitHub Actions: - name: Terraform Apply run: terraform apply -auto-approve -lock-timeout=15m timeout-minutes: 30
- name: Cleanup Lock
- if: always()
- run: terraform force-unlock -force LOCK_ID || true
# For GitLab CI: terraform_apply: script: - terraform apply -auto-approve after_script: - terraform force-unlock -force LOCK_ID || true ```
Step 9: Prevent Lock Issues
```bash # Use -lock-timeout: terraform apply -lock-timeout=10m
# Disable lock (not recommended): terraform apply -lock=false
# Use workspaces for parallel work: terraform workspace new dev terraform workspace new prod
# Each workspace has separate state lock
# Check current workspace: terraform workspace show
# Use different state files per environment: # In backend config: terraform { backend "s3" { key = "${workspace}/terraform.tfstate" } }
# Run operations serially in CI/CD: # Use mutex or queue in pipeline ```
Step 10: Terraform Lock Verification Script
```bash # Create verification script: cat << 'EOF' > /usr/local/bin/check-tf-lock.sh #!/bin/bash
echo "=== Checking for Terraform processes ===" ps aux | grep terraform | grep -v grep
echo "" echo "=== Local Lock Files ===" find . -name "*.lock.info" 2>/dev/null cat .terraform.tfstate.lock.info 2>/dev/null || echo "No local lock file"
echo "" echo "=== Backend Configuration ===" grep -A10 "backend" *.tf 2>/dev/null | head -20
echo "" echo "=== State File Status ===" ls -la terraform.tfstate* 2>/dev/null || echo "No local state files"
echo "" echo "=== Workspace ===" terraform workspace show 2>/dev/null || echo "Not initialized"
echo "" echo "=== S3/DynamoDB Lock Check ===" if grep -q "dynamodb_table" *.tf 2>/dev/null; then TABLE=$(grep "dynamodb_table" *.tf | head -1 | awk -F'"' '{print $2}') BUCKET=$(grep "bucket" *.tf | head -1 | awk -F'"' '{print $2}') echo "DynamoDB table: $TABLE" echo "S3 bucket: $BUCKET" aws dynamodb scan --table-name $TABLE 2>/dev/null || echo "Cannot access DynamoDB" else echo "No DynamoDB lock table configured" fi
echo "" echo "=== Force Unlock Command ===" echo "If stuck, run: terraform force-unlock LOCK_ID" EOF
chmod +x /usr/local/bin/check-tf-lock.sh
# Run: /usr/local/bin/check-tf-lock.sh
# Quick unlock alias: alias tf-unlock='terraform force-unlock' ```
Terraform State Lock Checklist
| Check | Command | Expected |
|---|---|---|
| Lock status | terraform apply | No lock error |
| Lock table | DynamoDB/GCS | Table exists |
| Permissions | IAM policy | Full access |
| Backend config | terraform init | Backend configured |
| No stale lock | force-unlock | Removed |
| Timeout | -lock-timeout | Set appropriately |
Verify the Fix
```bash # After fixing state lock
# 1. Run terraform init terraform init // Backend initialized
# 2. Check state terraform state list // State accessible
# 3. Run plan terraform plan // No lock error
# 4. Run apply terraform apply -lock-timeout=5m // Operation succeeds
# 5. Verify no stale lock terraform force-unlock // Error: no lock found (expected)
# 6. Check backend aws dynamodb scan --table-name terraform-locks // Lock released ```
Related Issues
- [Fix Terraform State Corrupted](/articles/fix-terraform-state-corrupted)
- [Fix Terraform Init Failed](/articles/fix-terraform-init-failed)
- [Fix Terraform Provider Authentication Failed](/articles/fix-terraform-provider-authentication-failed)