Skip to content
Published on

Ingress トラフィック分割 — カナリア・ブルーグリーンデプロイ実践

Authors

はじめに

新しいバージョンを本番にデプロイする作業は、いつでも緊張するものです。単体テストと結合テストをすべて通過していても、実トラフィックの前でしか現れない問題が確かに存在するからです。メモリリーク、特定のクライアントでのみ再現するシリアライズのバグ、想定より遅い外部 API の応答といったものは、ステージングではなかなか捕まえられません。

こうしたリスクを下げる最も実証された方法が、段階的デリバリーです。代表例は、新バージョンに全体トラフィックの1パーセントだけを流し、指標が正常か確認したうえで5パーセント、25パーセント、50パーセントへとゆっくり増やしていくカナリア(canary)デプロイです。問題が見つかればすぐに0パーセントへ戻せます。影響範囲が小さく、ロールバックが速いのです。

Kubernetes 環境でこのトラフィック分割を実装する中心的な地点が、まさに Ingress 層です。クラスタへ入ってくる外部トラフィックがどのバックエンドへ向かうかを決める場所だからです。本記事では ingress-nginx のカナリアアノテーションから始め、Argo Rollouts のような自動化ツールとの連携、コントローラごとのトラフィック分割方式の違い、ブルーグリーンパターン、そしてメトリクスベースの自動昇格までを、現場ですぐ使える形でまとめます。

まず重要な文脈を一つ押さえておきます。2026年現在、Ingress API は事実上 frozen 状態です。もはや新機能は追加されず、Kubernetes ネットワーキングの後継標準は Gateway API です。ingress-nginx プロジェクトもメンテナンスモードへ移行し、主にセキュリティパッチ中心で運用されています。それでも現場には依然として膨大な量の Ingress ベースのインフラが稼働しているため、既存資産を安全に運用しつつ Gateway API ネイティブなトラフィック分割へ移行する、両方の観点を扱います。

トラフィック分割の基本概念

トラフィック分割は、同じホストやパスへ届いたリクエストを複数のバックエンドバージョンへ分けて送る手法です。大きく二つの軸に分けられます。

一つ目は重み付け(weight-based)分割です。全リクエストのうち一定の割合を新バージョンへ送ります。たとえば90対10なら、10回に1回ほど新バージョンが応答します。ランダムな分配なので、実際のユーザー行動とは無関係に均等なサンプルを得られます。

二つ目はルールベース(rule-based)分割です。特定のヘッダー、クッキー、クライアントの特性に応じてルーティングします。たとえば社内従業員のリクエストにだけベータ版を見せたり、特定のクッキーを持つユーザーだけに新しい UI を体験させたりします。これは重み付け分割より制御性が高い一方、サンプルが偏りやすいという欠点があります。

                         ┌─────────────────────┐
   ユーザーリクエスト ──▶ │  Ingress コントローラ │
                         └──────────┬──────────┘
                                    │ 重み/ヘッダー/クッキーで判断
                       ┌────────────┴────────────┐
                       ▼                          ▼
              ┌─────────────────┐       ┌─────────────────┐
              │  stable (v1)    │       │  canary (v2)    │
              │  90% トラフィック│       │  10% トラフィック│
              └─────────────────┘       └─────────────────┘

カナリアとブルーグリーンは、このトラフィック分割を活用する代表的な二つの戦略です。カナリアは比率を段階的に増やしてリスクを分散し、ブルーグリーンは二つの環境を同時に立ち上げて一度に切り替えます。それぞれの長所と短所は記事の後半で詳しく比較します。

ingress-nginx カナリアアノテーション詳細

ingress-nginx は別途の CRD なしに、アノテーションだけでカナリアデプロイを実装できます。核心は、同じホストとパスを持つ Ingress を二つ作り、そのうち一方にカナリアアノテーションを付ける構造です。

基本構造

まず安定(stable)バージョンを指す通常の Ingress があります。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-stable
  namespace: production
spec:
  ingressClassName: nginx
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: app-v1
                port:
                  number: 80

そして同じホストとパスを持ちつつ、カナリアアノテーションが付いた二つ目の Ingress を追加します。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-canary
  namespace: production
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
  ingressClassName: nginx
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: app-v2
                port:
                  number: 80

こうすると app.example.com へ入るトラフィックの10パーセントが app-v2 へ、残り90パーセントが app-v1 へ流れます。canary-weight の値を変えるだけで比率が即座に調整されます。

