一个 terraform apply 误删了整个 staging 数据库

一个 terraform apply 误删了整个 staging 数据库

一次看似普通的 terraform apply,最终执行了 -1 to destroy,把 staging 环境的托管数据库整个删掉。操作者只改了两个资源的标签,计划里也只显示 ~2 to change,却没注意到上方静静躺着的销毁记录。

这个事故听起来极端,却在 Terraform 用户中并不罕见。Terraform 把资源名称当作唯一标识,一旦模块里有人改了 resource 的 name,整个对象身份就变了:旧实例要被删除,新实例要被创建。执行者如果只扫一眼变更数量,很容易错过致命的 destroy 操作。

国内团队使用阿里云 RDS、腾讯云 TDSQL 或华为云 RDS 时,类似故事反复上演。许多公司把 Terraform 当作基础设施即代码的唯一入口,却没有建立起对计划输出的严格审查机制。

事故完整经过:从改标签到数据库消失

事情发生在一次常规的配置更新。工程师需要给两台虚拟机打上新的环境标签,以便更好地做成本分摊。他打开了对应的 Terraform 模块,修改了 tags 参数,运行 terraform plan。

计划输出显示「2 to change」,看起来安全。他快速浏览后输入 yes。实际执行时 Terraform 先删除了旧的托管数据库实例,再按新配置重建。整个过程在几分钟内完成,staging 环境直接失去数据库。

事后复盘发现,模块维护者在前一次重构中把数据库资源的名称从 db-staging 改成了 db-staging-v2。这个改动让 Terraform 认为原来的实例已经「不存在」,必须销毁。工程师在审查计划时只关注了修改数量,没有滚动到计划文件顶部去看 destroy 部分。

整个事故从按下 yes 到数据库无法恢复,只用了不到十分钟。备份虽然存在,但恢复窗口超过了业务允许的停机时间,导致测试数据全部丢失。

根本原因:Terraform 把名称当作资源身份

Terraform 的核心设计理念是声明式。用户写出期望的状态,Terraform 负责把现实世界调整到匹配状态。而资源块的地址(resource type + name)就是这个声明里的唯一键。

当模块开发者把 resource “alicloud_db_instance” “staging” 改成 resource “alicloud_db_instance” “staging_new” 时,Terraform 状态文件中旧的地址就失效了。它会把旧实例标记为需要删除,同时把新地址标记为需要创建。这不是 bug,而是 Terraform 按设计行事的结果。

国内很多团队在模块复用时频繁重构名称,却很少同步更新状态迁移计划。另一个常见问题是多人协作时,没有强制代码审查环节检查资源名称变更。结果就是一次普通的标签修改,背后却藏着数据库重建的炸弹。

国内云厂商环境下 IaC 变更管理的现实挑战

阿里云、腾讯云和华为云都提供了 Terraform Provider,但它们的资源生命周期管理细节各有不同。以阿里云 RDS 为例,删除实例的操作默认会保留最后备份,但恢复实例需要手动指定备份集,耗时较长。腾讯云的数据库实例删除后进入回收站,保留期通常只有 7 天,超过即彻底清除。

这些机制让「误删」代价更高。很多团队把 Terraform 跑在本地开发者机器上,没有统一的 CI/CD 流水线,导致 plan 和 apply 的执行环境不一致,计划输出也难以留存审计。

更普遍的问题是缺少变更窗口管理。生产和 staging 环境共用一套模块,改一个参数可能同时影响多个环境,却没有环境隔离的机制。

如何用状态迁移和 import 避免无谓销毁

Terraform 提供了 terraform state mv 命令来解决名称变更问题。在重构模块前,先把旧资源地址移动到新地址,就能让 Terraform 认为这是同一个对象。

具体做法是:先运行 terraform state list 找到当前资源地址,再用 terraform state mv old_address new_address。移动完成后重新 plan,应该不再出现 destroy。

对于已经存在的云上资源,也可以用 terraform import 把它们拉进状态文件。阿里云 RDS 的 import 格式通常是实例 ID,命令类似 terraform import alicloud_db_instance.staging rm-xxx。import 之后再调整配置,最后用 terraform apply 只做 in-place 更新。

这些操作虽然能解决问题,但需要团队养成「任何名称变更必须先迁移状态」的习惯。建议把这个步骤写进代码审查 checklist。

备份策略与快速恢复机制的落地建议

单纯依赖云厂商默认备份远远不够。建议对 staging 环境也开启自动备份,且备份保留时间至少 30 天。阿里云 RDS 支持设置备份周期和保留天数,推荐每天备份并开启日志备份,以便实现秒级恢复。

更进一步可以引入快照策略。华为云支持给 RDS 实例创建手动快照,腾讯云则提供 DBS 数据库备份服务,可以跨地域备份。团队应该把创建快照的步骤也纳入 Terraform,但要用独立的模块,避免和主实例放在一起导致连锁删除。

恢复演练必须定期做。每个季度至少进行一次「从备份恢复整个 staging 环境」的演练,记录耗时和数据一致性。只有当恢复时间稳定在 15 分钟以内,才能说备份策略真正有效。

多层防护:plan 审查、审批流和破坏性操作锁定

第一道防线是强制 plan 审查。把 terraform plan 输出保存为 artifact,要求至少两人签字确认无 destroy 操作后再 apply。GitHub Actions 或阿里云 DevOps 流水线都可以轻松实现这个流程。

第二道防线是使用 Terraform Cloud 或开源的 Atlantis,在 pull request 中直接展示 plan,并要求特定标签(如 safe-to-apply)才能合并。

第三道防线是对破坏性操作加保护。可以在模块里用 lifecycle { prevent_destroy = true } 保护核心数据库资源。这样即使计划里出现 destroy,Terraform 也会直接报错拒绝执行。

对于云上资源,还可以配合 RAM 权限最小化原则,给运行 Terraform 的角色只授予必要权限,不允许直接 DeleteDBInstance。所有删除操作必须走工单或审批流。

建立变更管理文化,把事故变成团队能力

这个事故的真正教训不是「要仔细看 plan」,而是需要系统性地把 IaC 变更当作生产变更来管理。国内很多中大型团队已经开始采用 GitOps 方式管理基础设施,把所有 Terraform 代码放在 Git 仓库,通过流水线执行。

建议从以下几点开始改进:1) 所有模块变更必须经过 peer review,重点检查 resource name 和 count/for_each 的改动;2) 引入 terraform validate 和 tflint 做静态检查;3) 对所有数据库资源强制添加 prevent_destroy,并在 CI 中验证;4) 建立统一的 Terraform 状态后端,使用 S3 或阿里云 OSS 加 DynamoDB 锁,避免状态冲突。

当团队把「一次 apply 可能删库」当作默认假设来设计流程时,事故发生的概率会大幅下降。Terraform 本身足够强大,问题通常出在围绕它的流程和习惯上。

最后提醒:下次看到 terraform plan 里有任何 - to destroy,哪怕只有一行,也请完整读完整个输出。那个安静的 -1 可能就是下一个 staging 数据库。

参考来源