「ロールバックして」が一瞬で終わる時と終わらない時があるのはなぜか?
本番で不具合が出て「とりあえずロールバックして」と言われたことがあるはずだ。ある時は数十秒で収まったのに、別の時は数分ハラハラしながら Pod の入れ替わりを見守った——同じ「ロールバック」という一言でも、中身はデプロイ方式ごとにまるで別の操作だ。
- Rolling Update のロールバックは「逆方向のロールアウト」であり、進むのと同じだけ時間がかかる
- Blue-Green は環境を丸ごと二重化し、ロールバックは経路の切り戻し一本で済む
- Canary はトラフィックの重みを段階的に上げ、戻す時は重みを 0 に戻すだけだが自動化には監視が要る
デプロイ方式とは「新旧バージョンの共存のさせ方」の設計である
デプロイとは古いバージョンを新しいバージョンに置き換える作業だが、置き換えの瞬間には必ず新旧が同時に存在する時間がある。Kubernetes の Deployment が標準で使う RollingUpdate はこの時間を「土台は 1 つのまま、中身を少しずつ入れ替える」方式で処理する。maxSurge(一時的に増やせる上限)と maxUnavailable(同時に減らせる上限)の 2 つのパラメータが、この入れ替えの速さとリスクの両方を決める。
ここで見落とされがちなのは、「ロールバック」という操作がこの入れ替えの仕組みそのものに縛られるという点だ。ロールバックは特別な巻き戻し機能ではなく、同じ入れ替えメカニズムを逆方向に走らせているにすぎない。だからこそ、入れ替えの設計が違えばロールバックの体感速度も方式ごとに全く違う結果になる。
Rolling Update: ロールバックは「もう一度ロールする」こと
kubectl rollout undo を叩くと、Kubernetes は 1 世代前の ReplicaSet を新しい目標として、通常のデプロイと同じ手順で新しい Pod を起動し古い Pod を終了させていく。つまり「戻す」と言っても瞬時に切り替わるわけではなく、maxSurge / maxUnavailable の制約に従って 1 台ずつ入れ替わる、通常のロールアウトと同じ所要時間がかかる。
途中で不具合が起きているとロールバック中にも新しい Pod の起動と旧 Pod の終了が交互に発生し、Pod 削除時のエンドポイント除去と SIGTERM 送信のタイミングずれによる一時的な 502 が、ロールバックのたびに再発しうる。「戻せば安全」と思って待っている間も、進行中のロールアウトと同じリスクを踏み続けていることになる。
舞台が 1 つしかない劇場と、舞台を 2 つ持つ劇場の違いに近い。舞台が 1 つなら、キャストを新しい俳優に 1 人ずつ入れ替えながら公演を続けるしかなく、元のキャストに戻すにも同じ入れ替え作業をもう一度頭から行う。舞台が 2 つあれば、片方で新キャストのリハーサルを済ませておき、客席の案内をどちらの舞台に向けるかを切り替えるだけで済む——切り替え自体は一瞬でも、そのために舞台をまるごと二重に維持するコストを普段から払っている。
Blue-Green: 舞台を 2 つ用意し、経路を一括で切り替える
Blue-Green デプロイは、稼働中の環境(Blue)とは別に新バージョンの環境(Green)を丸ごと用意し、ヘルスチェックを通過したらロードバランサーの向き先を Blue から Green へ一括で切り替える。新旧が同時にリクエストを受けることはなく、切り替えは経路の付け替え 1 回で完了する。
ロールバックが速いのは、切り替え後もしばらく旧環境(Blue)を残しておくからだ。たとえば AWS CodeDeploy の BlueInstanceTerminationOption は、切り替え成功後に旧インスタンスを何分後に終了するかを設定できる猶予時間(デフォルト値が存在する)を持つ。この猶予時間の内側なら、ロールバックは経路をもう一度 Blue に向けるだけで済む。猶予時間が過ぎて旧環境が終了された後は、Rolling Update と同じく新しい環境をもう一度作り直すデプロイになる。
Blue-Green(舞台が 2 つ)
LB ─┬─▶[Blue v1] 100%(現行)
└─▶[Green v2] 0%(待機・検証中)
切替後
LB ─┬─▶[Blue v1] 0%(猶予時間だけ残存)
└─▶[Green v2] 100%(現行)
ロールバック=経路をもう一度 Blue へ
Canary: 少数のトラフィックだけ流し、比率を段階的に上げる
Canary デプロイは Blue-Green と同じく新旧 2 系統を並行稼働させるが、切り替えを一括ではなく比率で行う。Argo Rollouts では setWeight ステップで新バージョンに流すトラフィックの割合を指定し、pause で様子を見ながら段階的に比率を上げていく。トラフィック管理機構(サービスメッシュや Ingress)を使わない基本的な Canary では、この比率をレプリカ数の割合で近似する——10 レプリカで重み 20% を指定すると、新バージョンには約 2 レプリカが割り当てられる形だ。
ロールバックは重みを 0 に戻すだけなので数秒〜数分で終わるが、これを自動化するには「何をもって異常とするか」を先に決めておく必要がある。Argo Rollouts の AnalysisRun はメトリクスがしきい値を超えたことを検知すると、canary のレプリカを 0 にスケールしトラフィックを stable 側へ戻す。逆に言えば、メトリクスもアラートしきい値も無ければ、異常を検知する主体が存在せず「一部のユーザーだけが壊れたバージョンを使い続ける」時間が長引くだけになる。
それでも一瞬で戻せないもの: 状態を持つ変更
Blue-Green も Canary も、新旧のアプリケーションコードは瞬時に切り替えられる。しかし両者が同じデータベースを共有している場合、DB のスキーマ変更まで含めてロールバックが一瞬で終わるとは限らない。新バージョンのデプロイでカラムを削除したり型を変えたりすると、切り戻した旧バージョンのコードがそのスキーマを読めずに壊れる可能性がある。
この問題を避けるための定石が expand/contract パターンだ。まず新しい列や構造を追加するだけで既存の列は残す「expand」、新旧両方のコードが動く期間を経て新コードへの移行を終えたら不要になった旧構造を削除する「contract」という 2 段階に分け、その間は常に新旧どちらのバージョンからもスキーマが読める状態を保つ。アプリのロールバックが速くても、DB の変更が後方互換でなければロールバックできる保証は消える。
ECS や Lambda の AWS CodeDeploy、Kubernetes 上の Argo Rollouts や Flagger、Vercel・Netlify のプレビュー環境から本番昇格まで、いずれも「新旧を並行稼働させて経路や重みで制御する」という同じ骨格を持つ。障害対応で「ロールバックして」と言われたら、まずどの方式が使われているかを確認すると、待つべき時間の見積もりが立つ。
kind で立てた Kubernetes クラスタ上で、Rolling Update のロールバックが「もう一度のロールアウト」であることを rollout status の出力で確認する。
kind create cluster --name demo kubectl create deployment web --image=nginx:1.25 --replicas=4 kubectl rollout status deployment/web # 新バージョンへロール kubectl set image deployment/web nginx=nginx:1.27 kubectl rollout status deployment/web # ロールバック(=逆方向のロールアウト) kubectl rollout undo deployment/web kubectl rollout status deployment/web kubectl rollout history deployment/web
set image 後と rollout undo 後の rollout status を見比べると、どちらも「Waiting for deployment ... N out of 4 new replicas have been updated」という同じ形式の進捗メッセージを段階的に出力する。ロールバックが専用の瞬間巻き戻しではなく、通常のロールアウトと同じ入れ替え処理であることが出力からそのまま読み取れるはずだ。
- ロールバックは方式によらずだいたい同じ時間で終わる — Rolling Update のロールバックは通常のロールアウトと同じ入れ替え処理を逆方向に行うため、進むのと同じだけ時間がかかる。Blue-Green や Canary は旧環境・旧比率がまだ残っているので、経路や重みを戻すだけで済み桁違いに速い。
- Blue-Green にしておけば常に安全にロールバックできる — アプリのコードは経路切替で一瞬で戻せても、DB スキーマを後方互換でない形で変更していれば旧バージョンのコードはそのスキーマを読めない。expand/contract のようにスキーマ側も新旧両対応にしておく必要がある。
- Canary は一部のユーザーにしか影響しないから設定を丁寧にしなくても安心 — 自動ロールバックはメトリクスのしきい値判定に依存する。監視やアラートを設定していなければ異常を検知する主体が存在せず、壊れたバージョンを使い続けるユーザーが放置される時間が長引くだけになる。
- ロールアウト(rollout)
- 新しいバージョンへ段階的に入れ替えていく一連の処理。ロールバックもこの処理を逆方向に走らせたもの。
- setWeight
- Argo Rollouts の Canary ステップで、新バージョンに振るトラフィック比率を指定するフィールド。
- 終了猶予時間(termination wait time)
- Blue-Green デプロイで切替後に旧環境を残しておく時間。この間だけ経路の切り戻しによる即時ロールバックができる。
- AnalysisRun
- Argo Rollouts が Canary 中にメトリクスを評価し、しきい値超過を検知すると自動でトラフィックを stable 側へ戻す仕組み。
- expand/contract パターン
- DB スキーマ変更を「追加→新旧並行→旧構造削除」の3段階に分け、常に新旧バージョンから読める状態を保つ手法。
- Performing Blue/Green Deployments with AWS CodeDeploy and Auto Scaling Groups — AWS 公式ブログによる Blue-Green の切替・巻き戻しの具体的な仕組み
- BlueInstanceTerminationOption - AWS CodeDeploy API Reference — 旧環境の終了猶予時間を制御する API の一次ソース
- Canary - Argo Rollouts — setWeight・pause・AnalysisRun など Canary ステップの公式リファレンス
- Using the expand and contract pattern | Prisma's Data Guide — 後方互換を保ちながら DB スキーマを変更する手順の解説