canary-by-header アノテーション

特定のヘッダーを持つリクエストだけをカナリアへ送りたいときに使います。重み付けより精密に制御できるため、QA チームや社内ユーザー向けテストに適しています。

metadata:
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-by-header: "X-Canary"
    nginx.ingress.kubernetes.io/canary-by-header-value: "always"

この設定では、X-Canary ヘッダーの値が always のリクエストだけがカナリアバックエンドへ向かいます。header-value を指定しない場合は既定で always と never を認識し、always ならカナリアへ、never なら必ず安定バージョンへ送ります。ヘッダー値に正規表現マッチングを適用したい場合は canary-by-header-pattern アノテーションを使えます。

metadata:
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-by-header: "X-Region"
    nginx.ingress.kubernetes.io/canary-by-header-pattern: "ap-.*"

クッキーベースの分割は、一度カナリアへルーティングされたユーザーがセッション中ずっと同じバージョンを体験するようにしたいときに有用です。

metadata:
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-by-cookie: "canary-user"

canary-user という名前のクッキー値が always ならカナリアへ、never なら安定バージョンへ向かいます。クッキー値がないか別の値なら、重みルールへフォールバックします。

アノテーションの優先順位

複数のカナリアアノテーションを同時に使う場合、評価順序が決まっています。これを知らないと意図と異なるルーティングが発生する恐れがあります。

優先順位アノテーション動作
1 (最優先)canary-by-headerヘッダー一致時に即座にカナリア/安定を決定
2canary-by-cookieクッキー値で決定
3 (最後)canary-weight上のルールに当たらなければ重みで確率分配

つまりヘッダールールが最初に評価され、次にクッキー、最後に重みの順です。ヘッダーで always が指定されると、重みが0でも該当リクエストはカナリアへ行きます。この点はトラブルシュート時によく混同を招くので、必ず覚えておくとよいでしょう。

アノテーション方式の限界

アノテーション方式は設定が簡単ですが、いくつかの明確な限界があります。重みを変えるには毎回 Ingress リソースを修正して適用する必要があるため、自動化された段階的昇格には不向きです。またカナリア Ingress は同一ホスト/パスごとに一つしか許容されないため、三つ以上のバージョンを同時に比較するシナリオは実装できません。これらの限界のため、現場では Argo Rollouts のようなツールで重み調整を自動化することが多いです。

Argo Rollouts と Ingress 連携

Argo Rollouts は Kubernetes の Deployment を置き換える Rollout という CRD を提供します。重みを段階的に上げ、各段階で一時停止したり自動昇格したりする作業を、コントローラが自動で処理します。ingress-nginx と連携すると、先ほど見た canary-weight アノテーションを Argo Rollouts が自動で調整してくれます。

Rollout CRD の基本構造

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: app
  namespace: production
spec:
  replicas: 5
  selector:
    matchLabels:
      app: app
  template:
    metadata:
      labels:
        app: app
    spec:
      containers:
        - name: app
          image: registry.example.com/app:v2
          ports:
            - containerPort: 80
  strategy:
    canary:
      canaryService: app-canary
      stableService: app-stable
      trafficRouting:
        nginx:
          stableIngress: app-stable
      steps:
        - setWeight: 5
        - pause: { duration: 5m }
        - setWeight: 25
        - pause: { duration: 5m }
        - setWeight: 50
        - pause: { duration: 10m }
        - setWeight: 100

ここで核心は trafficRouting.nginx.stableIngress フィールドです。Argo Rollouts はこの Ingress を基準にカナリア Ingress を自動生成し、steps に定義された重みに合わせて canary-weight アノテーションを更新します。setWeight 5 は5パーセント、pause は当該時間だけ待機を意味します。duration のない pause は、人が手動で昇格するまで無限に待機します。

Service 構成

Rollout が動作するには、安定バージョンとカナリアバージョンをそれぞれ指す二つの Service が必要です。

apiVersion: v1
kind: Service
metadata:
  name: app-stable
  namespace: production
spec:
  selector:
    app: app
  ports:
    - port: 80
      targetPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: app-canary
  namespace: production
spec:
  selector:
    app: app
  ports:
    - port: 80
      targetPort: 80

