Expand and Contract パターンによる破壊的変更の段階的適用
1. Expand and Contract パターンとは?
Expand and Contract パターン(Parallel Changeとも呼ばれる)は、システムの破壊的な変更を、ダウンタイムなしに安全に実装するための手法です。REST APIやDBスキーマなどの破壊的変更を伴う場合に有効です。
このパターンは、変更を次の3つのフェーズに分けて実行します。
- 拡張 (Expand) フェーズ
- 古い構造を壊さずに、新しい構造をシステムに導入します。
- 移行 (Migrate) フェーズ
- 既存のデータやクライアントを新しい構造に移行します。
- 収縮 (Contract) フェーズ
- 古い構造にアクセスするものがなくなったことを確認した後、古い構造を削除します。
この手法は、REST API、DBスキーマなど、さまざまなインターフェースの変更に適用可能です。
2. 従来手法(ビッグバン)のリスク
破壊的な変更を一度に適用するアプローチは「ビッグバン」デプロイメントと呼ばれます。この手法は、以下のリスクを伴います。
- ダウンタイムの発生
- データ移行など時間のかかる作業中、システムへのアクセスを一時的に遮断する必要があります。
- 高リスクでありロールバックが困難
- すべての変更を一度に実施するため、予期せぬ問題が発生するリスクが非常に高くなります。問題発生時のロールバックは、複雑かつ時間がかかる作業になりがちです。
3. Expand and Contract パターンによる解決
このパターンは、変更を独立したステップに分割することで、上記のリスクを解消し安全性と可用性を向上することができます。
- ゼロダウンタイムデプロイ
- システムを停止させることなく、互換性のない変更をデプロイできます。新旧のバージョンが一時的に並行稼働しても、システム全体の整合性が維持されます。
- 安全なロールバック
- ほとんどの段階で、直前の状態との後方互換性が保たれます。問題が発生した場合、直前の状態にロールバックすることができます。
4. 具体的な手順:DBの移行
このパターンをDBの変更に適用する場合、変更をいくつかのデプロイステップに分けます。
以下の例では、顧客テーブルの Name カラムを FirstName と LastName に分割する、破壊的スキーマ変更を解説します。
【前提:変更前の初期状態】
コードとDBスキーマは、旧構造(例:単一の Name カラム)での運用に完全に依存しています。この状態から、ダウンタイムなしに破壊的なスキーマ変更を実装します。

【拡張 (Expand) フェーズ】
新しい構造を導入し、データの二重書き込みを開始します。
| ステップ | アクション | 目的/備考 |
|---|---|---|
| Step 1 | 新構造の導入と二重書き込みの開始 | 新しいカラム(例: FirstName, LastName)を導入します。アプリケーションは新旧両方に書き込みますが、読み込みは旧構造から行います。 |
| Step 2 | 既存データの新構造への移行 | 既存のすべてのデータを旧構造から新構造へ移行します。データの一貫性を保つため、Step 1と同じロジックで移行コードを作成する必要があります。 |

【移行 (Migrate) フェーズ】
読み込みを新しい構造に切り替えます。
| ステップ | アクション | 目的/備考 |
|---|---|---|
| Step 3 | 新構造での運用開始(読み込みの切り替え) | データの読み込み元を新構造に切り替えます。この段階で、新構造が意図通りに機能するか、E2Eのテストを行います。ロールバックの選択肢を残すため、旧構造への書き込みは継続します。 |

【収縮 (Contract) フェーズ】
古い構造を削除します。
| ステップ | アクション | 目的/備考 |
|---|---|---|
| Step 4 | 旧構造への書き込みの停止 | システムを収縮させる最初のステップとして、旧構造への書き込みを停止します。これ以降、ロールバックはデータのバックアップからの復元を必要とし、データ損失を伴う可能性があるため、注意が必要です。 |
| Step 5 | 旧構造の削除 | Step 4がサーバーに展開された後、DBから旧構造を削除します。 |

5. API移行への応用
このパターンは、DBの移行だけでなく、公開APIの破壊的変更にも同様に適用できます。
ここでは、DB移行が完了した直後の状態から、クライアントが利用するAPIをリファクタリングする例を考えます。
【前提:DBはすでに移行が完了している状態】

【拡張 (Expand) フェーズ】
| ステップ | アクション | クライアントの使用状況 | 目的/備考 |
|---|---|---|---|
| Step 1 | 新しいAPIの導入 | 旧API (/user/v1) を引き続き使用。 |
変更後の構造に対応した新API (/user/v2) を導入しデプロイします。旧APIへのアクセスは引き続き維持します。 |

【移行 (Migrate) フェーズ】
| ステップ | アクション | クライアントの使用状況 | 目的/備考 |
|---|---|---|---|
| Step 2 | クライアントを新APIへ移行 | クライアントを徐々に /user/v1 から /user/v2 へ更新します。 |
クライアント側で新しいAPIへ移行します。サーバー側では、引き続き両方のAPIをサポートします。監視により、旧APIのトラフィックが無視できるレベルになるのを待ちます。 |

【収縮 (Contract) フェーズ】
| ステップ | アクション | クライアントの使用状況 | 目的/備考 |
|---|---|---|---|
| Step 3 | 旧APIの削除 | 全てのクライアントが /user/v2 を使用。 |
旧API(/user/v1) へのアクセスが完全に停止したことを確認した後、旧API関連のコードを削除します。 |

このように適用することで、外部クライアントのデプロイ状況に関わらず、ダウンタイムなしにAPIとDBの両方に破壊的変更を加えることが可能になります。
6. トレードオフ
Expand and Contract パターンは、システムを停止させずに変更を適用するためのプラクティスです。
しかし、このパターンには以下のトレードオフが存在します。
| 欠点 | 必要な対策 |
|---|---|
|
リリースまでの時間 小さな変更でも、すべてのステップを完了するのに時間がかかる。 |
デプロイ頻度を高め、一連の変更を素早く適用しきる体制を作る。 |
|
一時的な負荷増加 拡張期間中、データの冗長化、書き込み操作の増加、移行プロセスによる負荷増加が発生する。 |
一時的なコストとして許容する。 |
|
工数の増加 手順が多く、各デプロイステップを個別に実行および管理する必要があるため、工数が増加する。 |
コストとして許容し、ゼロダウンタイムでリリースできるメリットとトレードオフであることを認識する。 |
|
中途半端な放置 移行のメリットは早期に得られやすいため、最後のクリーンアップの優先度が下がりやすい。その結果、新旧のコードが混在したままとなり、メンテナンスコストの高い「技術的負債」として残り続ける。 |
機能的なメリットが得られた後も、そこで満足せず、最後の不要なコード削除まで実施することをルールとする。 |
Expand and Contract パターンは、小さく安全な変更を積み重ねていく地道なアプローチです。この手順を守り続けることで、変化に強く安定したシステムを築くことができます。
参考情報
私たち BABY JOB は、子育てを取り巻く社会のあり方を変え、「すべての人が子育てを楽しいと思える社会」の実現を目指すスタートアップ企業です。圧倒的なぬくもりと当事者意識をもって、子どもと向き合う時間、そして心のゆとりが生まれるサービスを創出します。baby-job.co.jp/
Discussion