🔨

Expand and Contract パターンによる破壊的変更の段階的適用

に公開

1. Expand and Contract パターンとは?

Expand and Contract パターン(Parallel Changeとも呼ばれる)は、システムの破壊的な変更を、ダウンタイムなしに安全に実装するための手法です。REST APIやDBスキーマなどの破壊的変更を伴う場合に有効です。

このパターンは、変更を次の3つのフェーズに分けて実行します。

  1. 拡張 (Expand) フェーズ
    • 古い構造を壊さずに、新しい構造をシステムに導入します。
  2. 移行 (Migrate) フェーズ
    • 既存のデータやクライアントを新しい構造に移行します。
  3. 収縮 (Contract) フェーズ
    • 古い構造にアクセスするものがなくなったことを確認した後、古い構造を削除します。

この手法は、REST API、DBスキーマなど、さまざまなインターフェースの変更に適用可能です。

2. 従来手法(ビッグバン)のリスク

破壊的な変更を一度に適用するアプローチは「ビッグバン」デプロイメントと呼ばれます。この手法は、以下のリスクを伴います。

  • ダウンタイムの発生
    • データ移行など時間のかかる作業中、システムへのアクセスを一時的に遮断する必要があります。
  • 高リスクでありロールバックが困難
    • すべての変更を一度に実施するため、予期せぬ問題が発生するリスクが非常に高くなります。問題発生時のロールバックは、複雑かつ時間がかかる作業になりがちです。

3. Expand and Contract パターンによる解決

このパターンは、変更を独立したステップに分割することで、上記のリスクを解消し安全性と可用性を向上することができます。

  • ゼロダウンタイムデプロイ
    • システムを停止させることなく、互換性のない変更をデプロイできます。新旧のバージョンが一時的に並行稼働しても、システム全体の整合性が維持されます。
  • 安全なロールバック
    • ほとんどの段階で、直前の状態との後方互換性が保たれます。問題が発生した場合、直前の状態にロールバックすることができます。

4. 具体的な手順:DBの移行

このパターンをDBの変更に適用する場合、変更をいくつかのデプロイステップに分けます。

以下の例では、顧客テーブルの Name カラムを FirstNameLastName に分割する、破壊的スキーマ変更を解説します。

【前提:変更前の初期状態】

コードと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  テックブログ

Discussion