Argo Rollouts コントローラはロールアウト進行中にこの二つの Service のセレクタを動的に管理し、安定ポッドとカナリアポッドをそれぞれ正しい Service へ接続します。運用者は Service のセレクタを直接触る必要はありません。

ロールアウトの進行フロー

   デプロイ開始
   setWeight 5  ──▶ カナリア5%、5分待機
   setWeight 25 ──▶ カナリア25%、5分待機
   setWeight 50 ──▶ カナリア50%、10分待機
   setWeight 100 ─▶ 全量カナリア、昇格完了
   stable へ昇格 (イメージ差し替え確定)

各段階で問題が検知されれば、kubectl argo rollouts abort コマンドで即座に中断し、重みを0へ戻せます。

# ロールアウト状態をリアルタイム確認
kubectl argo rollouts get rollout app -n production --watch

# 手動昇格 (pause 段階で)
kubectl argo rollouts promote app -n production

# 中断とロールバック
kubectl argo rollouts abort app -n production

コントローラごとのトラフィック分割方式の比較

トラフィック分割を実装する方式は、Ingress コントローラごとにかなり異なります。アノテーションを使うものもあれば、専用 CRD を使うものもあります。マイグレーションやマルチコントローラ環境で混同を防ぐには、違いを正確に知る必要があります。

コントローラ分割方式重みヘッダー/クッキー備考
ingress-nginxアノテーション対応対応ホストごとにカナリア一つ制限
Traefikweighted services (CRD)対応ミドルウェアで一部IngressRoute または TraefikService 使用
HAProxy Ingressアノテーション対応ヘッダー対応blue-green アノテーション別途提供
ContourHTTPProxy (CRD)対応ヘッダー対応weight をバックエンドに直接指定

Traefik の weighted services

Traefik はアノテーションの代わりに TraefikService という CRD で重み付け分割を表現します。複数のサービスを束ねて比率を指定する構造です。

apiVersion: traefik.io/v1alpha1
kind: TraefikService
metadata:
  name: app-split
  namespace: production
spec:
  weighted:
    services:
      - name: app-v1
        port: 80
        weight: 90
      - name: app-v2
        port: 80
        weight: 10

こう定義した TraefikService を IngressRoute でバックエンドとして参照すると、90対10の比率でトラフィックが分かれます。重みは絶対値ではなく相対比率として解釈されるため、合計が100である必要はありません。

Contour の HTTPProxy

Contour は HTTPProxy CRD でバックエンドの weight フィールドを直接指定します。

apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata:
  name: app
  namespace: production
spec:
  virtualhost:
    fqdn: app.example.com
  routes:
    - conditions:
        - prefix: /
      services:
        - name: app-v1
          port: 80
          weight: 90
        - name: app-v2
          port: 80
          weight: 10

Contour も weight を相対比率として処理します。一つ注意すべき点は、weight を明示しないサービスの既定値はコントローラの実装によって異なり得るため、すべてのバックエンドに明示的に重みを指定するのが安全です。

方式選択ガイド

アノテーション方式は参入障壁が低いものの表現力が限定的です。CRD 方式はより豊かな表現が可能で宣言的ですが、別途のリソースタイプを学ぶ必要があります。自動化ツール(Argo Rollouts、Flagger)を導入する計画があるなら、そのツールがどのコントローラのどの方式を対応しているかを先に確認するとよいでしょう。そして長期的には、これらすべてのベンダー別方式が Gateway API の標準 weight フィールドへ収束しつつある点を念頭に置く必要があります。

ブルーグリーンパターン

ブルーグリーンはカナリアとは異なるアプローチです。新バージョン(green)を安定バージョン(blue)と完全に同じ規模で事前に立ち上げておき、検証が終わればトラフィックを一度に切り替えます。段階的な比率調整がないため、切り替え瞬間のリスクはカナリアより大きいものの、すべてのユーザーが常に一貫したバージョンを見ることになり、ロールバックが非常に速いという長所があります。

サービススイッチング方式

最も単純なブルーグリーン実装は、Service のセレクタを変えることです。

apiVersion: v1
kind: Service
metadata:
  name: app
  namespace: production
spec:
  selector:
    app: app
    version: blue
  ports:
    - port: 80
      targetPort: 80

green 環境の検証が終われば、セレクタの version を green へ変えて適用すると、Ingress 設定はそのままにバックエンドだけ即座に切り替わります。ロールバックが必要なら version を blue へ戻すだけです。

