Terraform

Terraform State Locking: Preventing Concurrent Run Collisions in Practice and on the Exam

2026-04-19
NicheeLab Editorial Team

Terraform treats the state file (tfstate) as the single source of truth, so collisions between concurrent runs can corrupt or destroy it. State Locking is the official mechanism for preventing this — on supported backends, mutual exclusion is enforced automatically.

This article covers backend-by-backend lock behavior, how to implement S3+DynamoDB locking, how to handle collisions, CI/CD serialization patterns, and the points most often tested on the Associate/Pro exams.

State Locking Basics and Why Concurrent Runs Are Dangerous

On plan, apply, destroy, and similar runs, Terraform acquires a lock on the state file whenever the backend supports it. This prevents another process from updating the same state at the same time. If the lock cannot be acquired, the CLI waits (adjustable with -lock-timeout) or eventually fails on timeout.

If the backend does not implement locking, concurrent runs fall into a dangerous "last writer wins" mode, leading to state inconsistencies and orphaned ("ghost") resources. For team or CI/CD use, the basic rule is to always choose a remote backend that supports locking.

Where lock information is stored depends on the backend: DynamoDB for S3, a Blob Lease for AzureRM, the workspace run queue for Terraform Cloud, and so on. Some backends write a .terraform.tfstate.lock.info file in the working directory, which is useful for diagnosing collisions.

  • plan, apply, destroy, import, move, and similar operations acquire a lock
  • The local backend does not support locking and is discouraged outside solo development
  • The S3 backend only gets robust locking once you combine it with a DynamoDB table
  • Do not use -lock=false outside emergencies (high state-corruption risk)
  • Use -lock-timeout=5m or similar to extend how long Terraform waits
BackendLocking SupportLock Implementation / RequirementsNotes
localNoNoneRecommended only for personal or evaluation use
s3Yes (DynamoDB required)DynamoDB table with partition key = LockIDS3 versioning, encryption, and KMS are strongly recommended
azurermYesBlob Storage LeaseNo additional external store required
gcsYesUses Cloud Storage generation control and conditional writes (managed by Terraform)No external store required
consulYesConsul session lockRequires a Consul cluster
httpNoNoneYou must implement collision avoidance yourself

Concurrent CI/CD runs and the lock flow (S3 + DynamoDB example)

Lock acquireLock Denied (waiting)Lock GrantedCI Runner Aterraform applyDynamoDB (LockID)S3 Buckettfstate (version)CI Runner Bterraform applyWhile the lock is held, Runner B waits within the -lock-timeout window and resumes or retries once the lock is released.

Example of specifying a lock wait time (for collision-prone environments)

terraform apply -lock-timeout=5m

Key Points of Locking Support by Backend

Locking support is a decisive factor when choosing a backend. For team and CI/CD use, avoid lock-less options like local and http, and pick s3+dynamodb, azurerm, gcs, or remote (Terraform Cloud) according to your operational requirements.

S3 only achieves real locking when combined with a DynamoDB table. Azure Blob enforces mutual exclusion via Lease, GCS prevents collisions through generation control and conditional writes, and the Terraform Cloud remote backend serializes workspace runs without any additional external store.

  • S3: always set dynamodb_table. The table's partition key must be LockID (String)
  • AzureRM: no additional store needed; mutual exclusion is provided by Blob Lease
  • GCS: Terraform performs safe updates by relying on bucket generation control
  • Remote (Terraform Cloud): the run queue fundamentally avoids collisions
  • HTTP/Local: no locking support — avoid for team or CI use

Configuration examples for representative backends (excerpt)

# S3 + DynamoDB
terraform {
  backend "s3" {
    bucket         = "my-tfstate-bucket"
    key            = "env/prod/terraform.tfstate"
    region         = "ap-northeast-1"
    dynamodb_table = "tf-locks"
    encrypt        = true
  }
}

# AzureRM (Blob Leaseによるロック)
terraform {
  backend "azurerm" {
    resource_group_name  = "rg-tfstate"
    storage_account_name = "sttfstate001"
    container_name       = "tfstate"
    key                  = "env/prod/terraform.tfstate"
  }
}

