データベースマイグレーションの切り替えフェーズ:クリーンな移行を実現する方法

次のシナリオを想像してください。あなたのチームは数週間かけて、古いデータベーススキーマから新しいスキーマへデータを慎重に移行してきました。Expand-Contractパターンは順調に動作しています。アプリケーションは新旧両方の構造に書き込みを行い、バックフィルスクリプトで履歴データも移行済みです。検証チェックもすべてパスしています。書類上は完璧に見えます。

しかし、ここからが多くのエンジニアを緊張させる瞬間です。アプリケーションを新しい構造からのみ読み取るように切り替える——これがカットオーバーフェーズであり、データベースマイグレーションが成功するか、予期せぬ本番インシデントを引き起こすかの分かれ目です。

カットオーバーとは何か

カットオーバーとは、アプリケーションが古い構造からの読み取りを停止し、新しい構造に完全に依存する時点を指します。単純に聞こえますが、実際には慎重な調整と検証が必要です。

カットオーバー前、アプリケーションはデュアルリード状態にあります。マイグレーション開始以降、新しいデータは両方の構造に書き込まれています。履歴データはバックフィル済みです。アプリケーションは2つの場所から読み取っています。マイグレーション前に存在したデータは古い構造から、その後書き込まれたデータは新しい構造からです。

カットオーバーはこのデュアルリードロジックを排除します。すべての読み取りリクエストが新しい構造のみに向かうようにアプリケーションコードを変更します。これは通常、コード変更とデプロイであり、スキーマ変更ではありません。読み取りパスを更新し、アプリケーションをビルドし、他の機能更新と同様にデプロイします。

次のシーケンス図は、デュアルリードからカットオーバーへの移行を示しています。

以下は、Node.jsサービスでのコード変更の簡略化した例です。

// カットオーバー前:デュアルリードロジック
async function getUserProfile(userId) {
  // 新しい構造を優先して確認
  const newProfile = await db.query(
    'SELECT * FROM user_profiles_v2 WHERE user_id = $1', [userId]
  );
  if (newProfile.rows.length > 0) {
    return newProfile.rows[0];
  }
  // 古い構造にフォールバック
  const oldProfile = await db.query(
    'SELECT * FROM user_profiles WHERE user_id = $1', [userId]
  );
  return oldProfile.rows[0] || null;
}

// カットオーバー後:シングルリードロジック
async function getUserProfile(userId) {
  const profile = await db.query(
    'SELECT * FROM user_profiles_v2 WHERE user_id = $1', [userId]
  );
  return profile.rows[0] || null;
}
sequenceDiagram participant OldApp as 旧アプリ participant NewApp as 新アプリ participant OldDB as データベース(旧構造) participant NewDB as データベース(新構造) Note over OldApp,NewDB: カットオーバー前 OldApp->>OldDB: 読み取り NewApp->>OldDB: 読み取り(旧データ) NewApp->>NewDB: 読み取り(新データ) Note over OldApp,NewDB: カットオーバー発生 NewApp->>NewDB: 読み取りのみ Note over OldApp: 旧アプリは退役

無視できないリスク

カットオーバー時の危険は部分的な障害です。一部のアプリケーションインスタンスがまだ古い構造から読み取り、他のインスタンスが切り替わっている場合、不整合な結果が生じる可能性があります。ユーザーはリクエストを処理するサーバーによって異なるデータを目にするかもしれません。

だからこそ、カットオーバー戦略が重要です。主に2つのアプローチがあります。

ビッグバンカットオーバーは、すべてのインスタンスを一度に切り替えます。迅速で調整も簡単ですが、問題が発生した場合、すべてのユーザーが即座に影響を受けます。

段階的カットオーバーは、インスタンスをリージョン、アベイラビリティゾーン、またはユーザーグループごとにバッチで切り替えます。これにより影響範囲が限定されます。1つのアベイラビリティゾーンを切り替えてエラーが発生した場合、次のゾーンに進む前に調査できます。トレードオフは、調整と監視の複雑さが増すことです。

本番経験のあるほとんどのチームは、データベースマイグレーションでは段階的カットオーバーを好みます。追加の調整作業は、セーフティネットに見合う価値があります。

隠れた依存関係の発見

カットオーバー後、アプリケーションは古い構造から読み取らなくなります。しかし、本当にそれだけでしょうか?本番環境では、複数のサービス、バッチジョブ、レポートスクリプト、手動クエリが同じデータベースを共有していることがよくあります。

よくある間違いは、メインアプリケーションの更新だけで十分だと考えることです。その間に、午前2時に実行される夜間バッチジョブがまだ古いカラムをクエリしていたり、データアナリストが毎週月曜日に古いテーブルを使った手動レポートを実行していたりします。これらの隠れた依存関係は、カットオーバー後に障害を引き起こしたり、古いデータを生成したりします。

これらを発見する最も信頼できる方法は、データベースクエリの監視です。PostgreSQLのpg_stat_statementsやMySQLのperformance_schemaなどのツールを有効にします。古いカラムやテーブルを参照するクエリを探します。この監視は、既知のすべてのプロセスの少なくとも1サイクル分実行します。週次レポートジョブがある場合は、カットオーバー後1週間待ってから古い構造が未使用であることを宣言します。

一部のチームは、ステージング環境で古いカラムへの読み取りアクセスを無効にし、既知のすべてのアプリケーションシナリオを実行するテストも行います。エラーが発生しなければ、本番環境は次のステップに進んでも安全である可能性が高いです。

カットオーバー後の対応

カットオーバーが完了し、すべての依存関係がクリーンであることが確認されたら、アプリケーションは完全に新しい形式に移行しています。古い構造はデータベースにまだ存在しますが、誰も読み取っていません。ここから、コントラクトフェーズ(古い構造を完全に削除する)の計画を開始できます。

ただし、削除を急いではいけません。カットオーバー後も、安全期間として古い構造を維持します。期間はチームの自信と、余分なカラムやテーブルを保持するコストによって異なります。1週間保持するチームもいれば、特に重要な顧客データが関係するマイグレーションでは1ヶ月保持するチームもいます。

この期間中は、未発見の依存関係を示す可能性のあるアラートやエラーログを監視します。何もなければ、古い構造を削除しても問題ありません。

カットオーバーの実践的チェックリスト

本番環境でカットオーバーを実行する前に、以下のチェックリストを確認してください。

  • すべての履歴データがバックフィルされ、検証済みである
  • デュアルライトが少なくとも1ビジネスサイクル以上エラーなく動作している
  • 読み取りパスのコード変更が準備され、レビュー済みである
  • ロールバック計画が文書化されている:必要に応じて古い構造への読み取りを戻す方法
  • データベースクエリ監視が有効で、古い構造のクエリを検出する設定がされている
  • 既知の依存アプリケーション、バッチジョブ、レポートがすべて特定され、更新済みである
  • ステージング環境で古い構造の読み取りアクセスを無効にしたテストが実施済みである
  • 段階的カットオーバー計画が定義されている:どのインスタンスまたはリージョンを最初に切り替えるか
  • カットオーバー後の読み取りエラーやデータ不整合を検出する監視ダッシュボードが設定されている
  • コミュニケーション計画が準備されている:誰がカットオーバーについて知る必要があるか、いつ通知するか

まとめ

カットオーバーは、マイグレーションパターンが理論から現実のユーザー影響へと変わる瞬間です。単なるコード変更として扱わないでください。監視、検証、明確なロールバック経路を必要とする本番イベントとして扱いましょう。隠れた依存関係が存在しないことを確認するために費やす追加の1日は、古い構造を急いで削除することで節約できる1時間よりもはるかに価値があります。