事故はこう始まった:誰かが prod のあの DynamoDB テーブルのプロビジョンドキャパシティが足りないと思い、直接 2 本の aws dynamodb update-table を叩いた —— 課金を PROVISIONED からオンデマンドに切り替え、ついでに GSI も追加した。変更後にテストして、使えることを確認し、作業完了。
CloudFormation は通していなかった。
1 週間後、まったく無関係なリリースで、CloudFormation がそのままロールバックで固まり、stack 全体が UPDATE_ROLLBACK_COMPLETE に入り、誰も再デプロイのボタンを押せなくなった。エラーメッセージは一文だけ:Attempting to create an index which already exists。
CFN は自分の台帳だけを信じ、現実を見ない
CloudFormation の本質は台帳だ:テンプレートを渡すと、「この一式のリソースはこうあるべき」と記録し、それに従って作成・更新する。問題は、diff を取るときに比較しているのが新しいテンプレート vs 自分自身の台帳だということ —— クラウド上のリソースが実際にどうなっているかは見に行かない。
だから誰かがそれを迂回してリソースを直接変更すると、台帳と現実が一致しなくなる。これがdriftだ。
さらに厄介なのはロールバックそのものだ:CFN が「GSI を追加する」失敗した操作をロールバックするとき、自分がたった今作ったと思い込んでいるその GSI を削除しに行く —— しかしそれは実際には本番で使っているインデックスだ。ロールバックによる誤削除。だから誰も触れなかった。デプロイのたびに、この賭けをすることになるからだ。
直感に反する点:GSI を単独で import することはできない
自然に思いつくのは、では CFN にこの帯域外で作られた GSI を改めて「引き取らせれば」いいのでは、ということだ。CloudFormation には resource import がある。
だがここに落とし穴がある:import はリソース全体単位である。テーブル全体、バケット全体は import できるが、GSI は独立したリソースではなく、AWS::DynamoDB::Table の property だ。「すでに stack に管理されているテーブル内のある子属性」だけを単独で import することはできない。
CFN に子属性の drift を受け入れさせたいなら、いったんテーブル全体を stack から外し、その後テーブル全体を import し直すしかない。
解法:remove → retain → reimport
核心は DeletionPolicy: Retain を利用すること —— CFN がリソースを「手放す」ときに、実際には削除しないようにする。
第 4 ステップで実際に import changeset を書くときには、目立つ場所には書かれていない落とし穴がさらに 2 つある。一度踏んで初めて分かるものだ:
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 に触れず、テーブルはずっとサービスを提供し続け、失敗しても戻せる。怖いのはそれが 1 週間埋まったままになってから爆発し、しかもまったく関係のないリリースで爆発したことだ。
- IaC を迂回して直接
update-tableしたり、コンソールを変更したりするのは、遅効性の毒だ。その場では使えても、代償は帳簿に記録され、次の不運な誰かがリリースするときに一括精算される。 - CFN の diff は「テンプレート vs 帳簿」であり、決して「現実」を含まない。だから
detect-stack-driftは定期的に実行する価値がある。ロールバック中に教えられるまで待ってはいけない。 - import はリソース粒度だ。子属性(GSI、パラメータグループの特定項目……)のドリフトは、諦めてリソース全体を reimport するしかない。
あのテーブルはいま CFN の管理下に戻り、帳簿と現実が一致し、detect-stack-drift もクリーンだ。次のリリースでは、ようやく賭けをしなくて済む。

WeChat Pay
Alipay
コメント
コメントは即時公開されますが、ポリシー違反時は非表示になる場合があります。