データベースマイグレーションが失敗したとき:ロールバック vs ロールフォワード
あなたのチームが本番環境でデータベースマイグレーションを実行した。5分後、モニタリングダッシュボードが真っ赤に染まる。エラー率が急上昇し、ユーザーから障害報告が相次ぐ。今すぐ決断を下さなければならない:変更を元に戻すのか、それとも別の修正を加えて前進するのか?
この瞬間が、計画を持っているチームとパニックに陥るチームを分ける。そして、答えは決して「単にロールバックすればいい」ほど単純ではない。行った変更の種類、関わるデータ、システムの状態、これらすべてがどちらの道が安全かを決定する。
復旧の2つの道
データベースマイグレーションの失敗から復旧する方法は、基本的に2つある。それぞれ動作が異なり、異なるリスクを伴い、異なる状況に適用される。
ロールバックとは、実行したマイグレーションを元に戻すことだ。ダウンマイグレーションを実行する。これは、行った操作の完全な逆の操作にあたる。カラムを追加したのであれば、ダウンマイグレーションはそのカラムを削除する。データ型を変更したのであれば、ダウンマイグレーションはそれを元に戻す。
ロールフォワードとは、問題のあるマイグレーションをそのままにしておき、問題を修正する新しいマイグレーションを書くことだ。後戻りはしない。修正を加えて前進する。
どちらの戦略にも適した場面がある。重要なのは、必要になる前に、どちらが自分の状況に合っているかを知っておくことだ。
ロールバックすべき時
ロールバックは、元に戻しても安全な変更に最も適している。通常、これは非破壊的な操作であり、変更を元に戻してもデータの損失や破損が発生しない。
ロールバックの良い候補は以下の通り:
- NULL可能なカラムの追加
- 新しいインデックスの作成
- 新しいテーブルの追加
- ビューや関数の作成
これらの変更は追加的なものだ。元に戻すときは、追加されたものを削除する。その過程で既存のデータが失われたり、破損したりすることはない。
ユーザーテーブルに last_login_at カラムを追加したシナリオを考えてみよう。このカラムはNULL可能なので、既存の行には影響がない。デプロイ後、アプリケーションコードにバグがあり、誤ったタイムスタンプを書き込んでいることが判明した。カラムを削除してロールバックするのは安全だ。カラムが空だったか、保持する必要のないデータが含まれていたため、データは損なわれない。
ロールフォワードすべき時
ロールフォワードは、マイグレーションが破壊的である場合、または元に戻すことが元の問題よりも大きな害を引き起こす場合に、より良い選択となる。
ロールフォワードがより安全な状況:
- カラムやテーブルの削除
- 精度を失う方法でのデータ型の変更
- 既存のデータ値を大規模に変更する
- テーブルのマージ
- 他のシステムが依存しているNOT NULL制約の削除
legacy_status カラムを削除するマイグレーションを実行したとしよう。そのカラムのデータはもう存在しない。カラムを追加し直すダウンマイグレーションを書いても、データは復元されない。そのステータスフィールドに依存していたユーザーは、NULL値を目にすることになる。最善の策は、カラムを再作成し、バックアップやアプリケーションログからデータを投入する新しいマイグレーションを書くことだ。
もう一つのよくあるケース:VARCHARからINTEGERにカラムを変更し、文字列値を数値に変換したとする。データ型をVARCHARに戻すロールバックは、整数値が文字列にきれいに変換できない可能性があるため危険だ。値 42 は "42" になるが、変換中に切り捨てられたり丸められたりした値はどうなるのか?情報が失われている。ロールフォワードなら、これらのエッジケースを明示的に処理する注意深いマイグレーションを書くことができる。
実際に機能するダウンマイグレーションを書く
ロールバックをサポートすることを選択した場合、ダウンマイグレーションには真の注意が必要だ。アップマイグレーションの機械的な逆であってはならない。すべてのダウンマイグレーションは、ロールバック時点に存在するデータを考慮しなければならない。
以下は、ダウンマイグレーションを危険にするものだ:
具体的な例を考えてみよう。users テーブルにNULL可能なカラム last_login_at を追加したが、アプリケーションコードにバグがある。安全なダウンマイグレーションとロールフォワードによる修正は次のようになる:
-- 安全なダウンマイグレーション:カラムを削除するが、安全であることを確認してから行う
BEGIN;
-- ステップ1:このカラムに依存するアプリケーションコードやビューがないことを確認する
-- (このチェックはデプロイパイプラインで行われ、SQL内ではない)
-- ステップ2:カラムを削除する
ALTER TABLE users DROP COLUMN IF EXISTS last_login_at;
COMMIT;
-- ロールフォワードマイグレーション:正しい名前でカラムを追加する
BEGIN;
-- 意図した名前と型でカラムを追加する
ALTER TABLE users ADD COLUMN last_login_at TIMESTAMP;
-- オプションでアプリケーションログやバックアップからバックフィルする
-- UPDATE users SET last_login_at = ... WHERE id IN (...);
COMMIT;
このダウンマイグレーションは、カラムがNULL可能で追加的なものであるため安全だ。ロールフォワードマイグレーションは、スキーマ変更を元に戻すことなく問題を修正する。
- データがアップマイグレーション実行時と同じ状態であると仮定している
- アップマイグレーション後に追加または変更された行を無視している
- データの整合性をチェックせずに、盲目的にスキーマ変更を元に戻している
デフォルト値を持つNOT NULLカラムを追加する場合の安全なダウンマイグレーションは、以下のことを行うべきだ:
- カラムを削除してもアプリケーションのクエリが壊れないことを確認する
- カラム追加後に挿入された行を処理する
- 外部キー関係がカラムに依存していないことを確認する
VARCHARからINTEGERへのデータ型変更の場合、ダウンマイグレーションは、きれいな文字列表現を持たない値を処理する必要がある。整数を文字列にキャストし直す必要があるかもしれないが、NULLや元の文字列値にはなかったエッジケースも処理する必要がある。
無視できない本当のリスク
ロールバックは単純そうに聞こえるが、チームが何か問題が起きて初めて気づく深刻なリスクを伴う。
最大のリスクはデータ損失だ。 マイグレーションがカラムを削除すると、データは失われる。バックアップがない限り、どんなダウンマイグレーションもそれを戻すことはできない。マイグレーションの前にバックアップを取っていなければ、ロールバックは恒久的なデータ損失を受け入れることを意味する。
マイグレーションの依存関係が隠れた罠を作り出す。 マイグレーション2がマイグレーション1で追加されたカラムに依存している場合、マイグレーション1の前にロールバックするとすべてが壊れる。アプリケーションは、もはや存在しないカラムを期待するためクラッシュするかもしれない。削除された値を参照する行があるため、データの不整合が生じるかもしれない。
ロールフォワードにも独自のリスクがある。 最大のものは時間だ。新しいマイグレーションを書き、パイプラインを通し、デプロイする必要がある。その間、アプリケーションは壊れた状態で動作し続ける。ユーザーはエラーを経験している。チームは迅速に修正するようプレッシャーを感じており、それが別のミスを犯す可能性を高める。
ロールフォワードには、現在のデータベース状態の正確な知識も必要だ。仮定に基づいて修正を書くことはできない。マイグレーションが設計された時点のデータがどのようなものかではなく、現在のデータがどのようなものかを正確に知る必要がある。
必要になる前に決断を下す
ロールバックとロールフォワードのどちらを選ぶかを決めるのに最悪なタイミングは、本番環境が炎上している時だ。その時点で、あなたはストレスを感じ、時間は刻々と過ぎ、判断力は低下している。
より良いアプローチは、すべてのマイグレーションを実行前に分類することだ。各マイグレーションに復旧カテゴリを割り当てる:
- ロールバックしても安全:新しいカラム、テーブル、インデックスなどの追加的な変更
- ロールバック前にバックアップが必要:既存のデータを変更したり、NULL可能なカラムを削除する変更
- ロールフォワードのみ:カラムの削除、データ型の変更、テーブルのマージなどの破壊的な変更
この分類をマイグレーションファイルやデプロイメントのランブックに文書化する。何か問題が発生したとき、チームは分類を読み、定義済みの戦略を実行する。議論は不要だ。迷いも不要だ。
クイック判断チェックリスト
本番環境でマイグレーションを実行する前に、以下の質問を自問しよう:
以下の判断ツリーは、プレッシャーの中でもチェックリストを適用するのに役立つ:
- 変更は追加的か、それとも破壊的か?
- ダウンマイグレーションは、データを含む正確な以前の状態を復元できるか?
- マイグレーション前に取得した、検証済みのバックアップはあるか?
- ダウンマイグレーションはステージング環境でテスト済みか?
- このマイグレーションは、以前に実行された他のマイグレーションに依存しているか?
- ロールフォワード修正を書いている間のダウンタイムのコストはどれくらいか?
これらのすべてに答えられないのであれば、まだ本番環境でマイグレーションを実行してはならない。
具体的な教訓
ロールバックとロールフォワードは、互換性のある戦略ではない。異なる種類の変更に適用され、異なるリスクを伴う。データベースインシデントをうまく処理するチームは、SQLをタイピングするのが最も速いチームではない。マイグレーションが実行される前に復旧について考えていたチームだ。変更を分類し、ダウンマイグレーションをテストし、バックアップを準備していたチームだ。ダッシュボードが真っ赤になったとき、彼らはパニックにならなかった。彼らはすでに作っていた計画を実行したのだ。