首页 > Technology > 正文

Terraform state lock 报错:先查三件事

fenij 2026-09-24 2 Technology

报错 Error acquiring the state lock 弹出来的时候,多数人的第一反应是 terraform force-unlock,手快一点的直接把锁文件删了。先说结论:锁本身很少出错。九成情况是「上一次 run 没干净退出」或者「有并发 job 正在跑」。前者可以解锁,后者一解锁就是两个 writer 同时写同一份 state——云上资源可能还没炸,state 已经和真实环境对不上了。

1
读 Lock Info
2
找活着的进程
3
备份 state 再解锁

先读懂报错里那段 Lock Info

Error: Error acquiring the state lock

Error message: ConditionalCheckFailedException: The conditional request failed
Lock Info:
  ID:        a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d
  Path:      my-bucket/prod/terraform.tfstate
  Operation: OperationTypeApply
  Who:       runner@gitlab-runner-01
  Version:   1.9.8
  Created:   2026-09-24 01:12:33.123456789 +0000 UTC
  Info:

这段不是装饰。Who 说明谁占着锁,Created 说明它从什么时候开始占,Operation 决定破坏力(OperationTypePlan 只读,OperationTypeApply 会改资源),Path 对应后端里那份 state 的 key。四个字段里读错一个,后面的判断就全歪了。

锁的来源 典型特征 正确动作
并发 run(CI 或同事) Who 指向一台还活着的主机,Created 在几分钟内 等它跑完,或用 -lock-timeout 排队等;不要解锁
卡死或被打断的旧 run Created 超过 30 分钟,那台机器上查不到 terraform 进程 备份 state 后 force-unlock
崩溃残留的陈旧锁 Who 指向已销毁的容器,或本地只剩一个锁文件 备份 state 后 force-unlock;本地 state 可直接删锁文件

第一步:确认还有没有活着的 writer

锁唯一的作用就是防止两个进程同时改 state,所以判断顺序永远是「先找 writer,再决定解不解」。在本地、跳板机或那台 CI runner 上跑:

# Linux / macOS:注意 [t] 的写法,避免把 grep 自己也算进去
ps -ef | grep '[t]erraform'
pgrep -fl terraform

# 看看有没有容器化的 runner 正在执行
docker ps --filter name=tf-runner

# Windows(PowerShell)
Get-Process terraform -ErrorAction SilentlyContinue

查到进程,就别动锁——哪怕它已经挂了 40 分钟,先 kill 掉再等它自己释放。Who 里那台主机你连不上、也没有访问 CI 控制台的权限,那就先别猜,去问一声比强解安全得多。

第二步:直接读后端里的锁对象

报错只给了一份摘要,要看细节得去后端把锁记录捞出来。三种后端的查法不一样:

# S3 + DynamoDB 后端:锁 item 的 LockID 就是 "bucket/key"
aws dynamodb get-item \
  --table-name terraform-locks \
  --key '{"LockID":{"S":"my-bucket/prod/terraform.tfstate"}}' \
  --region cn-north-1

# GCS 后端:锁是 state 旁边的一个 .tflock 对象
gsutil stat gs://my-bucket/prod/terraform.tfstate.tflock

# 本地 / 文件后端:锁信息就在配置目录里
ls -l .terraform.tfstate.lock.info
cat .terraform.tfstate.lock.info

这里有个容易踩的位置差异:用本地 state 时锁文件是 .terraform.tfstate.lock.info,配了远程后端之后锁文件挪到了 .terraform/terraform.tfstate.lock.info。在错误的目录里 ls 半天,很容易误判成「锁已经没了」,然后对着一个其实存在的锁反复 force-unlock

DynamoDB 那条记录里的 Info 字段是段 JSON,含 Who / Created / Operation。它和报错里的 Lock Info 应当一一对上;对不上,说明你看的不是同一份 state——多半是 workspace 或 backend key 切错了,terraform workspace show 一眼就能验。

第三步:先备份 state,再 force-unlock

确认没有活着的 writer 之后,动作顺序不能反:备份在前面。

# 1. 先把当前 state 拉一份到本地,文件名带时间戳
terraform state pull > state.backup.$(date +%s).json
ls -l state.backup.*.json

# 2. 用报错里给出的那个 ID 原样解锁,不要自己拼
terraform force-unlock a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d

# 3. 无人值守场景跳过确认
terraform force-unlock -force a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d

解锁完别直接 apply。先跑一次带退出码的 plan,用机器可读的方式确认 state 还能用:

terraform plan -detailed-exitcode; echo "exit=$?"
# exit=0 无差异;exit=2 有差异(正常)
# exit=1 表示报错,state 可能已经损坏,直接从备份恢复

别做的两件事

-lock=false 不能解锁。它只是让本次运行不去申请锁,不会清掉别人留下的锁,结果是两个 writer 一起写。Hashicorp 自己在文档里也不推荐这么用。

手删 DynamoDB 里那条锁记录,比 force-unlock 更危险。绕过锁机制直接删 item,如果原来那个持有者其实还在跑,两边会同时往同一个 state key 写;S3 后写的覆盖先写的,而失败的一方可能已经建好了资源却没记进 state。真到这一步,只能靠 S3 版本化回滚:aws s3api list-object-versions --bucket my-bucket --prefix prod/terraform.tfstate

处置结论

陈旧锁 → 可以 force-unlock(先备份 state)。并发中的锁 → 老实等,加 -lock-timeout=5m 比解锁快。想用 -lock=false 绕过 → 不推荐,它连本次运行的保护都一并取消了。

踩坑实录

有次 GitLab CI 的 apply 报了 state lock,我看 Lock Info 里 Created 是 22 分钟前,判断是「被打断的旧 job」,直接 force-unlock -force,解锁成功,重新跑了一遍 pipeline。问题出在下一个 job 的 plan 上:它列出 3 个 -/+ destroy and then create replacement,全是已经在跑的 aws_s3_bucket。登到 runner 上一看,ps -ef | grep '[t]erraform' 里那个旧进程还活着——它卡在 waiting for S3 Bucket (xxx) to be deleted 的轮询上,进度停在 18 分钟前,看着像死了,其实没死。22 分钟不是陈旧锁的判据,没进程才是。那次只跑到 plan 就发现,没真删东西,但流程里多出来一条硬规矩:解锁前必须登到 runner 上 grep 一次进程。

根治:让锁不再卡住

把并发挡住,比事后解锁省事得多。GitHub Actions 和 GitLab CI 各有一行配置能做串行化:

# GitHub Actions:同一 state 的 workflow 排队,不并发
concurrency:
  group: terraform-prod
  cancel-in-progress: false
# GitLab CI:resource_group 加锁 + job 超时,避免跑飞
deploy:
  resource_group: terraform-prod
  timeout: 20m
  script:
    - terraform init
    - terraform apply -auto-approve -lock-timeout=5m

-lock-timeout 默认是 0,也就是「拿不到锁立刻失败」。CI 里并发几秒的重叠很常见,把它设成 5m,让后到的 job 自己等,比让流水线红一片友好得多。

-lock-timeout 默认值
0
拿不到锁立刻失败
陈旧锁的唯一判据
无活进程
时间长短不作数
force-unlock 的作用域
只碰锁
不动任何资源
解锁前的必备动作
备份 state
state pull 一条命令

常见问题

Q:force-unlock 会改云上资源吗?
不会。它只清掉锁,不碰任何基础设施。真正有风险的是解锁后两个 writer 并存,所以「先确认没有活进程」这一步不能省。

Q:报错里的 ID 和 DynamoDB 里的 LockID 是同一个东西吗?
不是。LockID 是 state 的定位(bucket/key),报错里的 ID 是这一把锁的标识。force-unlock 传报错给出的那个 ID,别自己拼字符串。

Q:本地 state 也需要 force-unlock 吗?
不需要。本地 state 没有后端协调,确认没有 terraform 进程后,删掉 .terraform.tfstate.lock.info 就能继续。

小结

整套流程就三步:读 Lock Info,去找活着的进程,备份 state 再解锁。顺序反了,代价就是用一份和现实脱节的 state 去跑 apply。你在 CI 里还遇到过别的锁死法——比如 Consul 后端或者 Terraform Cloud 的队列堵住——欢迎在 fenij.com 留言说说,我这边整理成第二篇。

相关完整手册

系统化的排查与配置思路,建议顺手收藏这几篇完整手册: