ゼロダウンタイムマイグレーション は何をしますか?
概要
ローリングデプロイ中にシステムを稼働させたままスキーマ変更を行うための規律です。ロールアウト中は、古いPodと新しいPodが 1つのデータベースを共有 します。そのため、スキーマは 両方のアプリバージョンと同時に互換性 がなければなりません。破壊的なマイグレーションを、それを必要とするデプロイと一緒に実行することは、古典的なセルフダウンタイムの原因です。
作成ルール(全プロジェクト共通)
- 2種類のマイグレーションを明確に区別する: 冪等性を持つ
current/セット(開発時のイテレーション用 — 毎サイクル再実行可能、CREATE OR REPLACE/IF NOT EXISTSを使用、オブジェクトは 最終形 で記述、作成後のALTERは不可)と コミット済み のマイグレーション(不変、順序付け、実際に出荷されるもの)。ALTER DEFAULT PRIVILEGESのみ、インラインALTERとして許容されます。 - 所有権はマイグレーションの実行方法に基づく: 散在する
ALTER ... OWNERは使用しません。 - 単一のブートストラップ情報源 をローカル/Dockerの初期化パスとシャドウ/テストDBで共有する — すべての環境が同一に構築されます。
Expand/Contract(パラレルチェンジ)
適用条件: データベースにデプロイ後も保持すべきデータが存在する場合(ステージング+実データ、本番)。対象外: ローカル/リセット可能なプロジェクト — ただし、コミット済みマイグレーションは あたかもこのルールが適用されるかのように 記述し、プロモーション時に書き換えが発生しないようにします。
スキーマ変更は 3つのリリース に分割して実行します。1回のリリースでは行いません。
- Expand — 新しい形状を 追加的に、後方互換性を持って追加する:新しいNULL許容カラム、新しいテーブル、新しい関数。アプリバージョン N はそのまま稼働し続けます。
- Migrate — 境界のあるバッチ でバックフィルを実行する(テーブル全体をロックする
UPDATEは避ける)。必要に応じてアプリ N+1 からデュアルライトを行う。読み取りを新しい形状に切り替える。NOT NULLはこのタイミングでのみ追加する(制約はNOT VALIDで追加し、その後VALIDATEを別途実行する)。 - Contract — アプリ N が完全に退役した 後 に、後の リリースで古いカラムや制約を削除する。
PostgreSQLの落とし穴
| 落とし穴 | 安全なアプローチ |
|---|---|
揮発性/大規模なバックフィルデフォルト値を持つ NOT NULL カラム | NULL許容で追加 → バッチでバックフィル → 制約 NOT VALID を追加 → VALIDATE |
| 制約の追加 | 最初に NOT VALID を追加し、その後 VALIDATE CONSTRAINT を実行する(弱いロック) |
| インデックスの構築 | CREATE INDEX CONCURRENTLY — トランザクション内では実行できません。マイグレーションツールのトランザクションラッピングから隔離する |
| ブロックされたマイグレーションによる本番停止 | lock_timeout / statement_timeout を設定し、迅速に失敗させる |
| カラムのリネーム | Expand/Contractパターン:新しいカラムを追加 → バックフィル → 読み取りを切り替え → 古いカラムを削除(リリースを分ける) |
「最終形の CREATE OR REPLACE」が本番では不十分な理由
そのルールは 開発者のエルゴノミクス であり、開発環境では正しいものです。しかし、実ユーザーが存在しローリングデプロイを行う本番環境では、ロールアウト中は両方のアプリバージョンがスキーマを共有するため、Expand/Contract こそが変更を安全にする方法です。両者を混同してはいけません。
詳細な根拠: 憲法第17条、engineering-constitution/references/engineering-constitution.md にバンドルされています。デプロイパイプライン: cloud-delivery-aks。