本番投入前に本当のフィードバックを得たいなら

チームが新しいレコメンドエンジンを開発したとしよう。ステージング環境では完璧に動いている。テストも通っている。プロダクトチームは興奮している。しかし、心のどこかで「ステージングのトラフィックは本物とは違う」と分かっている。ユーザーは奇妙なデータを持ち、異常なパターンを示し、誰も予想しなかった行動をとるものだ。

全ユーザーに一度にデプロイすることもできる。しかし、もし問題があれば、すべてのユーザーがその影響を受ける。ブルー/グリーンスワップを行い、必要なら素早くロールバックすることもできる。しかし、それでも新しいバージョンが実際の負荷でどう振る舞うかは、全員が移行するまで分からない。

本当に必要なのは、一部のユーザーだけに新しいバージョンを先に試してもらい、残りのユーザーは古いバージョンを使い続ける方法だ。もし問題が起きても、影響を受けるのはごく一部のユーザーだけ。うまくいけば、徐々に利用者を増やしていける。

それがカナリアデプロイメントだ。

名前の由来は炭鉱から

炭鉱の初期の頃、作業員はトンネルにカナリアを持ち込んでいた。この鳥は一酸化炭素などの有毒ガスに敏感だ。カナリアが鳴くのをやめたり死んだりしたら、鉱夫は危険を知り、手遅れになる前に避難できた。

カナリアデプロイメントも同じように機能する。新しいバージョンのアプリケーションを、まずは一部のユーザーにだけ導入する。新しいバージョンに問題があれば、影響を受けるのはその数人のユーザーだけで、すぐに引き戻せる。問題なく動作すれば、徐々に利用者を拡大していく。

カナリアは新しいバージョンそのものではない。カナリアとは、それをテストしてくれる少数のユーザーグループのことだ。

実際の仕組み

アプリケーションが10個のPodでKubernetes上で動いているとしよう。通常は10個すべてのPodが全ユーザーにサービスを提供する。カナリアデプロイメントでは、新しいバージョンを実行するPodを1〜2個立ち上げる。そして、トラフィックのごく一部(5%や10%など)をその新しいPodにルーティングする。残りのトラフィックは引き続き古いバージョンに送られる。

この間、チームは主要なメトリクス(エラー率、応答時間、スループット、ユーザーレポート)を監視する。新しいバージョンが一定時間問題なく動作しているように見えたら、トラフィックの割合を増やす。何か問題が発生したら、すべてのトラフィックを古いバージョンに戻す。

以下のフローチャートは、その判断プロセスを示している:

flowchart TD A[開始] --> B[5%のトラフィックを新バージョンへ] B --> C[メトリクス監視: エラー、レイテンシ] C --> D{正常か?} D -->|Yes| E[10%に増加] D -->|No| F[古いバージョンにロールバック] E --> G[再度監視] G --> H{正常か?} H -->|Yes| I[25%に増加] H -->|No| F I --> J[50%に増加] J --> K[100%に増加] K --> L[カナリア完了]

これはローリングアップデートとは異なる。ローリングアップデートでは、どのユーザーが新しいバージョンにヒットするかを制御せずに、インスタンスを1つずつ置き換えていく。たまたま更新されたインスタンスにアクセスしたユーザーは、すぐに新しいコードを使うことになる。カナリアデプロイメントでは、新しいバージョンにどれだけのトラフィックを流すかを明示的に制御でき、その割合をいつでも変更できる。

トラフィック分割の場所

トラフィックを分割する仕組みは、インフラストラクチャスタックによって異なる。

ネットワーク層では、HAProxyやNGINXなどのロードバランサーが、新しいバージョンへのリクエストの割合をルーティングできる。これはシンプルで、ほとんどのセットアップで機能する。

サービスメッシュ層では、IstioやLinkerdなどのツールがより細かい制御を提供する。HTTPヘッダー、Cookie、特定のユーザー属性に基づいてトラフィックを分割できる。これにより、内部テスター、ベータユーザー、特定の地域のユーザーを対象に、他のユーザーに影響を与えずにテストできる。

アプリケーション自体がトラフィック分割を実装する場合もある。アプリケーション自身がユーザーIDやアカウントタイプに基づいて、どのバージョンを提供するかを決定する。このアプローチは最大の柔軟性を提供するが、コードベースに複雑さを追加する。

