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:

bash
Error: Error acquiring the state lock: operation error S3: PutObject, forbidden

Why This Happens

  1. 1.Another apply running - Concurrent Terraform operation
  2. 2.Crashed operation - Previous run left stale lock
  3. 3.Network issue - Lock acquisition failed mid-operation
  4. 4.Permission denied - Cannot acquire lock in backend
  5. 5.Backend misconfigured - Lock table not configured
  6. 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

CheckCommandExpected
Lock statusterraform applyNo lock error
Lock tableDynamoDB/GCSTable exists
PermissionsIAM policyFull access
Backend configterraform initBackend configured
No stale lockforce-unlockRemoved
Timeout-lock-timeoutSet 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 ```

  • [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)