ロールバックが事態を悪化させるとき(そして代わりに取るべき対策)
新しいバージョンをデプロイした。パイプラインは成功。ヘルスチェックも通過。CPUやメモリの使用率も正常だ。しかし、あなたのスマホがユーザーからのメッセージで振動し始める。昨日まで動いていた機能が壊れている。データベースに書き込まれているデータがおかしい。そしてエラーログには?特に異常は見当たらない。
こういう瞬間、多くのチームは「ロールバックしよう」と考える。単純に聞こえる。新しいバージョンを古いバージョンに戻せばいい。問題解決。しかし実際には、ロールバックは管理可能な問題を大惨事に変えかねない。古いバージョンは、新しいバージョンがすでに書き込んだデータを理解できないかもしれない。データベーススキーマが変更されているかもしれない。あるいは、ロールバック自体に時間がかかりすぎて、移行中により多くのユーザーが影響を受ける。
ロールバックはボタンを押すだけの作業ではない。プレッシャーの中、不完全な情報をもとに下す決断であり、その影響はシステム全体に波及する。いつ、どのようにロールバックすべきか(そしてすべきでないか)を理解することが、迅速に復旧できるチームと事態を悪化させるチームの分かれ目となる。
ロールバックの本当のシグナル
多くのチームは、デプロイが正常かどうかを判断するためにヘルスチェックに依存している。しかしヘルスチェックは、アプリケーションが技術的に動作しているかどうかを教えてくれるだけだ。アプリケーションが正しいことをしているかどうかは教えてくれない。
次のシナリオを考えてみよう。新しいバージョンが顧客の注文をデータベースに正常に書き込んでいる。エラーもクラッシュもない。しかし、注文データが間違った通貨フォーマットで保存されている。アプリケーションは技術的には正常だが、機能的には壊れている。ヘルスチェックはこれを検知できない。ビジネスレベルのメトリクスを設定していない限り、モニタリングでも検知できないだろう。
ロールバックの判断は通常、複数のシグナルの組み合わせから生まれる。
- ヘルスチェックが失敗し始める
- エラー率が急上昇する
- ユーザーからの報告が期待と異なる動作を説明している
- ビジネスメトリクスが通常のパターンから逸脱する
しかし、チームが見落としがちなもう一つの要素がある。それは時間だ。ロールバックを決断するまでにどれだけ待つのか?5分?30分?誰かが苦情を言うまで?待てば待つほど、新しいバージョンによって書き込まれるデータが増える。そして書き込まれるデータが増えれば増えるほど、ロールバックは困難になる。
すべてのデプロイの前に、明確な観測期間を設定しよう。事前に決めておく。「最初の15分間に問題が見つからなければ、安定しているとみなす。その期間内に問題が見つかれば、すぐにロールバックする。」これにより、悪い状況をさらに悪化させる迷いを取り除くことができる。
ステートレスとステートフルは同じではない
ロールバックの容易さは、アプリケーションが状態(ステート)を保持するかどうかにほぼ完全に依存する。
ステートレスアプリケーションの場合、ロールバックは簡単だ。トラフィックを古いバージョンに戻すだけ。復元するデータも、調整するスキーマもない。古いバージョンは中断したところから再開できる。なぜなら、新しいバージョンの状態に依存していないからだ。これが、ステートレスサービスが積極的なロールバック戦略の第一候補となる理由である。
ステートフルアプリケーションの場合、ロールバックは別物だ。新しいバージョンが、古いバージョンが知らない新しいフィールドを持つレコードを10,000件データベースに書き込んだと想像してほしい。アプリケーションをロールバックすると、古いバージョンはそれらのレコードを読み取ろうとする。データ形式が期待と一致しないためクラッシュする。あるいはさらに悪いことに、新しいバージョンがデータベースのテーブル構造を変更していた場合、古いアプリケーションはスキーマの非互換性のために起動すらできない。
これが落とし穴だ。データをロールバックせずにアプリケーションだけをロールバックすること。デプロイメントがデータベーススキーマを変更したり、新しい形式でデータを書き込んだりした場合、コードだけをロールバックしても不十分だ。以下のいずれかを行う必要がある。
- デプロイ前の時点にデータベースをリストアする
- スキーマ変更を元に戻すマイグレーションスクリプトを作成する
- 一部のデータが失われたり破損したりすることを受け入れる
これらの選択肢にはそれぞれコストとリスクがある。データベースのリストアには時間がかかり、その間にシステムの他の部分が書き込んだ正当なデータが失われる可能性がある。逆マイグレーションは、プレッシャーの中で書くのではなく、デプロイ前にテストして準備しておく必要がある。
実際に機能する3つの戦略
状況に応じて、異なるロールバックアプローチが必要となる。以下は、チームが実際に使用している3つの戦略である。
以下の判断ツリーが選択の指針となる。
フォワードフィックス
古いバージョンに戻る代わりに、問題を修正した新しいバージョンをビルドしてデプロイする。これはステートフルアプリケーションにとって多くの場合最も安全な選択肢である。データの変更を元に戻す必要がなく、修正するだけで済むからだ。
フォワードフィックスは以下の場合に有効である。
- バグが限定的で迅速に修正できる
- パイプラインが数分で新しいバージョンをデプロイできる
- 壊れたバージョンによって書き込まれたデータが復旧可能、または移行できる
リスクは時間だ。バグが深刻で修正に数時間かかる場合、作業中もユーザーは問題を経験し続ける。フォワードフィックスには、問題を診断して迅速に修正するチームの能力への信頼が必要である。
トラフィックシフト
カナリアリリースやブルーグリーンデプロイメントを使用している場合、ロールバックは古いバージョンにトラフィックを戻すだけで完了する。古いバージョンはまだ実行中で、トラフィックを受け入れる準備ができている。デプロイプロセスを待つ必要はない。一部のユーザーが古いバージョンに、他のユーザーが新しいバージョンにアクセスする移行期間もない。
これは最も高速なロールバック方法である。ただし、この方法はデプロイ戦略を最初からこの目的で設計している場合にのみ機能する。ローリングアップデートを使用している場合、待機中の古いバージョンは存在しない。ロールバックは古いアーティファクトを使ってデプロイプロセス全体を再度実行することを意味する。時間がかかり、移行中にユーザーがエラーにさらされる。
以下は、カナリアデプロイメント中にKubernetesを使用して古いバージョンにトラフィックを戻す具体的な例である。
# 現在のトラフィック分割を確認(2つのセレクタを持つサービスを想定)
kubectl get virtualservice my-app -o jsonpath='{.spec.http[0].route[*].weight}'
# トラフィックの100%を古いバージョン(v1)にシフト
kubectl patch virtualservice my-app --type='json' -p='[
{"op": "replace", "path": "/spec/http/0/route/0/weight", "value": 100},
{"op": "replace", "path": "/spec/http/0/route/1/weight", "value": 0}
]'
# 変更を確認
kubectl get virtualservice my-app -o yaml | grep -A5 "route:"
このアプローチは、重み付けルーティングをサポートするサービスメッシュまたはイングレスコントローラ(IstioやTraefikなど)があることを前提としている。よりシンプルなセットアップでは、サービスのセレクタを更新して古いバージョンのPodのみを指すようにすることで同じことを実現できる。
現状を受け入れパッチ適用
時には、新しいバージョンをそのまま実行し続け、問題をその場で修正するのが最善の判断であることもある。直感に反するように聞こえるかもしれないが、考えてみてほしい。新しいバージョンがすでに古いバージョンでは読み取れないデータを書き込んでいる場合、ロールバックはダウンタイムを確実にする。新しいバージョンを実行し続ければ、修正に取り組んでいる間もユーザーはシステムを使用できる。
このアプローチは以下の場合に有効である。
- 問題が重大でない(軽微な表示の問題、ブロッキングしないバグ)
- 新しいバージョンによって書き込まれたデータに価値があり、ロールバックすると失われる
- チームが妥当な時間内にパッチを提供できる
鍵となるのは、いつ受け入れ、いつ行動するかを知ることだ。問題がコア機能に影響を与えたり、データを破損したりする場合、現状を受け入れてパッチを適用するのは適切な選択ではない。
次のデプロイ前の実践的チェックリスト
デプロイする前に、チームで以下の質問を確認しよう。
- ロールバックの判断をトリガーするシグナルは何か?(ヘルスチェック、エラー、ユーザー報告、ビジネスメトリクス)
- 判断するまでにどれだけ観測するか?(5分、15分、30分)
- このデプロイはデータベーススキーマやデータ形式を変更するか?
- 変更する場合、テスト済みの逆マイグレーションまたはリストア計画はあるか?
- 古いバージョンはまだ実行中で、トラフィックを受け入れる準備はできているか?
- ロールバックよりもフォワードフィックスの方が速くできるか?
答えを書き留め、チームで共有しよう。ロールバックを計画するタイミングはデプロイの前であり、インシデントの最中ではない。
まとめ
ロールバックは万能のセーフティネットではない。ステートレスアプリケーションにとっては信頼性の高い脱出ハッチである。ステートフルアプリケーションにとっては、事態を悪化させる罠となり得る。ロールバックの判断は、影響を受けたデータの量、フォワードフィックスの速度、そして古いバージョンがシステムの現在の状態でまだ動作できるかどうかに依存する。
すべてのデプロイの前にロールバックパスを計画しよう。シグナルを把握し、観測期間を設定しよう。そして覚えておいてほしい。最善の復旧方法は、後退することではなく、前進して修正することもあるということを。