事故是这么开始的:有人嫌 prod 那张 DynamoDB 表的预置容量不够,直接敲了两条 aws dynamodb update-table —— 把计费从 PROVISIONED 切成按需,顺手又加了个 GSI。改完测了,能用,收工。
没走 CloudFormation。
一周后,一次完全无关的发版,CloudFormation 直接卡死回滚,整个 stack 进了 UPDATE_ROLLBACK_COMPLETE,谁也不敢再点部署。错误信息就一句:Attempting to create an index which already exists。
CFN 只信自己的账本,不看现实
CloudFormation 本质是个账本:你交一份模板,它记下"这套资源应该长什么样",然后照着创建和更新。问题是,它做 diff 的时候,比的是新模板 vs 它自己的账本 —— 它从不去看云上资源实际是什么样。
所以当有人绕过它直接改了资源,账本就和现实对不上了。这就是漂移(drift)。
更阴的是回滚本身:CFN 回滚一个"加 GSI"失败的操作时,会去删掉它以为自己刚建的那个 GSI —— 而那其实是你线上正在用的索引。回滚误删。这也是为什么没人敢动:每次部署都在赌这一把。
反直觉的点:你没法 import 一个 GSI
自然的想法是:那让 CFN 重新"认领"这个带外建的 GSI 不就行了?CloudFormation 有 resource import 啊。
但这里有个坑:import 是整资源级的。你能 import 一整张表、一整个桶,但 GSI 不是一个独立资源,它是 AWS::DynamoDB::Table 的一个 property。你没法把"一张已经被 stack 管着的表里的某个子属性"单独 import 进去。
想让 CFN 接纳一个子属性的漂移,只能把整张表先踢出 stack,再整张重新 import 回来。
解法:remove → retain → reimport
核心是利用 DeletionPolicy: Retain —— 让 CFN "放手"一个资源时,不真的删掉它。
第 4 步真正写 import changeset 的时候,还有两个不写在显眼处的坑,踩过一次才知道:
aws cloudformation create-change-set \
--change-set-type IMPORT \
--template-body file://original.yaml \ # 要 Original YAML(transform 前),不是 processed JSON
--parameters "$PARAMS" \ # UsePreviousValue 把 stack 全部参数传齐
--resources-to-import '[{"ResourceType":"AWS::DynamoDB::Table",...}]'
- 参数要用
UsePreviousValue一次性传齐全部(不只是必填那几个)。少传的参数会回退成模板默认值,导致 CFN 把一堆无关资源判成"被改了",直接报 "N resources modified" 拒绝。 - 模板要用 Original YAML(transform 之前的),不是
get-template默认给的 processed JSON。processed 版本提交回去,CFN 对那些由 SAM 展开生成的资源还是会判出差异。
留给自己的几条
真正吓人的不是这套操作 —— detach 那步压根不碰 DynamoDB,表一直在服务,做坏了也能退回来。吓人的是它埋了一周才炸,而且炸在一次跟它毫无关系的发版上。
- 绕过 IaC 直接
update-table/ 改控制台,是慢性毒药。当下能用,代价记在了账本里,等下一个倒霉蛋发版时一次结清。 - CFN 的 diff 是"模板 vs 账本",永远不含"现实"。所以
detect-stack-drift值得定期跑,别等它在回滚里告诉你。 - import 是资源粒度的。子属性(GSI、参数组某项……)的漂移,认命整资源 reimport。
那张表现在回到了 CFN 名下,账本和现实对上了,detect-stack-drift 干净。下次发版,终于不用赌了。

微信
支付宝
评论
评论发布后会立即公开,如触发规则可能被审核下架。