Argo Rollouts の blueGreen 戦略

Argo Rollouts はブルーグリーンも宣言的に対応します。

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: app
  namespace: production
spec:
  replicas: 5
  selector:
    matchLabels:
      app: app
  template:
    metadata:
      labels:
        app: app
    spec:
      containers:
        - name: app
          image: registry.example.com/app:v2
  strategy:
    blueGreen:
      activeService: app-active
      previewService: app-preview
      autoPromotionEnabled: false
      scaleDownDelaySeconds: 300

activeService は実際の運用トラフィックを受ける Service で、previewService は新バージョンを事前に検証できる別の Service です。autoPromotionEnabled を false にすると、preview で十分検証したあと人が手動で昇格するまで、アクティブトラフィックは既存バージョンを維持します。scaleDownDelaySeconds は切り替え後に以前のバージョンのポッドをどれだけ維持するかを定め、速いロールバックのため一定時間生かしておくのがよいでしょう。

# preview 検証後にアクティブ切り替え
kubectl argo rollouts promote app -n production

メトリクスベースの自動昇格

手動昇格は安全ですが、人の判断と待機時間が必要です。運用規模が大きくなると、メトリクスを基準に自動昇格または自動ロールバックする仕組みが必要になります。Argo Rollouts は AnalysisTemplate でこれを対応します。

AnalysisTemplate の定義

Prometheus クエリで成功率を測定し、しきい値未達時にロールアウトを失敗扱いにする例です。

apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: success-rate
  namespace: production
spec:
  args:
    - name: service-name
  metrics:
    - name: success-rate
      interval: 1m
      count: 5
      successCondition: result[0] >= 0.99
      failureLimit: 2
      provider:
        prometheus:
          address: http://prometheus.monitoring:9090
          query: |
            sum(rate(http_requests_total{service="app-canary",status!~"5.."}[2m]))
            /
            sum(rate(http_requests_total{service="app-canary"}[2m]))

successCondition は成功率99パーセント以上を要求します。interval は測定周期、count は測定回数、failureLimit は許容する失敗回数です。5回の測定のうち失敗が2回を超えると、ロールアウトが自動で中断され重みが0へ復帰します。

Rollout に分析を接続

  strategy:
    canary:
      canaryService: app-canary
      stableService: app-stable
      trafficRouting:
        nginx:
          stableIngress: app-stable
      steps:
        - setWeight: 10
        - pause: { duration: 5m }
        - analysis:
            templates:
              - templateName: success-rate
            args:
              - name: service-name
                value: app-canary
        - setWeight: 50
        - pause: { duration: 10m }
        - setWeight: 100

これでカナリア10パーセント段階のあと自動で成功率分析が実行され、基準を満たして初めて次の段階へ進みます。人の介入なしに安全な段階的デプロイが完成します。Flagger も似た思想でメトリクスベースの自動昇格を提供するので、GitOps ワークフローに応じて選べばよいでしょう。

Gateway API の weight ベースルーティング

前述のとおり Ingress API は frozen 状態であり、重み付けトラフィック分割は Gateway API で標準機能として提供されます。ingress-nginx のアノテーションやコントローラ別 CRD がしていた仕事を、HTTPRoute の backendRefs weight フィールド一つで標準化して表現できます。

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: app
  namespace: production
spec:
  parentRefs:
    - name: main-gateway
  hostnames:
    - "app.example.com"
  rules:
    - backendRefs:
        - name: app-v1
          port: 80
          weight: 90
        - name: app-v2
          port: 80
          weight: 10

この一行の weight フィールドがベンダー依存のアノテーションを置き換えます。Argo Rollouts もすでに trafficRouting.plugins を通じて Gateway API を対応しているため、自動化されたカナリアワークフローを標準 API の上で構成できます。新規プロジェクトなら最初から Gateway API ベースで設計することを推奨し、既存の Ingress 資産は ingress2gateway のようなツールで段階的に移行できます。

ヘッダーベースの分割も Gateway API では標準マッチャーで表現されます。

  rules:
    - matches:
        - headers:
            - name: X-Canary
              value: "always"
      backendRefs:
        - name: app-v2
          port: 80
    - backendRefs:
        - name: app-v1
          port: 80

落とし穴とトラブルシューティング

現場でトラフィック分割を運用していると、繰り返し出くわす落とし穴があります。あらかじめ知っておくとデバッグ時間を大きく減らせます。

