データベースマイグレーションが失敗したとき:ロールバックよりロールフォワードが優れている理由
usersテーブルにphone_numberカラムを追加するデータベースマイグレーションをデプロイしたとします。マイグレーションは正常に実行されました。しかしその後、チームはこのカラムを使用するアプリケーションコードがまだデプロイされていないことに気づきました。新しいNOT NULLカラムに値を提供せずにINSERTしようとするため、新しいユーザー登録がすべて失敗するようになりました。
本番システムが停止しています。どうしますか?
ほとんどのチームは本能的にダウンマイグレーション、つまり変更を元に戻してカラムを削除するスクリプトに手を伸ばします。しかし、その本能は元の問題よりも大きな損害を引き起こす可能性があります。より良いアプローチがあります。それはロールフォワードです。
ダウンマイグレーションの問題点
ダウンマイグレーションは理論上はきれいに見えます。カラムを追加するupマイグレーションと、それを削除するdownマイグレーションを書きます。何か問題が発生した場合、ダウンマイグレーションを実行すればすべてが元の状態に戻ります。
しかし実際には、ダウンマイグレーションはいくつかの理由で危険です。
第一に、データ損失はほぼ確実です。アップマイグレーション実行後に何らかの行が挿入または更新された場合、カラムを削除するとそれらの値は消えます。回復できない顧客データを失う可能性があります。
第二に、ダウンマイグレーションはアプリケーションコードとデータベーススキーマの間に不一致を生み出します。アプリケーションコードがすでに新しいカラムの存在を期待している場合、それを削除すると実行時エラーが発生します。古いアプリケーションコードもデプロイする必要があり、複数のロールバックを同時に調整する必要があります。
第三に、ダウンマイグレーションはほとんどテストされません。チームは後付けとして書き、実際の緊急時だけに表面化するバグがよくあります。障害発生中に本番環境で未テストのスクリプトを実行することは、さらなるダウンタイムのレシピです。
ロールフォワードとは何か
ロールフォワードは、決してマイグレーションを元に戻さない戦略です。代わりに、マイグレーションが問題を引き起こした場合、問題を修正する新しいマイグレーションを書きます。データベースは以前の状態に戻るのではなく、修正された状態に前進します。
先ほどの例を使うと、phone_numberを削除するダウンマイグレーションを実行する代わりに、カラムをNULL可能にするかデフォルト値を追加する新しいマイグレーションを書きます。カラムは残りますが、障害の原因となった制約は削除されます。新しいユーザー登録は再び機能し、phone_numberにすでに保存されているデータは保持されます。
修正マイグレーションのSQLは次のようになります:
-- version_002: phone_number制約の修正
-- このマイグレーションはphone_numberをNULL可能にし、古いアプリケーションコードが
-- 値を提供せずに行を挿入できるようにします。
ALTER TABLE users
ALTER COLUMN phone_number DROP NOT NULL;
各マイグレーションは累積的な変更になります。最初のマイグレーションがカラムを追加しました。2番目のマイグレーションがカラムの制約を修正しました。マイグレーショントラッカーは両方の変更を順番に記録するため、version_002が履歴を消去せずにversion_001を修正したことがわかります。
チームがロールフォワードを好む理由
最大の利点はデータ損失ゼロです。最初のマイグレーションで問題を発見する前に500件の電話番号が保存されていた場合、それらの番号は修正後も残ります。ダウンマイグレーションではそれらが完全に削除されていたでしょう。
ロールフォワードはアプリケーションコードとデータベーススキーマの整合性も維持します。カラムがまだ存在するため、phone_numberを読み取るアプリケーションコードは引き続き機能します。同時にコードのロールバックを調整する必要はありません。データベースを修正し、その後自分のペースでアプリケーションコードを修正します。
このアプローチは、チームがアプリケーションコードのバグを処理する方法を反映しています。バグが本番環境に到達した場合、コードベース全体を先週のバージョンに戻したりはしません。修正をプッシュします。データベースマイグレーションも同じように機能するべきです。修正は新しいマイグレーションであり、元に戻すことではありません。
ロールフォワードが複雑になる場合
すべてのロールフォワード修正がカラム制約の変更ほど単純であるとは限りません。カラムのデータ型をVARCHARからINTEGERに変更したマイグレーションを考えてみてください。変換によって既存データが切り詰められたり破損したりした場合、修正マイグレーションでは以下が必要になるかもしれません:
- 元のデータ型を持つ新しいカラムを追加する
- 破損したカラムからデータをコピーし、値を回復するための変換を適用する
- 新しいカラムを使用するようにアプリケーションコードの参照を更新する
- 後のマイグレーションで破損したカラムを削除する
これは単純なダウンマイグレーションよりも多くの作業です。しかし、より安全でもあります。ステージング環境で修正マイグレーションを最初に実行し、データ復旧ロジックを検証してから本番環境に適用できます。ダウンマイグレーションにはそのようなセーフティネットはありません。単にカラムを削除して最善を願うだけです。
重要な洞察は、ロールフォワードではデプロイ前に考えられるすべての障害を予測する必要がないということです。必要なのは、何か問題が発生した場合にチームが修正を書けるという自信だけです。これにより、リスクが予防(完璧にするのは不可能)から復旧(練習できるスキル)に移ります。
ロールフォワードの実践的チェックリスト
チームの戦略としてロールフォワードを採用する前に、以下のプラクティスが整っていることを確認してください:
マイグレーションが問題を引き起こした場合の判断ツリー:
- すべてのマイグレーションは理論上は元に戻せる必要がありますが、必ずしもコードで実装する必要はありません。 ダウンマイグレーションが何をするかを理解しますが、特別な理由がない限り作成しません。
- 修正マイグレーションはまずステージングでテストします。 元のマイグレーションを実行し、障害シナリオを導入してから修正マイグレーションを適用します。その後、データの整合性を検証します。
- マイグレーショントラッカーを信頼できる状態に保ちます。 マイグレーション記録を手動で変更または削除しないでください。トラッカーは何がいつ変更されたかを理解するための監査証跡です。
- 障害モードを文書化します。 修正マイグレーションを作成する際に、何が問題でなぜ修正が機能するのかを説明するコメントを追加します。これは将来同様のパターンに遭遇するチームメンバーに役立ちます。
- ロールフォワードシナリオを練習します。 四半期ごとに、誰かが意図的に不良マイグレーションを導入し、チームが時間制限のある中で修正を作成してデプロイする練習をする訓練を実施します。
まとめ
ダウンマイグレーションは罠です。単純さを約束しますが、データ損失、調整の手間、未テストの緊急スクリプトをもたらします。ロールフォワードはデータベースの変更をコードの変更と同様に扱います。何かが壊れた場合、後退ではなく前進して修正します。
次回マイグレーションが失敗したときは、元に戻したい衝動に抵抗してください。代わりに修正マイグレーションを書きましょう。データ、アプリケーション、そしてチームの健全性が感謝してくれるでしょう。