カナリアデプロイメントが輝くとき

カナリアデプロイメントは、中程度から高リスクの変更に最も有用だ。ステージングではデータやトラフィックパターンが本番と完全に一致しないため、完全に検証することが難しい変更である。

例としては以下が挙げられる:

  • レコメンドアルゴリズムの置き換え
  • 支払いロジックの更新
  • アプリケーションとデータベースの通信方法の変更
  • 新しいキャッシュ層の導入
  • 認証または認可フローの変更

このような変更に対して、カナリアデプロイメントは実世界での検証を通じて自信を与える。実際のユーザーデータ、実際のトラフィックパターン、実際のインフラストラクチャ条件で新しいバージョンがどう振る舞うかを、ごく一部のユーザーだけで確認できる。

実際の課題

カナリアデプロイメントは魔法の弾丸ではない。それ自体に一連の要件とリスクが伴う。

優れた可観測性が必要だ。 新旧バージョンのエラー率、レイテンシ、スループットを比較するダッシュボードがなければ、手探りで進むことになる。カナリアグループがコントロールグループよりも多くのエラーや遅い応答を経験しているかどうかを、数分以内に把握する必要がある。

一部のユーザーは異なる体験をすることになる。 新しいバージョンが劣っている場合、そのユーザーが最初に感じることになる。これは、早期フィードバックを得るために受け入れるトレードオフだ。問題が発生した場合の影響が許容範囲内になるよう、カナリアグループは十分に小さく保つこと。

セッションと状態の管理が厄介になる。 アプリケーションがユーザーセッションや状態を維持している場合、トラフィック分割によってそれらのセッションが壊れる可能性がある。ユーザーが古いバージョンでリクエストを開始し、次のリクエストで新しいバージョンに到達するかもしれない。セッションデータに互換性があるか、トラフィック分割がセッションアフィニティを尊重するようにする必要がある。

可観測性自体が課題になることもある。 監視ツールがすべてのインスタンスのメトリクスを集約している場合、古いバージョンと新しいバージョンを区別できない。バージョンまたはデプロイメントラベルでタグ付けされたメトリクスが必要だ。

セーフティネットの自動化

多くのチームは、カナリアデプロイメントと自動観測を組み合わせている。誰かが常にダッシュボードを監視する代わりに、しきい値を設定する。新しいバージョンのエラー率が古いバージョンより1%以上高くなった場合、パイプラインは自動的にカナリアを停止し、すべてのトラフィックを古いバージョンに戻す。

この自動化により、カナリアデプロイメントははるかに安全になる。システムは、人間が問題に気づき、調査し、ロールバックを決定するのを待たずに、自分自身を保護する。

段階的な拡大

新しいバージョンが安定しているように見えたら(たとえば1時間問題がない場合)、トラフィックの割合を段階的に増やす。一般的なステップは25%、50%、そして100%だ。各ステップで同じメトリクスを監視し、何も変わっていないことを確認する。

すべてのトラフィックが新しいバージョンに移行したら、カナリアデプロイメントは完了だ。古いバージョンはスケールダウンして削除できる。

実践的なクイックチェックリスト

カナリアデプロイメントを実装する前に、以下の要素が整っていることを確認すること:

  • トラフィック分割メカニズム(ロードバランサー、サービスメッシュ、またはアプリケーションロジック)
  • バージョンごとにタグ付けされたメトリクス(エラー率、レイテンシ、スループット)
  • 新旧バージョンのメトリクスをリアルタイムで比較するダッシュボード
  • 自動ロールバックのしきい値(例:エラー率の増加が1%を超えた場合)
  • バージョン間のセッションアフィニティまたは状態互換性
  • カナリアグループ(ユーザーが識別可能な場合)へのコミュニケーション計画

具体的なポイント

カナリアデプロイメントは、派手なツールや複雑な設定が目的ではない。変更による影響範囲を減らすことだ。実際のユーザーの小さなグループに、実際の条件下で作業を検証してもらい、その後で全員に適用する。この手法は、単純な真実を受け入れているからこそ機能する。ステージング環境がどれだけ優れていても、本番環境は必ず見落としを見つけるものだ。カナリアデプロイメントは、本番環境がその何かを見つけたときに、影響を受けるユーザーが全員ではなく、ごく一部であることを保証する。