データベースのロールバックがアプリケーションのロールバックより難しい理由
アプリケーションの新しいバージョンをデプロイしたとしよう。何かがうまくいかない。ロールバックボタンを押すと、古いバージョンが再び起動し、数分以内にすべてが正常に戻る。データは失われず、副作用も残らない。このプロセスはクリーンで可逆的に感じられる。
では、別のシナリオを想像してみてほしい。ordersテーブルにstatusカラムを追加するデータベースマイグレーションを実行する。マイグレーションは完了し、新しいカラムにはデフォルト値が設定され、更新されたアプリケーションはそのカラムに実際のデータを書き込み始める。数時間後、アプリケーションロジックにバグが見つかり、statusの値が信頼できないことが判明する。アプリケーションを以前のバージョンにロールバックすることに決める。古いコードが再び実行される。しかし、statusカラムはまだそこにある。そこに書き込まれたデータもまだ残っている。そして、古いアプリケーションコードはその余分なカラムを処理する方法を知らないか、さらに悪いことに、予期しないカラムに遭遇して壊れてしまう可能性がある。
これが核心的な問題だ。アプリケーションのロールバックはコードを元に戻すだけだが、データベースのロールバックは構造とデータの両方をマイグレーション前の状態に戻さなければならない。そして、それは自動的には行われない。
アプリケーションのロールバックが簡単な理由
アプリケーションをロールバックするとき、基本的には実行可能なコードのセットを別のものと交換している。古いバージョンが引き継ぎ、新しいリクエストを処理し始め、システムは継続する。ロールバック自体の間、永続的な状態は変更されない。データベースはロールバックがトリガーされる前とまったく同じ状態のままである。変更されるのは、実行されているコードのバージョンだけだ。
このシンプルさこそが、多くのチームがロールバックをセーフティネットとして扱う理由である。何か問題が発生したら、元に戻して後でもう一度試せばよい。これはステートレスなサービスや、データベーススキーマがバージョン間で変更されないアプリケーションではうまく機能する。
データベースのロールバックが異なる理由
データベースのロールバックには、稼働中のデータストアに対する構造変更を取り消すことが含まれる。つまり、カラムの削除、削除されたテーブルの復元、変更された制約の元に戻すなどだ。そして、アプリケーションコードとは異なり、データベースにはマイグレーションが実行されてから変更、追加、削除されたデータが含まれている可能性がある。
usersテーブルからlegacy_flagというカラムを削除するマイグレーションを考えてみよう。ロールバックが必要な場合、そのカラムを元に戻さなければならない。しかし、そのカラムにあったデータはどうなるのか?マイグレーションが単にカラムを削除しただけなら、事前にバックアップを取っていない限り、そのデータは失われる。マイグレーションがカラムをリネームしたり変換したりした場合は、その変換を正確に逆転させ、その間に書き込まれた新しいデータを破損しないようにする必要がある。
問題を示す具体的な例を挙げる。前方マイグレーションがカラムを追加し、対応するロールバックがそれを削除しようとする。
-- 前方マイグレーション: デフォルト値を持つNOT NULLカラムを追加
ALTER TABLE orders ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'pending';
-- 数時間後、新しいアプリケーションコードが実際のステータス値を書き込む
-- 一部の行は status = 'shipped', 'cancelled' などになっている
-- ロールバックマイグレーション: カラムを削除
ALTER TABLE orders DROP COLUMN status;
-- これは成功するが、すべてのステータスデータは永久に失われる。
ロールバックマイグレーションがカラムをリネームや変換してデータを保持しようとした場合、制約、インデックス、そしてロールバック後に古いアプリケーションによって書き込まれた新しいデータを処理する必要がある。これは脆弱で、ほとんどテストされていないプロセスである。
これは理論上の問題ではない。ダウンマイグレーション(前方マイグレーションによる変更を元に戻すスクリプト)に依存しているチームは、それらのスクリプトがほとんどテストされておらず、時には壊れており、本番環境で実行するのはほぼ常にリスクが伴うことに気づくことが多い。途中で失敗したダウンマイグレーションは、データベースを一部の変更は元に戻され、他は戻されていないという一貫性のない状態のままにする可能性がある。
より安全なアプローチ: 後方互換性のあるマイグレーション
より信頼性の高い戦略は、すべてのデータベースマイグレーションを後方互換性があるように設計することだ。つまり、行うスキーマ変更はアプリケーションの古いバージョンを壊してはならない。新しいカラムを追加する必要がある場合は、既存のカラムを削除したり変更したりせずに追加する。古いアプリケーションは新しいカラムを単に無視するため、引き続き動作する。新しいアプリケーションはそれを使い始める。新しいバージョンにバグがあることが判明した場合、データベースにまったく触れずにアプリケーションをロールバックできる。余分なカラムは残るが、古いコードはそれを気にしない。
このアプローチには規律が必要だ。すべてのスキーマ変更は、まだ実行されている可能性のあるすべてのアプリケーションバージョンへの影響を評価されなければならない。しかし、これは失敗したりデータを失ったりする可能性のあるダウンマイグレーションに依存するよりもはるかに安全である。
以下は、一般的な操作における後方互換性のあるマイグレーションの実際の動作を示す。
カラムの追加: 単に追加する。古いコードと新しいコードの両方で機能するデフォルト値を提供できない限り、
NOT NULLにしない。古いアプリケーションはそのカラムを読み書きしないため、影響を受けない。カラムのリネーム: 直接リネームしない。代わりに、新しい名前で新しいカラムを追加し、移行期間中は両方のカラムに書き込むようにアプリケーションを更新し、古いコードが実行されていないことを確認した後、後のマイグレーションで古いカラムを削除する。
カラムの削除: 最初にアプリケーションでその使用を停止する。その変更をデプロイする。その後、別のマイグレーションでカラムを削除する。アプリケーションをロールバックする必要がある場合、カラムはまだそこにある。
カラム型の変更: 新しい型で新しいカラムを追加し、データを段階的に移行し、新しいカラムを使用するようにアプリケーションを更新し、その後でのみ古いカラムを削除する。
これらのパターンはそれぞれ手順を増やすが、各手順はデータ損失なしで元に戻すことができる。
ダウンマイグレーションの真のコスト
一部のチームは、ダウンマイグレーションの方が記述が簡単に見えるため、依然として好む。変更を元に戻す単一のスクリプトは、多段階の後方互換性のあるアプローチよりもクリーンに感じられる。しかし、そのシンプルさの代償は、プレッシャーがかかったときに現れる。
本番インシデントが発生し、迅速にロールバックする必要がある場合、テストされていないダウンマイグレーションを実行して、失敗したり、時間がかかりすぎたり、静かにデータを削除したりする可能性があることは、最も避けたいことだ。時間的プレッシャー、ストレス、クリーンなフォールバックの欠如により、ダウンマイグレーションはギャンブルになる。
後方互換性のあるマイグレーションは、そのギャンブルを排除する。アプリケーションをデータベースとは独立してロールバックできるようにする。スキーマ変更をどうするかを決定する時間を与え、即座のリスクの高い逆転を強制しない。
データベースロールバック計画のための実践的チェックリスト
痛みを伴うロールバックシナリオを避けたい場合、すべてのマイグレーションの前に確認すべき短いチェックリストを以下に示す。
- このマイグレーション後も、古いアプリケーションバージョンは正しく実行できるか?
- マイグレーションがカラムを追加する場合、古いコードはそれを無視するか?
- マイグレーションがカラムを削除する場合、古いコードはすでにその使用を停止しているか?
- マイグレーションがカラムをリネームまたは変更する場合、古い構造と新しい構造の両方が共存する移行期間があるか?
- データ損失なしにこのマイグレーションを元に戻す、テスト済みで安全な方法はあるか?
これらすべてに「はい」と答えられない場合、そのマイグレーションは対処されていないロールバックリスクを抱えている。
まとめ
データベースのロールバックは、単にバージョンを元に戻すことではない。データを無傷で一貫性を保ちながら、アプリケーションが副作用なしに以前の状態に戻れるようにすることである。最も安全な道は、アプリケーションのロールバックとデータ損失の間で選択を強いることのないマイグレーションを設計することだ。すべてのスキーマ変更に後方互換性を組み込み、ダウンマイグレーションはデフォルトの戦略ではなく、最後の手段として扱う。午前2時にインシデントをデバッグしている未来の自分が感謝するだろう。