Terraform 状态文件被我弄乱后的补救
故障发生时的环境
4 月 12 日 01:07,我被值班电话叫醒,CodeArts 的 prod 部署在 apply 阶段连挂 3 次,失败流水线 ID 分别是 #18422、#18423、#18424。我们这套环境规模不大,生产侧是 3 台 ECS s6.large.2(2 vCPU / 4 GB,Ubuntu 22.04.3 LTS),一台 RDS MySQL 8.0.36(2C4G,ESSD 120GB),VPC、子网、路由、安全组全交给 Terraform。版本固定在 Terraform 1.6.6、huaweicloud provider 1.63.2、huaweicloud CLI 3.0.76,CI 里执行命令是 terraform apply -auto-approve -input=false -parallelism=10。
我那晚只睡了 1 小时 40 分。
state 在 OBS 桶 tfstate-prod,SSE-KMS 开着,桶版本控制也开着,但没做锁。我一直靠“同一时间别两个人发版”的口头约定,写这段的时候我都觉得离谱。下面这段 backend 配置当时看起来挺干净,现在看是埋雷。
terraform {
required_version = "1.6.6"
backend "s3" {
bucket = "tfstate-prod"
key = "network/prod.tfstate"
region = "cn-north-4"
endpoint = "obs.cn-north-4.myhuaweicloud.com"
skip_region_validation = true
skip_credentials_validation = true
force_path_style = true
}
}
状态文件怎么乱掉的
白天 17:26 有同事为压测临时放通流量,直接跑 CLI 删了一条规则,命令是 huaweicloud vpc security-group-rule delete 7d3f6c2e-2f1e-4db0-9b8a-3a9d9a3a3f11,RequestId 我在审计日志里翻到的是 3f8b4f2d1c8c4f6f9f6f95b3b2e2f4ab。同一天下班前另一位同事合了模块重命名 MR,把 module.net 改成 module.network,代码里地址改了,但 terraform state mv 没补。一个是线上资源被手改,一个是状态地址改了没迁,两个动作叠在一起就开始分叉。
00:58 和 01:02 两条流水线几乎同时起跑,后写入那条把前一条刚更新的 serial 覆盖了。我从 OBS 版本里对比出来 00:58 那次把 serial 从 412 写到 414,01:02 那次又写成 415,但内容不是在 414 基础上增量更新,而是拿了旧快照覆盖。apply 日志里这两段非常刺眼:
Error: error querying Security Group Rule (id=7d3f6c2e-2f1e-4db0-9b8a-3a9d9a3a3f11):
StatusCode=404 RequestId=3f8b4f2d1c8c4f6f9f6f95b3b2e2f4ab
ErrorCode=VPC.0101
ErrorMessage=Resource not found
Error: Provider produced inconsistent result after apply
When applying changes to module.network.huaweicloud_vpc_security_group_rule.web_ingress,
provider "registry.terraform.io/huaweicloud/huaweicloud" produced an unexpected new value.
这个坑是我自己挖的。
我怎么把它拉回一致
我先把 CodeArts 里 prod 分支自动触发关掉,发布页面打了“维护中”,再把两个正在排队的任务手动取消。这个动作花了 6 分钟,时间是 01:13 到 01:19。那几分钟我反复刷新流水线页面,怕又冒出新任务把 state 再写坏一次。
我做过一次失败尝试。01:31 我想偷懒整包回滚,直接把 OBS 的历史版本覆盖回主 key,然后跑 terraform plan,命令是:
obsutil cp "obs://tfstate-prod/network/prod.tfstate?versionId=3L4k...Aq" \
"obs://tfstate-prod/network/prod.tfstate" -f
terraform init -reconfigure
terraform plan
结果 plan 直接给出 Plan: 9 to add, 3 to change, 7 to destroy,里面包含一条当晚 00:46 刚合法生效的子网路由变更,差点被我一把抹掉。我当时手是抖的。这个方案我一开始以为能省时间,现在回头看风险太高,我在 01:38 停掉了这条路,重新从当前损坏 state 做增量修复。
我从 OBS 拉了最近 10 个版本做对账,先看 serial 和 lineage,再比 resources 地址列表。对账命令如下:
obsutil ls obs://tfstate-prod/network/prod.tfstate -v
obsutil cp "obs://tfstate-prod/network/prod.tfstate?versionId=3L4k...Aq" ./prod-prev.tfstate
terraform state pull > prod-broken.tfstate
jq '.serial,.lineage' prod-prev.tfstate prod-broken.tfstate
jq -r '.resources[].module + "." + .type + "." + .name' prod-prev.tfstate | sort > prev.list
jq -r '.resources[].module + "." + .type + "." + .name' prod-broken.tfstate | sort > broken.list
diff -u prev.list broken.list
结论是 prev serial=412,broken serial=415,lineage 一样,但 415 少了 4 条 huaweicloud_vpc_security_group_rule,多了一个 module.network.huaweicloud_nat_gateway.main。这个 NAT 是并发 apply 时误建的,计费中心第二天显示 NAT 使用 43 分钟 0.88 元,附带 EIP 0.31 元,总计 1.19 元,钱不多,脸很疼。
修复时我没有直接回退整包,而是删掉误建资源,再把缺失项逐条 import,最后跑 refresh-only 收敛状态。命令和顺序是这样:
terraform state rm module.network.huaweicloud_nat_gateway.main
terraform import module.network.huaweicloud_vpc_security_group_rule.web_ingress[0] 5a4e7e4d-6b4b-4d8a-9ad8-2dd7f8d6aa21
terraform import module.network.huaweicloud_vpc_security_group_rule.web_ingress[1] 8f3ab6c3-b5c7-4a51-92bc-c61cc7f8e9aa
terraform import module.network.huaweicloud_vpc_security_group_rule.web_ingress[2] 11e6f0d9-0a0d-4eb9-8a19-3eb3f1f80f64
terraform import module.network.huaweicloud_vpc_security_group_rule.web_ingress[3] b7ac55dc-67e1-4fcb-a566-94e9f3c1092a
terraform state mv module.net.huaweicloud_vpc.main module.network.huaweicloud_vpc.main
terraform state mv module.net.huaweicloud_vpc_subnet.app[0] module.network.huaweicloud_vpc_subnet.app[0]
terraform plan -refresh-only -out refresh.tfplan
terraform apply refresh.tfplan
这一步从 01:46 跑到 02:15,29 分钟,期间我还处理了一次 provider 读超时,报错是 context deadline exceeded while reading VPC subnet,重试第 2 次才过。01:07 到 03:24 业务可用性没掉,发布窗口丢了 2 小时 17 分,这个损失是实打实的。
我后来加的约束
我没上复杂平台改造,只做了能当天落地的约束。CodeArts 里 prod 环境改成单并发;任何人用 CLI 改 Terraform 托管资源,MR 必须贴命令和 RequestId;每次 apply 后自动跑 terraform plan -refresh-only,输出非空就阻断后续发布。这个规则上线后 11 天里拦住了 2 次漂移,一次是手工改了安全组描述,一次是控制台删了测试子网。
我还塞了个很土的锁脚本,用 OBS 对象当互斥锁,30 秒拿不到锁就退出,退出码 2,流水线会标红:
LOCK_KEY="obs://tfstate-prod/lock/prod.lock"
if obsutil stat ${LOCK_KEY}; then
echo "lock exists"; exit 2
fi
obsutil cp ./build_id.txt ${LOCK_KEY}
trap 'obsutil rm ${LOCK_KEY}' EXIT
这个锁不是强一致,任务异常退出会残留锁文件,我现在靠 15 分钟超时清理,体验一般。
下周我会把 state 拆成 network、compute、app 三个后端,减少单文件冲突面;另外准备把 CLI 变更收敛到一个审计脚本里,至少别再出现“先改了再补记录”。
- 点赞
- 收藏
- 关注作者
评论(0)