# GCS(世代管理+条件付き書き込み)
terraform {
  backend "gcs" {
    bucket = "tfstate-bucket"
    prefix = "env/prod"
  }
}

# Terraform Cloud(実行キューによる直列化)
terraform {
  backend "remote" {
    organization = "my-org"
    workspaces {
      name = "infra-prod"
    }
  }
}

Implementing Robust Locking with S3 + DynamoDB (Step by Step)

The most widely adopted setup is using S3 as the state store and DynamoDB for locking. This keeps things safe even with multiple runners or multiple developers running Terraform concurrently.

In production, enable S3 versioning, KMS encryption, and a strict bucket policy, and provision a simple DynamoDB table with partition key = LockID (String).

  • S3 bucket: enable versioning, SSE-KMS recommended, plus a deletion-restriction policy
  • DynamoDB: name the table something like tf-locks, with partition key = LockID (String)
  • Terraform backend: always specify dynamodb_table
  • IAM: grant the CI/CD role S3 read/write plus DynamoDB GetItem/PutItem/DeleteItem/UpdateItem

Required AWS resource definitions (example)

provider "aws" {
  region = "ap-northeast-1"
}

resource "aws_s3_bucket" "tfstate" {
  bucket = "my-tfstate-bucket"
}

resource "aws_s3_bucket_versioning" "tfstate" {
  bucket = aws_s3_bucket.tfstate.id
  versioning_configuration { status = "Enabled" }
}

resource "aws_kms_key" "tfstate" {
  description             = "KMS for Terraform state"
  deletion_window_in_days = 30
}

resource "aws_s3_bucket_server_side_encryption_configuration" "tfstate" {
  bucket = aws_s3_bucket.tfstate.id
  rule { apply_server_side_encryption_by_default { kms_master_key_id = aws_kms_key.tfstate.arn, sse_algorithm = "aws:kms" } }
}

resource "aws_dynamodb_table" "locks" {
  name         = "tf-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"
  attribute { name = "LockID" type = "S" }
}

# バックエンド設定(別ファイルやinit時の-varフラグで渡す)
# terraform {
#   backend "s3" {
#     bucket         = "my-tfstate-bucket"
#     key            = "env/prod/terraform.tfstate"
#     region         = "ap-northeast-1"
#     dynamodb_table = "tf-locks"
#     encrypt        = true
#   }
# }

Detecting, Waiting Out, and Clearing Lock Collisions

On a lock collision, Terraform prints messages like "Error acquiring the state lock" and shows information about the lock holder (ID, Operation, Who, Created, Path, etc.). Waiting long enough via -lock-timeout resolves most cases.

If a process crash leaves an orphaned lock behind, clear it manually with terraform force-unlock <ID>. Before unlocking, always confirm that no other apply is actually running.

  • Prefer waiting; do not reach for -lock=false casually
  • Force-unlocking an orphaned lock is the last resort — leave an unlock log and audit trail
  • In CI/CD, combine a retry strategy (exponential backoff) with extended -lock-timeout

Example error message and unlock command

# 典型的なエラー例(抜粋)
# Error: Error acquiring the state lock
# Details: Resource temporarily unavailable
# Lock Info:
#   ID:        9a6e5e1f-4f0a-4c2a-9a9d-cb1b0e5d1234
#   Operation: OperationTypeApply
#   Who:       runner@build-host
#   Created:   2026-04-18 09:15:22
#   Info:      Working Directory: ., State: s3/env/prod/terraform.tfstate

# 手動解除(孤立ロックのみ)
terraform force-unlock 9a6e5e1f-4f0a-4c2a-9a9d-cb1b0e5d1234

CI/CD Concurrency Patterns (for Both Production and the Exam)

Locking prevents simultaneous state updates, but pipeline design also needs to ensure "the same workspace is never touched concurrently." Separate state per environment (via workspaces or directory splits) and serialize jobs that target the same environment.

With the Terraform Cloud remote backend, runs are automatically serialized per workspace, and you also get queue management, RBAC, and policy guardrails. For self-hosted setups, layer backend locking together with your CI tool's concurrency feature.

  • State separation: split by key/prefix/workspace such as env/prod, env/stage
  • CI serialization: collapse jobs targeting the same key/workspace via concurrency
  • Use the remote (Terraform Cloud) run queue for safe serialization
  • -parallelism is resource-level parallelism, not locking — beware of confusing them on the exam

Serialization example with GitHub Actions + Terraform Cloud backend

# .github/workflows/terraform.yml(同一環境を直列化)
name: terraform
on: [push]
concurrency:
  group: tf-prod   # 同一グループは直列化
  cancel-in-progress: false
jobs:
  apply:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - run: terraform init -input=false
      - run: terraform apply -auto-approve -lock-timeout=10m

# Terraform Cloudのremoteバックエンド
terraform {
  backend "remote" {
    organization = "my-org"
    workspaces { name = "infra-prod" }
  }
}

Exam Checklist and Common Pitfalls

Associate/Pro exams frequently test the basics: DynamoDB is required to enable locking on S3, AzureRM uses Blob Lease, remote relies on queue serialization, and local/http have no locking. They also commonly test that -parallelism is not locking and that force-unlock is a last resort.

Even a plan acquires a lock, so be mindful of concurrent reads against the same state. In CI, raise the safety bar with both environment separation and serialization.

  • S3 locking = configure a DynamoDB table (LockID key)
  • AzureRM uses Blob Lease, GCS uses generation control + conditional writes, Consul uses session locks
  • local/http have no locking — unsuitable for team/CI use
  • Wait via -lock-timeout; -lock=false is generally discouraged
  • -parallelism is resource execution parallelism — separate from locking

Quick reference for options (frequently tested on Associate/Pro)

# ロック関連
terraform plan   -lock-timeout=5m
terraform apply  -lock-timeout=10m
terraform force-unlock <LOCK_ID>   # 孤立ロックの解除(慎重に)

# 並列度(ロックとは別概念)
terraform apply  -parallelism=5

Check Your Understanding

Associate / Pro

問題 1

Multiple CI runners reference state on the same S3 backend, and applies occasionally collide and fail. Which is the recommended fix that involves the smallest change?

  1. Create a DynamoDB table (partition key = LockID) and add dynamodb_table to the backend configuration
  2. Just enable versioning on the S3 bucket
  3. Set terraform apply -parallelism to 1
  4. Add another workspace and reference the same state file from multiple workspaces

正解: A

Proper locking on the S3 backend is only enabled when you pair it with a DynamoDB table. Versioning improves recoverability but is not a lock. -parallelism is resource-level parallelism and does not prevent collisions. Sharing a single state file across multiple workspaces is also incorrect.

Frequently Asked Questions

Does terraform plan also acquire a lock?

Yes. Operations that read, write, or update state — plan, refresh, apply, destroy, import — all acquire a lock when the backend supports locking. Pure read-only operations such as show or simple state inspection may not acquire a lock.

Is it safe to use -lock=false?

Not recommended. Reserve it for emergency recovery scenarios where you can strictly guarantee no other process is running concurrently. Using it routinely in normal operations or CI/CD carries a high risk of state corruption.

What is the correct procedure for clearing an orphaned lock?

First, confirm that no other Terraform run is actually active — contact teammates and review run logs if needed. Then run terraform force-unlock <ID> using the lock ID from the error message. Record the unlock action in an audit log and proceed cautiously with the next apply.

Check what you learned with practice questions

Practice with certification-focused question sets

無料で問題を解いてみる
Author

NicheeLab Editorial Team

NicheeLab editorial team focused on data engineering and cloud certification learning. Content is structured around practical study needs and official exam domains.


Related articles
Terraform

HCL Syntax: Terraform's Configuration Language (2026)

HCL2 fundamentals for Terraform — blocks, attributes, expres...

Terraform

Terraform Authoring & Operations Pro: Complete Guide (2026)

Tactics for the Terraform Pro exam — module authoring, works...

Terraform

Terraform Providers: Plugin Management Fundamentals (2026)

Provider mechanics — required_providers, versions, mirrors, ...

Terraform

Terraform Resource Blocks: Declarative Infra Units (2026)

Resource block fundamentals — addresses, references, common ...

Terraform

Terraform Data Sources: Read-Only External Data (2026)

Data source basics — declaration, refresh behavior, dependen...

Browse all Terraform articles (102)
© 2026 NicheeLab All rights reserved.