Argo RolloutsのBlue-Greenは「2つの環境の交代」じゃなかった ― 名前に引きずられて誤解していた話
個人開発しているMinecraftサーバー監視アプリ「MineWatch」(公式サイト、アプリの全体像はこちら)の本番環境は、Argo RolloutsでBlue-Greenデプロイを組んでいます。自分で設計して実装した仕組みなのに、しばらく経って見返したら、自分の理解が「Blue-Green」という名前に引きずられて実装とズレていました。この記事は、その誤解に気づいて、実機で確認し直した記録です。
TL;DR
- Blue-Greenデプロイの元祖の考え方は、「BlueとGreenという2つの固定環境が、リリースのたびに役割(稼働中/待機中)を交代する」というものです。
- しかしArgo Rolloutsの実装は違います。「Blue」「Green」という固定の名前は無く、
activeとpreviewという2つのServiceが役割を持つだけで、実体(ReplicaSet)は毎回新しく作られ、古い方は昇格後に破棄されます。「2つの環境を使い回す」のではなく「常に使い捨てる」方式です。 - もう1つ誤解していたのは、バージョン別ルーティング(新旧アプリで別のAPIを見せる仕組み)と、Blue-Greenデプロイ(デプロイの安全性を確保する仕組み)を混同していたことです。この2つは別の問題を解決しています。
- 実機(
kubectl argo rollouts get rollout、Service の selector)で確認し直し、思い込みと実際の挙動の差を埋めました。
1. なぜBlue-Greenを入れたか
MineWatchは、App Storeの審査中もバックエンドのfixを都度本番へ反映する運用(release/x.y.zブランチ作成直後にmainへ先行マージ)にしています。この運用に落ち着くまでの経緯(git-flow移行後に実際に2回踏んだ事故)は別記事にまとめて記事にしようと思ってます。これにより本番への再デプロイ頻度が上がったため、都度の再デプロイを既存利用者に一切影響させず、確認してから手動で切り替えられるようにする目的で、api/web/adminをKubernetesのDeploymentからRollout(Argo Rollouts CRD)へ変換しました。production限定で、stagingは通常のDeploymentのままです。
2. 「Blue-Green」という名前から連想していたこと
Blue-Greenデプロイの元祖の説明(Martin Fowlerのbliki記事が有名です)は、だいたいこういう内容です。
- BlueとGreenという、同じ構成の環境を2つ用意する
- 常に片方が本番トラフィックを受けている(例: Blue)
- 新しいバージョンは、待機中のもう片方(Green)にデプロイする
- 検証してから、ルーターの向き先をGreenへ切り替える
- 次のリリースでは、役割が逆になる(今度はGreenが稼働中、Blueが待機中でデプロイ先になる)
この「2つの固定環境が交代する」というイメージのまま、自分が構築したMineWatchのBlue-Greenも同じだと思い込んでいました。つまり「旧バージョンのアプリはBlue環境(旧API)に残り続け、新バージョンのアプリだけがGreen環境(新API)を見る」というような、環境そのものが2つ常設されているイメージです。
3. 実際のArgo Rolloutsの仕組み: 固定2環境ではなく、使い捨て
MineWatchのRollout定義を読み直すと、activeServiceとpreviewServiceという2つのServiceを指定しているだけで、「Blue」「Green」という名前はどこにも出てきません。
動きはこうです。
- 新しいイメージがビルドされてmainにマージされると、新しい
ReplicaSetが生成される。これがpreviewとして起動する(Paused、まだ公開トラフィックは受けない)。 -
activeServiceは、それまで通り古いReplicaSetを指したまま。 - previewを検証してから
kubectl argo rollouts promote apiを実行すると、activeServiceのselectorが新しいReplicaSetへ書き換わる。 - 古い
ReplicaSetはscaleDownDelaySeconds(MineWatchでは300秒)だけ生かしたまま残り(即ロールバック用)、その後スケールダウン(0台)される。
「Blue」「Green」という名前の環境が2つ固定である訳ではなく、active/previewという2つの「役割(Serviceの指し先)」だけがあって、その中身(ReplicaSet)は毎回新しく作られては古いものが捨てられていく、という方式です。「2つの環境を使い回す」のではなく「常に新しい方を作って、古い方を捨てる」が実態でした。
4. 実機で確認: 昇格後、旧ReplicaSetは本当に消えているか
思い込みと実装の違いに気づいたので、実際に本番を見て確認しました。
$ kubectl argo rollouts list rollouts -n minewatch
NAME STRATEGY STATUS STEP SET-WEIGHT READY DESIRED UP-TO-DATE AVAILABLE
admin BlueGreen Healthy - - 1/1 1 1 1
api BlueGreen Healthy - - 2/2 2 2 2
web BlueGreen Healthy - - 1/1 1 1 1
apiの詳細を見ると、直近のリビジョンだけがstable,activeで、それより前のリビジョンは全部ScaledDownでした。
├──# revision:7
│ └──⧉ api-7bdb595dbb ReplicaSet ✔ Healthy 38h stable,active
├──# revision:6
│ └──⧉ api-f7b4947d7 ReplicaSet • ScaledDown 28d
├──# revision:5
│ └──⧉ api-79f5c9d6d8 ReplicaSet • ScaledDown 36d
実際に走っているPodの数(kubectl get rs)でも確認しました。現行リビジョンだけがdesired/current 1以上で、他は全部0です。
NAME DESIRED CURRENT
api-7bdb595dbb 2 2
api-f7b4947d7 0 0
api-79f5c9d6d8 0 0
api-6457d6bf9d 0 0
...(以下同様、全部0)
「次のリリースのために旧環境を温存しておく」という発想は、少なくとも今の実装には無く、昇格が終われば旧世代のPodは実際に0台まで削られていました。
5. もう1つの誤解: バージョン別ルーティングと互換性を混同していた
MineWatchには、iOSクライアントが自分のアプリのVERSIONをヘッダで申告し、nginxが自動でpreviewへ振り分ける仕組み(C-5)もあります。
map $http_x_app_version $api_backend {
default "${API_UPSTREAM_HOST}"; # 通常はこっち(active)
"${VERSION}" "api-preview.minewatch..."; # 現在の候補バージョンと一致するときだけpreview
}
これを見て、最初は「指定バージョンより古いアプリは旧API(active)、新しいアプリは新API(preview)を見る、という形でずっと住み分けている」とイメージしていました。しかし実際のmapはdefault節が「現在の候補バージョンと一致しないものは全部active」という定義です。「旧バージョン専用の旧API」という3つ目の受け皿は存在しません。 activeは単一の可変スロットで、昇格すればその中身は即座に新しいコードへ置き換わります。
しかも運用上、apiの昇格自体は、iOSのTestFlightアップロードや審査提出より先に完了させる設計です(リリース手順書には「apiは問題なければ早めに昇格する(バックエンドは審査より先に稼働している必要があるため)」とあります)。つまり、審査に出す前の時点で、既存の旧バージョンのアプリユーザーも含めて全員が新しいAPIを見ることになります。
これが壊れないのは、ルーティングで新旧を分けているからではなく、API自体が新旧のアプリバージョンを同時に相手にしても壊れないよう設計されているからです(v1/v2のようなAPIバージョニング、DBスキーマ変更をExpand-Contractで進める方針)。バージョン別ルーティング(C-5)は、あくまで「審査中にAPIだけ追加で直したとき、実際のTestFlight実機で検証する」ための補助的な仕組みで、新旧クライアントの恒久的な住み分けを担っているわけではありませんでした。
6. 整理: デプロイの安全性と、クライアントの互換性は別問題
振り返ると、自分は次の2つを1つの仕組みだと思い込んでいました。
| 問題 | 解決する仕組み | 何を守るか |
|---|---|---|
| 新しいコードにバグがあったとき、いきなり全トラフィックに当たらないようにしたい | Argo RolloutsのBlue-Green(active/preview + 手動promote) |
デプロイの安全性。検証してから切り替える、すぐロールバックできる |
| 新旧のアプリバージョンが同時に同じバックエンドを叩いても壊れないようにしたい | APIバージョニング(v1/v2)とExpand-Contract | クライアントの互換性。強制アップデートできないモバイルアプリ特有の制約への対応 |
この2つは独立していて、片方があれば片方が要らなくなる関係でもありません。互換性が保証されていても、デプロイしたコード自体にバグがあれば意味が無いのでBlue-Greenは要りますし、Blue-Greenで安全にデプロイできても、後方互換の無いAPI変更を突然入れれば旧アプリは壊れます。
まとめ
- Blue-Greenという名前から、「固定の2環境が交代する」元祖のイメージを持ち込んでいたが、Argo Rolloutsの実装は「
active/previewという役割に、毎回使い捨てのReplicaSetを割り当てる」方式だった。 - 「バージョン別ルーティングで新旧APIを恒久的に使い分けている」という理解も誤りで、実際は「現在の候補バージョンだけpreview、それ以外は全部active」という単純な分岐だった。
- 自分で設計したはずの仕組みでも、時間が経つと名前のイメージに引っ張られて理解がズレていく。実機(
kubectl argo rollouts get rollout、Serviceのselector)で定期的に答え合わせをすることの大事さを再確認した。 - デプロイの安全性(Blue-Green)とクライアントの互換性(APIバージョニング)は別の問題で、どちらか一方で他方を代替することはできない。
個人開発の環境での構成なので、そのまま組織に持ち込む場合は、Rolloutのリビジョン履歴保持数やロールバック手順の承認プロセスを先に確認してください。
JQITのエンジニアの95%以上は未経験からの採用です。
よければコーポレートサイトにも遊びに来てください。
未経験から学べます!一緒に挑戦していきましょう![]()
noteやXもやってます↓