Terraform 状态串线后我的救火记录
出事当晚我看到的东西
周二 22:17,我在生产仓库跑 terraform plan,只改了一条 443 入站白名单,第一屏就弹出 Plan: 6 to add, 0 to change, 37 to destroy。我连做了三次自检:pwd 在 /home/ops/iac-prod,terraform workspace show 回的是 prod,git rev-parse --short HEAD 是刚合并的 9f2c4ad。命令都没错,但 destroy 列表里出现了两台对外 API ECS 和共享型 ELB,我当时手心全是汗。
环境参数我后来在复盘里写得很死。Terraform 1.6.6,provider huaweicloud/huaweicloud 1.62.1,region 是 cn-north-4。CI Runner 是 c6.large.2,2 vCPU、4 GiB、Ubuntu 22.04、GitLab Runner 16.8.1。生产状态有 54 个资源,ECS 规格是 s7.xlarge.2,4 vCPU/8 GiB,共 2 台;还有 1 个 ELB、1 个 NAT 网关、6 条 EIP、5 个安全组、3 个子网。状态文件放 OBS 桶 team-iac-state,开了版本控制,这个开关后面救了命。
terraform {
backend "s3" {
bucket = "team-iac-state"
key = "prod/main.tfstate"
region = "cn-north-4"
endpoint = "obs.cn-north-4.myhuaweicloud.com"
skip_credentials_validation = true
skip_region_validation = true
force_path_style = true
}
}
根因不复杂,就是低级。monitor 仓库创建时从 prod 仓库直接复制了 backend,key 没改,两个仓库都写 prod/main.tfstate。我把流水线时间线和 OBS 版本时间对过一遍,21:48 monitor 合并到 main,触发 pipeline #9132,21:52 apply 成功,用时 3m41s,把 serial 从 184 写成了 27,状态资源数从 54 变成 8。22:17 我在 prod 跑 plan 才看到要删一大片。
# 片段,来自 22:17 的 plan 输出
# huaweicloud_compute_instance.api[0] will be destroyed
# huaweicloud_compute_instance.api[1] will be destroyed
# huaweicloud_elb_loadbalancer.shared will be destroyed
# huaweicloud_nat_gateway.main will be destroyed
# huaweicloud_vpc_eip.api_eip[0] will be destroyed
Plan: 6 to add, 0 to change, 37 to destroy.
22:24 我做了个错误动作,想用 target 先把 NAT 绑定那条 EIP 补齐,命令是 terraform apply -target=huaweicloud_vpc_eip.nat_eip -auto-approve,结果撞配额,日志里返回 EIP.0101,提示 floating IP 超限;我查了账号配额,EIP hard limit 是 6,我当时已经占满 6/6,这条路走不通。
Error: error creating VPC EIP: Bad request with: [POST https://vpc.cn-north-4.myhuaweicloud.com/v1/xxxx/publicips],
error message: {"error_code":"EIP.0101","error_msg":"The quota of floating IPs exceeds the limit."}
那一刻我把手从回车键上挪开了。
我又试了第二个失败动作,偷懒复用前一天的 plan artifact,文件名 .ci/artifacts/prod-20261021.tfplan,SHA256 是 3f0e...9a7c,执行 terraform apply .ci/artifacts/prod-20261021.tfplan 直接报 Error: Saved plan is stale。这个报错把我拉回现实了,计划文件绑定的是旧 serial,硬上只会继续污染状态,我马上停手,后面没再执行任何 apply。
这个方案我一开始是拒绝的。
恢复和我改掉的地方
22:29 我在团队群发 freeze,原文就一句,所有仓库暂停 apply,只保留 plan 和只读查询。然后我在 GitLab 把生产环境变量 TF_APPLY_ENABLED 从 true 改 false,保护分支上的 deploy job 也临时改成 manual,避免有人顺手点掉。这个动作花了不到 2 分钟,但把新的写入先截断了。
我上跳板机 s6.large.2(2 vCPU/4 GiB)拉 OBS 历史版本,先列出版本,再抓一份 bad 和一份 good 到本地比对。命令和输出我都留了:
obsutil ls -v obs://team-iac-state/prod/main.tfstate
obsutil cp obs://team-iac-state/prod/main.tfstate ./bad.tfstate -versionId=3L4k9f0x2a
obsutil cp obs://team-iac-state/prod/main.tfstate ./good.tfstate -versionId=3L4j8d7m1c
jq '.serial, .lineage, (.resources|length)' bad.tfstate
jq '.serial, .lineage, (.resources|length)' good.tfstate
# bad => 27, 0e1f3e2a-6c8b-4f2c-9a8d-3f4b4f0d7b11, 8
# good => 184,0e1f3e2a-6c8b-4f2c-9a8d-3f4b4f0d7b11,54
lineage 一样,说明是同一条状态链被覆盖,不是有人手工改 JSON 另起炉灶。我先把 bad 备份成 rollback-2026-10-22-2229.tfstate,再回推 good。回推前我重做了一次 init,防止本地缓存 backend 参数:
cp bad.tfstate rollback-2026-10-22-2229.tfstate
terraform init -reconfigure
terraform state push good.tfstate
terraform plan -refresh-only
terraform apply -refresh-only -auto-approve
refresh-only 跑了 6 分 12 秒,结束后还有 3 个漂移。第一个是我前面 target 失败留下的 EIP 半成品,云上有 ID 3d8f2f6a-xxxx-xxxx-xxxx-2d9fbcf9a9e7,state 里没有,我用 terraform import huaweicloud_vpc_eip.nat_eip 3d8f2f6a-xxxx-xxxx-xxxx-2d9fbcf9a9e7 补入。另两个是 monitor 误写进来的 SMN 订阅和告警 action,地址分别是 module.monitor.huaweicloud_smn_topic_subscription.ops_sms 和 module.monitor.huaweicloud_ces_alarmrule.cpu_high,这些不属于 prod,我执行 terraform state rm 清掉,然后在 monitor 仓库单独重建它自己的 state。
我还跑了三条华为云 CLI 做云侧清点,怕留下孤儿资源继续计费:huaweicloud vpc list-publicips --limit 100、huaweicloud nat list-nat-gateways --limit 100、huaweicloud ecs list-servers --limit 100。整次处置从 22:17 到 23:51,用时 1 小时 34 分;误建且延迟删除的 2 个按需 EIP 和 1 个 NAT 网关,当天账单多了 ¥46.73,这个金额我直接贴进事故单,没有四舍五入。
后续我把每一处改了一下都拆成可审计修改。backend 这块我不再把 key 写死在代码里,backend "s3" {} 只留空壳,具体 key 在 CI 注入,规则是 ${CI_PROJECT_PATH_SLUG}/${TF_ENV}/main.tfstate,这样 monitor 和 prod 天然隔离,仓库名改了也不会复用旧 key。
terraform init -input=false -reconfigure \
-backend-config=bucket=team-iac-state \
-backend-config=region=cn-north-4 \
-backend-config=endpoint=obs.cn-north-4.myhuaweicloud.com \
-backend-config=key=${CI_PROJECT_PATH_SLUG}/${TF_ENV}/main.tfstate
并发控制我也补了,GitLab deploy job 增加 resource_group: terraform-prod,同一环境一次只允许一个 apply;同时把 apply 参数里的 -lock-timeout 从 300s 调到 600s,原因很具体,之前晚高峰常见 2 到 3 条流水线同时卡锁,工程师会手动重试,结果队列被挤爆。改完后一周内生产 apply 共 19 次,没有再出现并发抢写。
deploy_prod:
stage: deploy
resource_group: terraform-prod
script:
- terraform apply -auto-approve -lock-timeout=600s
第三处是前置检查脚本 scripts/check_backend_key.sh,17 行,逻辑很粗暴:如果 grep 到硬编码 key = "prod/main.tfstate" 就 exit 1 并打印修复建议。我把它挂到 MR pipeline 的 validate 阶段,必须过检才允许合并;这不是优雅方案,但它能把同类错误挡在合并前。
#!/usr/bin/env bash
set -euo pipefail
if grep -R 'key\s*=\s*"prod/main.tfstate"' -n ./*.tf ./modules/**/*.tf; then
echo 'backend key is hardcoded, use CI backend-config key=${CI_PROJECT_PATH_SLUG}/${TF_ENV}/main.tfstate'
exit 1
fi
OBS 桶我补了生命周期策略,非当前版本保留 30 天,delete marker 保留 7 天,我用测试对象做过回滚演练,第 6 天还能按 versionId 拉回,第 8 天 delete marker 才会被清理。
现在还没做完的是 state 锁,OBS 这个 s3 backend 没有原生分布式锁,我还在靠 CI 串行和人工 freeze 兜底;下周我会在预发加一条校验,要求 plan 文件同时匹配 commit SHA 与 state serial,再把 state push、state rm、import 三类高风险动作单独打到 LTS,省得下次再从聊天记录里拼时间线。
- 点赞
- 收藏
- 关注作者
评论(0)