假设一种情况,用户使用terraform apply准备提交修改,但是突然断网了,有没有可能出现state文件修改了,但是对云资源的操作并没有完成的情况,如果再过十分钟,网络恢复了,这个时候再apply,还能把修改提交到云资源上么?
这是个很实际、也确实是 Terraform 用户会踩到的坑,答案是:是的,这种不一致完全可能出现,而且往哪个方向不一致要看网络具体是在哪个时间点断的——比你设想的那个方向(state 改了、云端没完成)更常见、更危险的其实是反过来的情况。拆开讲清楚。
根本原因:”云端操作完成” 和 “state 持久化” 不是一个原子操作
terraform apply 对每一个资源的处理,大致是这几步:
1. 向云 API 发起请求(POST/PUT) 2. (很多资源是异步长任务)轮询状态,直到云端返回"完成" 3. 拿到云端返回的最终属性 4. 把这些属性写入 state 文件,并推送到 remote backend
第 1~3 步和第 4 步之间没有分布式事务——Terraform 没办法把”云端确认完成”和”state 文件落盘”打包成一次原子提交。所以任何一次中断(网络断开、进程被杀、机器断电)只要卡在这两件事之间,就会留下不一致的窗口。
更值得担心的方向:云端其实成功了,但 state 没记下来
关键点是:云服务商的控制面不需要 Terraform 客户端全程在线才能完成操作。以 Azure 为例,ARM 的长任务模式是:客户端发一个 PUT,Azure 立刻返回 202 Accepted + 一个轮询地址,后续创建/更新是在 Azure 这边异步跑的,Terraform 只是隔一段时间去问一下”好了没”。
如果网络恰好在 Terraform 轮询等结果的时候断掉——Azure 那边完全可能已经把资源建好了,只是 Terraform 这个客户端因为连不上,收不到”完成”这个响应,于是把这次 apply 当成失败处理,不会把这个资源标记为”成功”写入 state。结果就是:云上资源已经存在、已经改好了,但你本地(或者远程 backend 里)的 state 文件完全不知道这件事——这才是最容易埋雷的方向,而不是你说的”state 改了但云端没完成”。
你设想的那个方向(state 改了,云端没完成):对应的是”tainted”机制
Terraform 只有在 provider 明确返回错误、但同时又带回了部分信息(比如”资源 ID 已经拿到了,但后续配置没跑完”)的情况下,才会往 state 里写一条不完整的记录,并把这个资源标记成 tainted(污染)。下次 apply 看到 tainted 资源,不会尝试”修复”它,而是直接销毁重建——因为 Terraform 不信任这个残缺状态里的属性是准确的。这种情况确实存在,但触发条件比较窄,不是网络一断就必然发生。
state 锁这时候会怎样
如果你用的是带锁的 remote backend(S3+DynamoDB、Terraform Cloud 等),apply 一开始就会拿到一把锁,正常情况下 apply 结束(不管成功失败)才会释放。进程被网络中断/直接杀死,这把锁不一定会自动释放——具体行为取决于 backend 的实现(有些基于租约的锁会在租约到期后自动过期,有些则会一直卡着)。所以你描述的场景里,10 分钟后网络恢复,很可能第一步就会先卡在:
Error: Error acquiring the state lock Lock Info: ID: xxxx-xxxx-xxxx ...
需要先确认没有别人真的在跑 apply,再手动:
terraform force-unlock <LOCK_ID>
(这个命令本身很危险,官方文档明确警告:如果这时候真的有另一个进程持有这把锁,强行解锁会导致两边同时写 state,把 state 文件搞坏——务必先确认清楚锁是不是”孤儿锁”。)
回到你的问题:锁解开之后,再 apply 还能提交成功么
能,但不是无脑直接 apply,而是先让它重新”对账”。 terraform plan/apply 默认会先做一次 refresh(重新读取云上资源的真实状态),这一步在很大程度上能自动纠正前面说的”云端已完成、state 没跟上”的问题——如果这个资源类型的创建本身是幂等的(比如 Azure Resource Group 走的还是我们之前聊过的那套 ARM 幂等 PUT 语义:同名资源再 PUT 一次只是确认/更新,不会报错),那这次重新 apply 基本能顺利把 state 和现实对齐,不会出问题。
但如果这个资源类型的”创建”操作不是幂等的(纯 POST 语义,没有”存在就更新”这个兜底,比如某些一次性生成 ID 的资源),refresh 之后 Terraform 仍然会认为”state 里没有这个资源,需要创建”,于是再发一次创建请求——这时候就可能报 Error: A resource with the ID ... already exists,或者更糟,直接建出一个重复的资源。遇到这种情况,通常需要手动介入:要么用我们上一个问题聊到的 terraform import/import 块把这个”已经存在但 state 不知道”的资源重新收编回来,要么手动清理掉云上多余的那份,再重新 apply。
这里也正好能看出 Crossplane/K8s controller 模式相对 Terraform 这种”一次性 CLI 执行”模式的一个结构性优势:Crossplane 的 reconcile loop 是持续不断在跑的,哪怕某一次 reconcile 因为网络问题失败了,不需要人在”网络恢复的那一刻”手动介入,它自己隔几秒/几分钟会再试一次,自动收敛;而 Terraform 默认只在你主动执行 plan/apply 的那一刻做一次性核对,中间这段”不一致窗口”能持续多久,取决于运维人员多久重新跑一次 apply。这也是为什么很多团队会在 CI/CD 里给 Terraform 单独配一个”定时漂移检测”的任务(定期跑 terraform plan -detailed-exitcode 报警),某种程度上是在人工补上 Terraform 本身没有的”持续 reconcile”能力。
Sources:
