ローリングアップデート:全停止せずにデプロイする方法

想像してみてください。あなたのアプリケーションは5台のサーバーで稼働し、すべてのサーバーがユーザーにサービスを提供しています。今、重大なバグ修正を含む新しいバージョンをプッシュする必要があります。昔ながらの方法では、すべてのサーバーを停止し、新しいバージョンをデプロイして、再び起動するでしょう。しかし、それはダウンタイムを意味します。すべてのユーザーにエラーページや、永遠に終わらないローディングスピナーが表示されることになります。

そのアプローチは、午前3時に少数の人が使う内部ツールには有効かもしれません。しかし、実際のユーザーがいる本番アプリケーションでは、すべてを一度に停止することは選択肢になりえません。全員に同時に影響を与えずにソフトウェアを更新する方法が必要です。

問題:オール・オア・ナッシングのデプロイ

アプリケーションの実行中のインスタンスすべてを同時に置き換えると、何もトラフィックを処理していない時間帯が発生します。デプロイ自体が数秒で完了しても、その数秒が収益の損失、ユーザーの不満、トランザクションの失敗につながる可能性があります。高可用性が求められるアプリケーションにとって、この時間帯は許容できません。

核心的な問題は、すべてのサーバーを1つの単位として扱っていることです。一緒に停止し、一緒に更新し、一緒に起動します。新しいバージョンに問題があれば、すべてのユーザーに同時に影響が及びます。デプロイが失敗した場合、ユーザーがすでにエラーを目にしている中で、古いバージョンを戻すために慌てふためくことになります。

解決策:インスタンスを1つずつ置き換える

すべてを一度に更新する代わりに、サーバーを1台ずつ更新することができます。仕組みは次のとおりです。

次のシーケンス図は、ロードバランサーがインスタンス間で更新をどのように調整するかを示しています。

sequenceDiagram participant LB as Load Balancer participant I1 as Instance 1 participant I2 as Instance 2 participant I3 as Instance 3 LB->>I1: Drain traffic I1-->>LB: Acknowledged I1->>I1: Deploy v2.0 I1->>I1: Health check /ready I1-->>LB: Ready LB->>I1: Resume traffic LB->>I2: Drain traffic I2-->>LB: Acknowledged I2->>I2: Deploy v2.0 I2->>I2: Health check /ready I2-->>LB: Ready LB->>I2: Resume traffic LB->>I3: Drain traffic I3-->>LB: Acknowledged I3->>I3: Deploy v2.0 I3->>I3: Health check /ready I3-->>LB: Ready LB->>I3: Resume traffic
  1. バージョン1.0のアプリケーションを実行している5台のサーバーがあります。
  2. 1台のサーバーをサービスから切り離します。
  3. そのサーバーにバージョン2.0をデプロイします。
  4. 新しいバージョンが正しく動作していることを確認します。
  5. そのサーバーをサービスに戻します。
  6. 次のサーバーに移動して繰り返します。

このプロセスのどの時点でも、4台のサーバーは古いバージョンを実行し続け、1台のサーバーが新しいバージョンを実行しています。更新されたサーバーにヒットしたユーザーは新しいバージョンを、他のサーバーにヒットしたユーザーは古いバージョンを取得します。サーバーが完全に停止することはないため、エラーが発生するユーザーはいません。

このアプローチはローリングアップデートと呼ばれます。その名前は、更新がインスタンス間を次々と「転がる」ように進む様子に由来し、まるで波が野原を移動するかのようです。インスタンスとは、アプリケーションが実行される単位(物理サーバー、仮想マシン、コンテナなど)です。

ヘルスチェックが重要な理由

ローリングアップデートが機能するためには、次のインスタンスを更新する前に、新しいバージョンが実際に正しく実行されているかどうかを判断できなければなりません。ここでヘルスチェックが重要になります。

ヘルスチェックは、インスタンスがトラフィックを受け入れる準備ができているかどうかをテストするシンプルな仕組みです。通常は、アプリケーションが正常に動作しているときに成功レスポンスを返す /health/ready のようなエンドポイントです。Kubernetes、ロードバランサー、カスタムデプロイツールなどのオーケストレーションシステムは、インスタンスにトラフィックを送信する前にこのエンドポイントをチェックします。

新しいバージョンをデプロイした後にヘルスチェックが失敗した場合、ローリングアップデートは停止します。問題のあるインスタンスは古いバージョンにロールバックでき、残りのサーバーは影響を受けません。被害を1つのインスタンスと少数のユーザーに限定できます。

ヘルスチェックがなければ、手探りで進むことになります。新しいバージョンが起動時にクラッシュすることに気づかずに5台すべてのサーバーを更新してしまうかもしれません。その時点では、すべてのユーザーが影響を受けています。

ローリングアップデートが適しているケース

ローリングアップデートは、後方互換性のある変更に最適です。これは、古いバージョンと新しいバージョンが問題なく並行して実行できる変更のことです。例としては次のようなものがあります。

  • 新しいログステートメントの追加
  • データ形式を変更しない軽微なバグの修正
  • UIの色やテキストの変更
  • まだ誰も使用していない新しいAPIエンドポイントの追加
  • 動作を変更しない依存関係の更新

更新中は古いインスタンスと新しいインスタンスが同時に実行されるため、後方互換性は不可欠です。新しいバージョンが異なる形式のデータを期待したり、異なるプロトコルを使用して他のサービスと通信したりする場合、古いインスタンスと新しいインスタンスが連携しようとするとエラーが発生します。

トレードオフ

ローリングアップデートはシンプルで、追加のインフラストラクチャを必要としません。既存のサーバーをそのまま使用します。並行環境を立ち上げたり、追加のキャパシティをプロビジョニングしたりする必要はありません。更新中もインフラストラクチャコストは変わりません。

欠点は速度です。ローリングアップデートは時間がかかります。各インスタンスの更新と確認を待つ必要があるからです。50台のサーバーがあり、それぞれのデプロイとチェックに1分かかる場合、更新にはほぼ1時間かかります。緊急のセキュリティパッチの場合、これでは遅すぎるかもしれません。

もう1つの制限は可視性です。ローリングアップデートで問題が発生した場合、その影響は徐々に広がります。一部のユーザーは問題を目にし、他のユーザーは目にしません。これにより、影響を受けたユーザーと影響を受けていないユーザーを明確に分離できる戦略と比較して、根本原因の特定が難しくなります。

実践的なチェックリスト

ローリングアップデートを実装する前に、以下の基本事項が整っていることを確認してください。

  • ヘルスチェックエンドポイント:アプリケーションは、正常に動作していることを確認するための信頼性の高い方法を公開する必要があります。
  • 後方互換性:新しいバージョンは、移行中に古いバージョンと一緒に動作できなければなりません。
  • ロールバック計画:ヘルスチェックが失敗した場合に、1つのインスタンスを元に戻す方法を知っておいてください。
  • 監視:更新中にエラー率と応答時間を監視し、問題を早期に発見します。
  • 十分なインスタンス数:ローリングアップデートは、少なくとも3つのインスタンスがある場合に最も効果的です。2つだけの場合、更新中に容量の半分を失うことになります。

まとめ

ローリングアップデートは、ほとんどの最新アプリケーションにおけるデフォルトのデプロイ戦略です。これは、ダウンタイムなしでソフトウェアを更新するという根本的な問題を解決するからです。アイデアはシンプルです。すべてを一度に置き換えるのではなく、1つのインスタンスを置き換え、動作を確認してから次のインスタンスに進みます。このアプローチにより、更新中もアプリケーションの可用性が維持され、何か問題が発生した場合の影響範囲を限定できます。

小さな後方互換性のある変更には、ローリングアップデートで十分なことがよくあります。シンプルで、コスト効率が良く、Kubernetesのようなコンテナオーケストレーションプラットフォームやクラウドデプロイツールで広くサポートされています。しかし、データベースマイグレーション、プロトコル変更、大規模な機能見直しなど、リスクの高い変更には、より細かい制御が必要になる場合があります。そのような場合は、ブルーグリーンデプロイメントやカナリアリリースといった戦略が役立ちます。ただし、それはまた別の機会に説明します。