ingress-nginx のセッションアフィニティ(sticky session)機能をオンにすると、クライアントが一度特定のバックエンドへ固定されます。ところがカナリアと安定バージョンは別個の Ingress なので、sticky cookie がカナリアルーティングと衝突し、重みが意図どおり動作しないことがあります。カナリアテスト中はアフィニティ設定を慎重に扱い、可能ならカナリア期間中はアフィニティを無効化するか、別のクッキー名を使うのが安全です。

metadata:
  annotations:
    nginx.ingress.kubernetes.io/affinity: "cookie"
    nginx.ingress.kubernetes.io/session-cookie-name: "stable-route"

重み合計の誤解

ingress-nginx の canary-weight は0から100までのパーセンテージで、カナリアへ送る比率を直接指定します。一方 Traefik と Contour の weight は相対比率なので合計が100である必要はありません。この違いを混同すると、想定とまったく異なる分配が起こります。たとえば Contour で50と50に設定すると半分ずつ分かれますが、同じ意味で ingress-nginx に canary-weight 50 を与えるとカナリアに50パーセントだけ行き安定バージョンが50パーセントを受けるのは偶然同じなだけで、動作原理が異なります。コントローラを変えるときは重みの意味を必ず再確認すべきです。

canary-weight-total 設定

ingress-nginx は既定で重みの総和を100と見なしますが、canary-weight-total アノテーションで分母を変えられます。より細かい比率(たとえば1000分の5)が必要なときに使います。

metadata:
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "5"
    nginx.ingress.kubernetes.io/canary-weight-total: "1000"

この設定は1000分の5、すなわち0.5パーセントをカナリアへ送ります。非常に低い比率のカナリアが必要な大規模トラフィック環境で有用です。

カナリア Ingress が無視される場合

カナリア Ingress のホストとパスが安定 Ingress と正確に一致しないと、カナリアアノテーションが無視され、単に別個の Ingress として扱われます。ホスト、パス、pathType がすべて同一かを必ず確認すべきです。また二つの Ingress が同じ ingressClassName を持って初めて、同じコントローラが処理します。

メトリクス遅延による誤った昇格

自動昇格時、Prometheus のスクレイプ間隔と分析 interval が合わないと、十分なデータが溜まる前に分析が通過してしまうことがあります。分析 count と interval をスクレイプ周期より余裕をもって取り、トラフィックが少ない時間帯にはサンプル不足による偽陽性を警戒すべきです。

運用チェックリスト

実践デプロイ前に次の項目を点検すれば、事故を大きく減らせます。

  • 安定/カナリア Service がそれぞれ正しいセレクタで分離されているか
  • カナリア Ingress のホスト、パス、pathType、ingressClassName が安定バージョンと正確に一致するか
  • 重みアノテーションの意味(パーセンテージ対相対比率)をコントローラごとに確認したか
  • セッションアフィニティがオンなら、カナリア期間中の影響を検討したか
  • 自動昇格を使うなら AnalysisTemplate の successCondition と failureLimit が適切か
  • Prometheus クエリがカナリアバックエンドだけを正確に選択するか
  • ロールバック手順(abort コマンド、Service セレクタ復旧)をチームが熟知しているか
  • ブルーグリーンなら scaleDownDelaySeconds で以前のバージョンを十分に維持するか
  • 監視ダッシュボードでバージョン別の指標を分離して見られるか
  • 長期的に Gateway API への移行計画が立てられているか

おわりに

トラフィック分割は、デプロイのリスクを制御可能な水準まで下げる最も実用的なツールです。ingress-nginx の簡単なアノテーションから始め、Argo Rollouts で段階的昇格を自動化し、メトリクスベースの分析で人の介入まで減らす、段階的な成熟の道筋を描けます。

ただし2026年の現実は、Ingress API が frozen 状態であり、ingress-nginx がメンテナンスモードへ入ったという点です。既存資産は安定的に運用しつつ、新しいトラフィック分割の標準は Gateway API の weight ベースルーティングにあります。backendRefs の weight フィールド一つでベンダー依存を取り除き、Argo Rollouts の Gateway API プラグインで同じ自動化を標準の上で実装できます。いま運用中のカナリアパイプラインを点検しつつ、同時に Gateway API への移行ロードマップを描いておくのが賢明な選択です。

参考資料