生产环境 Terraform 误删 RDS 实例,我用快照 + 时间点恢复 30 分钟完成抢救
生产环境 Terraform 误删 RDS 实例,我用快照 + 时间点恢复 30 分钟完成抢救
上周三下午,我正喝着咖啡,钉钉突然炸了。DBA 在群里发了一句:「生产订单库的 RDS 实例不见了。」
我当时一口咖啡差点喷出来。查了一圈,最后发现是 Terraform apply 的时候有人手滑把 prevent_destroy 从 true 改成了 false,然后顺手 terraform destroy 了一下。 order_db 就这么没了。就 30 秒的事,数据库蒸发。
说实话,这是我职业生涯里离事故最近的一次。好在最后 30 分钟就把数据全恢复了,订单业务没受什么影响。今天把这个完整的时间线和恢复方案写一下,万一有人也踩这个坑,能照着急救。
事故现场:15 分钟内发生了什么
14:32,DBA 的监控告警开始炸。RDS 连接数直接归零,应用层疯狂报 Connection refused。
14:35,我们发现 order_db 的实例在 AWS 控制台里已经显示 deleting 状态。不是重启,是正在删除。有人手动执行了 terraform destroy,目标资源正是 aws_db_instance.order_db。
14:40,拉了一下 Terraform 的 state 文件,确认资源已经被标记为 destroyed。同时开始查最近的可恢复时间点:RDS 自动备份是每天 02:00,每小时有一个快照。万幸的是,自动备份策略一直开着,而且 deletion protection 原本也设了,只是不知道谁在上次 PR 里把它关了。
从发现到开始恢复,整个过程 8 分钟。这 8 分钟里我们做了三件事:
- 立刻停掉所有可能写入的 Job(避免恢复后数据不一致)
- 确认 RDS 自动快照列表,找到最近可用快照
- 通知业务方暂停下单,切换只读降级模式
恢复方案:为什么选快照 + 时间点恢复,而不是 read replica
当时脑子里闪过了三个方案。
方案 A:从 read replica 提升为主库。 我们确实有 read replica,但问题是主库被删除的时候,read replica 也受到了级联影响,状态变成 inaccessible。这个方案直接放弃。
方案 B:从手动快照恢复。 最近一次手动快照是三天前的,数据丢失量太大,接受不 了。而且手动快照恢复出来的是一个新实例,需要改应用连接串,还要处理 binlog 位点。
方案 C:从自动快照 + 时间点恢复(Point-in-Time Recovery, PITR)。 这是 RDS 的自带能力。自动备份会保留最近 7 天的数据,加上事务日志,可以恢复到任意一个可恢复时间点(精确到秒)。
我们选了方案 C。原因很现实:
- 数据丢失量最小:可以恢复到删除前 5 分钟
- 操作最简单:AWS 控制台点几下就能启动,或者用 CLI 一条命令
- 恢复出来的实例 ID 变了,但端点可以重新绑定到同一个 Route53 记录
实操:30 分钟恢复时间线的每一步
下面这条时间线,是我们事后复盘整理的。实际做的时候其实更紧张,有些步骤是并行推进的。
14:45 - 定位可恢复时间点
aws rds describe-db-snapshots \
--snapshot-type automated \
--db-instance-identifier order-db \
--query 'DBSnapshots[0].{SnapshotCreateTime:SnapshotCreateTime,DBSnapshotIdentifier:DBSnapshotIdentifier}'
输出里最新的自动快照是当天 14:00 的。加上事务日志,可以恢复到 14:30 左右(事故发生前 2 分钟)。
14:48 - 启动时间点恢复
AWS CLI 比控制台快,脚本先写好,参数改一下就行:
aws rds restore-db-instance-to-point-in-time \
--source-db-instance-identifier order-db \
--target-db-instance-identifier order-db-recovered \
--restore-time 2026-06-17T14:30:00Z \
--db-instance-class db.r6g.large \
--vpc-security-group-ids sg-xxxxx \
--no-publicly-accessible
restore-time 填的是 UTC 时间,要确认时区。我们设的是 14:30(UTC+8 也就是 06:30 UTC),比实际删除时间早了 2 分钟,确保不会被删除动作污染。
target-db-instance-identifier 故意加了个 -recovered 后缀,防止跟原实例名冲突。后面再改回来。
14:52 - 等恢复实例可用
RDS 恢复不是瞬间完成的。creating → modifying → available,整个过程大概 15-20 分钟。这段时间里我们干了三件事:
- 预热连接池:把应用连接串先改到一个临时只读库,保证查询不走丢
- 检查恢复参数:确认
backup_retention_period、multi_az、deletion_protection这些参数在新实例上都重新配置好 - 通知下游:BI 数仓、消息队列消费者的延迟写入计划
等状态变成 available:
aws rds wait db-instance-available --db-instance-identifier order-db-recovered
15:05 - 参数对齐 + 安全加固
恢复出来的实例默认继承的是自动快照的原始参数,但有几个地方必须手工改:
- 安全组:临时实例用的是手动指定的 SG,需要切回生产 SG
- 参数组:如果原库用了自定义参数组(比如
max_connections、innodb_buffer_pool_size),需要手动关联 - Deletion Protection:必须重新打开,而且写进 Terraform,确保下次不会再被误删
aws rds modify-db-instance \
--db-instance-identifier order-db-recovered \
--deletion-protection \
--vpc-security-group-ids sg-prod-xxxxx \
--db-parameter-group-name order-db-param-group \
--apply-immediately
15:10 - 切回生产端点
我们是通过 Route53 的 CNAME 记录指向 RDS 端点的。所以恢复后只需要把 DNS 记录改到新实例的端点,然后清一下本地缓存。
# 更新 Route53 记录
aws route53 change-resource-record-sets \
--hosted-zone-id ZXXXXXXXXXXX \
--change-batch file://update-order-db.json
update-order-db.json 内容:
{
"Changes": [{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "order-db.internal",
"Type": "CNAME",
"TTL": 60,
"ResourceRecords": [{"Value": "order-db-recovered.xxxxx.us-east-1.rds.amazonaws.com"}]
}
}]
}
TTL 设 60 秒,让客户端尽快刷新。15:12 左右,应用日志里开始出现正常连接。
15:15 - 验证数据完整性
恢复不是结束。我们跑了三条验证:
- 行数对比:对几个核心表做
COUNT(*),跟删除前的监控指标对比,误差在千分之一以内 - 最近订单抽检:查
created_at >= '2026-06-17 14:00'的订单,随机抽 20 条核对金额和状态 - 上下游一致性:对比订单中心跟库存、支付系统的流水,确保没有孤单或重复
三条全过,才算恢复完成。这时候群里才敢发一句:「恢复了。」
事后加固:怎么避免下次再被删
这次能 30 分钟恢复,说实话有很大运气成分。如果自动备份没开,或者快照保留期只有 1 天,那就不是 30 分钟的事了。
所以事后我们做了五件事,现在写进了运维 checklist,每季度 review:
1. Terraform 强制保护
resource "aws_db_instance" "order_db" {
identifier = "order-db"
deletion_protection = true # 绝对不能关
skip_final_snapshot = false # 删除时强制生成最终快照
final_snapshot_identifier = "order-db-final-snapshot"
lifecycle {
prevent_destroy = true # 双重保险
}
}
prevent_destroy 这次就是被人手动改了。后来我们在 CI 里加了 policy check:
grep -n "prevent_destroy\s*=\s*false" terraform/**/*.tf && exit 1
谁敢把 prevent_destroy 改成 false,PR 直接挂。
2. 备份策略升级
- 自动备份保留期:从 7 天升级到 35 天(AWS 支持的最长自动备份周期)
- 跨区域快照复制:每天自动复制一份快照到另一个 Region
- 手动快照:每周做一次手动快照,保留 30 天
aws rds modify-db-instance \
--db-instance-identifier order-db \
--backup-retention-period 35 \
--apply-immediately
3. 恢复演练常态化
以前觉得恢复演练是走流程。现在每两个月做一次真实恢复演练:从自动快照恢复一个新实例,跑验证脚本,确认数据完整,然后销毁。演练结果写入 Confluence。
4. 最小权限原则
terraform destroy 能直接删掉生产库,说明权限太大了。后来 IAM policy 改了:
- 生产环境的
rds:DeleteDBInstance只允许通过特定 Role 执行 - 这个 Role 需要双重审批(MFA + 工单系统)
- 日常 CI/CD 用的 IAM 角色压根没有删除权限
5. 告警升级
RDS 删除动作本身是有 CloudTrail 事件的。我们补了一条告警:
aws logs create-log-group --log-group-name /rds/deletion-alerts
只要出现 DeleteDBInstance 事件,15 秒内 P0 告警直接打到值班手机和钉钉。不是事后查日志,是实时告警。
踩坑记录
这次恢复过程中,有几个坑实打实地踩了,写出来提醒自己:
坑 1:恢复时间参数填错时区
第一次填 --restore-time 的时候写的是 2026-06-17T14:30:00,结果 AWS 默认当成 UTC 了。实际想恢复的是 UTC+8 的 14:30,也就是 2026-06-17T06:30:00Z。差 8 小时,数据直接丢了 8 小时。还好发现得早,重新跑了一次。
坑 2:恢复出来的实例没有原实例的自定义参数
恢复后应用连上去,马上报 too many connections。查了一下,恢复实例用的默认参数组 max_connections=100,原实例用的是自定义参数组,配了 1000。这个参数没继承,需要手动绑定。
坑 3:安全组临时切错
恢复脚本里 --vpc-security-group-ids 一开始写成了测试环境的 SG,结果应用连不上。检查了好一会儿才发现是 SG 没放行生产网段。现在脚本里 SG ID 用变量,执行前强制 echo 确认。
坑 4:read replica 不是救命稻草
前面说了,read replica 在 primary 被删除的时候可能也会受影响。如果依赖 read replica 作为灾备方案,需要确认 delete_automated_backups 的行为,以及 replica 的独立备份策略。
坑 5:Terraform state 里的资源标记
terraform destroy 之后,state 文件里资源被标记为 destroyed。但 AWS 上恢复出来的新实例 Terraform 是不知道的。需要手动 terraform state rm 然后重新 import,不然下次 apply 可能又试图创建一个冲突的实例。
terraform state rm aws_db_instance.order_db
terraform import aws_db_instance.order_db order-db-recovered
写在最后
这次事故让我明白一个道理:备份不是目的,能恢复才是目的。
很多人以为开了自动备份就万事大吉,但真到出事的 时候,你能不能 30 分钟内找到恢复窗口、能不能准确定位快照、能不能把应用连到新实例,这些才是真正的考验。
我的建议是:别等出事再演练。找个周末,从自动备份恢复一个新实例,走一遍完整流程。你会发现很多平时不会注意的问题——时区、参数组、安全组、连接池、下游依赖。这些问题在演练里解决,总比在生产环境里摸索强。
最后,如果你的 Terraform 里还没给 RDS 加 prevent_destroy 和 deletion_protection,现在就去加。真的。
有问题欢迎评论区交流。如果你也经历过类似的事故,我很想听听你是怎么恢复的。
更多推荐
所有评论(0)