B システム設計/インフラ — banner-api GCE Ingress→Gateway API移行(HTTPRoute重み付けトラフィック分割 × HealthCheckPolicy × GCPBackendPolicy(Cloud Armor接続) × DataDogメトリクスベース段階的カナリア昇格(10%→50%→100%)・自動ロールバック)(MOps 販促システムチーム バナー配信API カナリアリリース事故 Bad→Good)

2026-08-25 (Day 141) 火曜 B: システム設計/インフラ ★★★★☆ GKE Gateway API / HTTPRoute Cloud Armor / HealthCheckPolicy / DataDog

概要

🎚️

HTTPRoute のネイティブ重み付け分割

nginx形式のカナリアアノテーションはGCE Ingressでは解釈されず黙って無視される。Gateway APIのHTTPRouteはbackendRefs[].weightをコントローラー非依存の標準機能として持つ。

🩺

HealthCheckPolicy でreadiness実態に合わせる

GCEデフォルトのヘルスチェック(パス/)はアプリの/healthzセマンティクスと一致しない。未readyなPodへのルーティングを構造的に防ぐ。

🛡️

GCPBackendPolicy でCloud Armor接続

公開APIには常時セキュリティポリシーが必要。GCPBackendPolicyでCloud Armorをバックエンドサービスへ紐付け、リリース中も防御レベルを維持する。

🤖

メトリクスベースの自動昇格・自動ロールバック

「Slackで苦情が来たら気づく」から、DataDogのerror_rateを機械的にポーリングし、閾値以下なら自動昇格・超過なら自動ロールバックする仕組みへ切り替える。

問題

ECサイト MOps 販促システムチームが運用するバナー配信API(banner-api)(トップページのタイムセールバナーを返す、GKE Autopilot上の公開APIで社内外から高頻度に呼ばれる)は、これまでGCE Ingress(kubernetes.io/ingress.class: gce)+ nginx形式のカナリアアノテーションでリリースを行ってきた。

先週起きたインシデント: 新バージョンのカナリアリリースを「まず10%のトラフィックだけ流す」つもりでnginx.ingress.kubernetes.io/canary-weight: "10"を設定してデプロイしたところ、実際には新バージョンにトラフィックの100%が即座に流れ、5分間エラー率が急上昇した。原因調査の結果、GCE Ingressコントローラーはnginx-ingress形式のカナリアアノテーションを解釈できず黙って無視すること、そして実際のトラフィック切替はServiceのselectorラベルを新Deploymentへ直接書き換える方式で行われており、これは重み付けではなくアトミックな全量切替だったことが判明した。

問題点を全て洗い出し、GKE Gateway API(HTTPRouteの重み付けトラフィック分割)× HealthCheckPolicy × GCPBackendPolicy(Cloud Armor接続) × 段階的カナリア(10%→50%→100%) × DataDogメトリクスに基づく自動ロールバックを考慮したBad→Goodリファクタリングを行ってください。

制約・前提条件

  • GKE Autopilot 1.30+、Terraform 1.8+(google プロバイダー 5.x)、リージョン asia-northeast1
  • banner-apiは公開APIであり、L7攻撃・ボットスクレイピング対策としてCloud Armorセキュリティポリシーの適用が必須要件
  • readiness probeは/healthz(起動後の依存先接続確認を含み、起床直後は失敗する想定)
  • カナリア昇格の判断基準: DataDog上のerror_rate(5xx比率)が閾値(1%)を超えなければ次の重みへ自動昇格、超えた場合は自動的に旧バージョンへ100%戻す
  • 2026年時点のGKEロードマップでは、classic Ingress(GCE Ingress)は新規機能追加が非推奨とされ、Gateway APIが標準の移行先として位置付けられている
期待する回答形式: 問題点の列挙(番号付き)+ 改善後 Gateway API構成(Terraform/YAML: Gateway・HTTPRoute・HealthCheckPolicy・GCPBackendPolicy含む)+ 段階的カナリア昇格・自動ロールバックの運用フロー(設計意図の説明)

悪い構成 (Before)

この構成には 7つの設計上の問題 が隠れています。見つけてみてください。
新人エンジニアが提出した現行構成
# 問題①: GCE Ingressはnginx形式のカナリアアノテーションを解釈しない(黙って無視)
# 問題②: 実際の切替はService selector直接書き換え(アトミックな全量切替)
# 問題③: ヘルスチェックがGCEデフォルトのままでアプリの/healthzと不一致
# 問題④: 公開APIにCloud Armorセキュリティポリシー未接続
# 問題⑤: 昇格・ロールバック判断がSlack目視の人間依存
# 問題⑥: GKEロードマップでGCE Ingress新規機能は非推奨なのに新機能を作り込み
# 問題⑦: 重み・監視期間・エラー率閾値が暗黙的でレビュー不可能

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: banner-api-ingress
  annotations:
    kubernetes.io/ingress.class: "gce"
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"   # 解釈されない
spec:
  rules:
    - http:
        paths:
          - path: /banners
            pathType: Prefix
            backend:
              service:
                name: banner-api-svc
                port:
                  number: 80
---
# 実際のカナリア切替はこちらのコマンドで実施していた
# kubectl patch service banner-api-svc \
#   -p '{"spec":{"selector":{"version":"v2"}}}'
問題点サマリー(7点)
1nginxカナリアアノテーションが黙って無視される — GCE Ingressコントローラーは非対応
2Service selector書き換え=アトミック全量切替 — 重み付け分割が原理的に不可能
3ヘルスチェックがGCEデフォルト — アプリの/healthzと不一致
4Cloud Armor未接続 — 公開APIが無防備
5昇格・ロールバックが人間の目視依存 — 検知遅延・属人化
6非推奨のGCE Ingress上に新機能を作り込み — 技術的負債が増加
7重み・閾値が暗黙的 — コードやレビュー記録に残らない

ヒント(段階的開示)

ヒント1 — 方向性
問題は3層に分類できる。(1) 選んだ道具とやりたいことの不一致 — nginx-ingress形式のアノテーションはGCE Ingressコントローラー向けの機能ではないため、canary-weightを書いても静かに無視される。「エラーも警告も出ないが、意図通りに動いていない」設定は最も気づきにくい種類のバグである。(2) 重み付け分割そのものが存在しない — Service selectorの書き換えは「Aを捨ててBにする」というスイッチであって、「Aを90%・Bを10%」という分割ではない。段階的カナリアを実現するには、ロードバランサ自身がトラフィックを比率で振り分ける機能を持つ道具に置き換える必要がある。(3) 昇格・撤退の判断が人間の目視に依存している — 「Slackで苦情が来たら気づく」という運用は、検知が遅れるだけでなく、判断基準が属人化し再現性がない。これら3層を解決する道具として、GCPが2026年時点で標準の移行先と位置付けるGateway APIのHTTPRoute重み付けルーティングと、メトリクスに基づく自動昇格・自動ロールバックの仕組みを考えてみてください。
ヒント2 — アプローチ
  • 問題①: nginx.ingress.kubernetes.io/canary-weightをGCE Ingressに設定 → コントローラー非対応のアノテーションは黙って無視される。Gateway APIのHTTPRouteはbackendRefs[].weightがネイティブ機能として存在し、コントローラーに依存せず重み付け分割が効く
  • 問題②: Service selectorの直接書き換えによる切替 → アトミックな全量切替であり「10%だけ試す」が原理的に不可能。HTTPRouteでbanner-api-stable Serviceとbanner-api-canary Serviceの2つにweightを割り振り、比率でトラフィックを分割する
  • 問題③: ヘルスチェックの明示設定がない → GCEデフォルトのヘルスチェック(パス/、間隔60秒)がアプリの/healthzセマンティクスと一致せず、起動直後の未readyなPodにもトラフィックが流れ得る。HealthCheckPolicyでアプリのreadiness実態に合わせたヘルスチェックを明示する
  • 問題④: 公開APIにも関わらずCloud Armorセキュリティポリシーが未接続 → GCPBackendPolicyでCloud Armorセキュリティポリシーをバックエンドサービスへアタッチし、L7攻撃・ボットスクレイピング対策を組み込む
  • 問題⑤: 昇格・ロールバックが人間の目視・手動kubectlコマンドに依存 → DataDogのerror_rateメトリクスをポーリングし、閾値以下なら次の重み(10%→50%→100%)へ自動昇格、閾値超過なら自動的に0%(旧バージョンへ全戻し)するワークフローをArgo Workflowsで組む
  • 問題⑥: GCE Ingressは2026年時点でGKEロードマップ上「新規機能は非推奨、Gateway APIへの移行が標準」とされているにも関わらず、新機能(カナリアリリース)をIngressベースで作り込もうとしている → 技術的負債を増やす前に、移行を機会にGateway APIへ切り替える
  • 問題⑦: カナリアの検証時間・閾値・昇格ステップが暗黙的 → 重み配列・監視期間・エラー率閾値をTerraform変数/Workflowパラメータとして明示し、リリースごとの調整余地とレビュー可能性を持たせる
ヒント3 — コードの骨格
# Gateway API: Gateway + HTTPRoute のスケルトン
resource "kubernetes_manifest" "banner_gateway" {
  manifest = {
    apiVersion = "gateway.networking.k8s.io/v1"
    kind       = "Gateway"
    metadata   = { name = "banner-gateway", namespace = "mops" }
    spec = {
      gatewayClassName = "gke-l7-global-external-managed"
      listeners = [{ name = "https", port = 443, protocol = "HTTPS", ... }]
    }
  }
}

resource "kubernetes_manifest" "banner_httproute" {
  manifest = {
    apiVersion = "gateway.networking.k8s.io/v1"
    kind       = "HTTPRoute"
    metadata   = { name = "banner-api-route", namespace = "mops" }
    spec = {
      parentRefs = [{ name = "banner-gateway" }]
      rules = [{
        matches = [{ path = { type = "PathPrefix", value = "/banners" } }]
        backendRefs = [
          { name = "banner-api-stable", port = 80, weight = 90 },
          { name = "banner-api-canary", port = 80, weight = 10 },  # 段階的に引き上げる
        ]
      }]
    }
  }
}

問題点分析(7点)

#問題点分類改善方法
1nginxカナリアアノテーションが黙って無視される道具ミスマッチ層Gateway API HTTPRouteのネイティブ重み付け
2Service selector書き換え=アトミック全量切替分割メカニズム欠如層stable/canary 2 Serviceへweight分割
3ヘルスチェックがGCEデフォルトのまま接続制御層HealthCheckPolicyで/healthz明示
4Cloud Armor未接続接続制御層GCPBackendPolicyでセキュリティポリシー接続
5昇格・ロールバックが人間の目視依存判断自動化層DataDog error_rateポーリング+自動昇格/ロールバック
6非推奨GCE Ingress上に新機能を作り込み道具ミスマッチ層Gateway APIへ移行を機会に切替
7重み・監視期間・閾値が暗黙的明文化層Terraform変数/Workflowパラメータ化

カナリアリリースフロー図 — Bad vs Good の変換

Bad(変更前)— GCE Ingress + nginxカナリアアノテーション canary-weight: "10" を設定してデプロイ 問題①: GCE Ingressコントローラーはnginx形式アノテーション非対応 エラーも警告も出ず、設定が黙って無視される 実際の切替: kubectl patch service selector書き換え 問題②: Aを捨ててBにするアトミックな全量切替 「10%だけ試す」が原理的に不可能 問題③④: ヘルスチェック不一致・Cloud Armor未接続のまま切替 新バージョンへ100%即時流入 未readyなPodにもトラフィックが流れる(問題③) 5分間エラー率が急上昇 Slackへの苦情投稿で人間が気づく 問題⑤: 検知まで時間がかかり判断基準も属人的 手動でselectorを旧versionへ書き戻して復旧 構造的なリスク 問題⑥: 非推奨のGCE Ingress上に新機能を積み増し続ける 問題⑦: 重み・監視期間・閾値が誰のレビューも通らない 公開APIがCloud Armor無しで無防備なまま稼働継続 × 「切替」と「分割」を混同している × 設定が効いているかコントローラー仕様を未確認 × 昇格・撤退の判断が人間の目視に依存 Good(変更後)— Gateway API × 自動昇格・自動ロールバック Gateway + HTTPRoute + HealthCheckPolicy + GCPBackendPolicy 修正①③④: コントローラーネイティブ機能で構成 Cloud Armor・ヘルスチェックはリリース中も常時有効 weight: stable=90 / canary=10 でカナリア開始 修正②⑦: HTTPRoute backendRefs.weight(Terraform変数で明示) 実際に比率でトラフィックが分割される Argo WorkflowsがDataDog error_rateを10分間ポーリング 修正⑤: 判断基準を機械的な閾値(1%)に統一 閾値以下 閾値超過 自動昇格 weight 10→50→100 と段階的に引き上げ 各ステップで再度10分監視 自動ロールバック weight canary=0へ即時 Slackへ通知 100%到達 → stableイメージ更新・canaryをスケールダウン 修正③④: HealthCheckPolicy/GCPBackendPolicyはstable/canary共通で常時適用 防御レベル・ヘルスチェック基準がリリース中も不変 ✓ HTTPRoute weightで実際に比率分割される ✓ ヘルスチェック・Cloud Armorが常時有効 ✓ 昇格・撤退の判断がメトリクスで自動化 修正

模範解答

Before — GCE Ingress + nginxカナリアアノテーション
# 問題①②③④⑤⑥⑦: 重み付け分割・ヘルスチェック・
# セキュリティ・自動化のいずれもコントローラーが対応していない
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: banner-api-ingress
  annotations:
    kubernetes.io/ingress.class: "gce"
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
  rules:
    - http:
        paths:
          - path: /banners
            pathType: Prefix
            backend:
              service:
                name: banner-api-svc
                port: { number: 80 }
---
# 実際の切替: kubectl patch service selector書き換え
# kubectl patch service banner-api-svc \
#   -p '{"spec":{"selector":{"version":"v2"}}}'
After — Gateway API(HTTPRoute重み付け + HealthCheckPolicy + GCPBackendPolicy)
# ── Gateway(GKE Gateway Controller、外部公開)──────
resource "kubernetes_manifest" "banner_gateway" {
  manifest = {
    apiVersion = "gateway.networking.k8s.io/v1"
    kind       = "Gateway"
    metadata = { name = "banner-gateway", namespace = "mops" }
    spec = {
      gatewayClassName = "gke-l7-global-external-managed"
      listeners = [{
        name = "https", port = 443, protocol = "HTTPS"
        tls = {
          mode = "Terminate"
          certificateRefs = [{ name = "banner-api-tls-cert" }]
        }
      }]
    }
  }
}

# ── HealthCheckPolicy(修正③: readiness実態に合わせる)──
resource "kubernetes_manifest" "banner_healthcheck_policy" {
  manifest = {
    apiVersion = "networking.gke.io/v1"
    kind       = "HealthCheckPolicy"
    metadata   = { name = "banner-api-healthcheck", namespace = "mops" }
    spec = {
      targetRef = { group = "", kind = "Service", name = "banner-api-stable" }
      default = {
        checkIntervalSec = 10, timeoutSec = 5
        healthyThreshold = 2, unhealthyThreshold = 3
        config = {
          type = "HTTP"
          httpHealthCheck = { requestPath = "/healthz", port = 8080 }
        }
      }
    }
  }
}

# ── GCPBackendPolicy(修正④: Cloud Armor接続)──────
resource "kubernetes_manifest" "banner_backend_policy" {
  manifest = {
    apiVersion = "networking.gke.io/v1"
    kind       = "GCPBackendPolicy"
    metadata   = { name = "banner-api-backend-policy", namespace = "mops" }
    spec = {
      targetRef = { group = "", kind = "Service", name = "banner-api-stable" }
      default = { securityPolicy = google_compute_security_policy.banner_api_armor.name }
    }
  }
}

resource "google_compute_security_policy" "banner_api_armor" {
  name = "banner-api-armor-policy"
  rule {
    action = "throttle", priority = 1000
    match {
      versioned_expr = "SRC_IPS_V1"
      config { src_ip_ranges = ["*"] }
    }
    rate_limit_options {
      conform_action = "allow", exceed_action = "deny(429)"
      rate_limit_threshold { count = 100, interval_sec = 60 }  # ボット対策
    }
  }
  rule {
    action = "allow", priority = 2147483647
    match { versioned_expr = "SRC_IPS_V1", config { src_ip_ranges = ["*"] } }
  }
}

# ── HTTPRoute(修正①②: ネイティブ重み付け分割)──────
resource "kubernetes_manifest" "banner_httproute" {
  manifest = {
    apiVersion = "gateway.networking.k8s.io/v1"
    kind       = "HTTPRoute"
    metadata   = { name = "banner-api-route", namespace = "mops" }
    spec = {
      parentRefs = [{ name = "banner-gateway" }]
      rules = [{
        matches = [{ path = { type = "PathPrefix", value = "/banners" } }]
        backendRefs = [
          { name = "banner-api-stable", port = 80, weight = var.canary_weight_stable },
          { name = "banner-api-canary", port = 80, weight = var.canary_weight_canary },
        ]
      }]
    }
  }
}

# 修正⑦: 重みをTerraform変数として明示(暗黙のマジックナンバー排除)
variable "canary_weight_canary" { type = number, default = 10 }
variable "canary_weight_stable" { type = number, default = 90 }
① カナリアデプロイ開始
   ├─ banner-api-canary Deploymentへ新バージョンをデプロイ
   ├─ HealthCheckPolicy/readinessProbeがPassするまで待機(修正③)
   └─ HTTPRoute weight を stable=90 / canary=10 へ設定(修正①②)

② 監視ウィンドウ(各ステップ10分、修正⑤⑦)
   ├─ Argo WorkflowsがDataDog APIを10分間ポーリングしerror_rate(5xx比率)を取得
   ├─ 閾値(1%)以下 → 次のステップへ自動昇格
   │    10% → 50% → 100% の順にweightを書き換え(kubectl patch HTTPRoute)
   └─ 閾値(1%)超過 → 自動ロールバック
        HTTPRoute weightを stable=100 / canary=0 へ即時書き戻し
        Slackへ「自動ロールバック発生」を通知(原因調査は人間が引き継ぐ)

③ 100%昇格・完了
   ├─ 全ステップでerror_rateが閾値以下だったことを確認後、canary=100%へ到達
   ├─ banner-api-stable Deploymentのイメージをcanaryと同一へ更新(世代交代)
   └─ banner-api-canary Deploymentをスケールダウン(次回リリースまで待機)

④ Cloud Armor・ヘルスチェックは常時有効(修正③④)
   → カナリア中もstable/canary両Serviceに同一のHealthCheckPolicy・GCPBackendPolicyが適用され、
     防御レベル・ヘルスチェック基準がリリース中に変化しないことを保証する
問題修正内容効果
① nginxアノテーションが無視されるGateway API HTTPRouteのネイティブ重み付けコントローラー非依存で確実に分割が効く
② selector書き換え=全量切替stable/canary 2 Serviceへweight分割段階的な比率での検証が可能に
③ ヘルスチェック不一致HealthCheckPolicyで/healthzを明示未readyなPodへのルーティングを防止
④ Cloud Armor未接続GCPBackendPolicyでセキュリティポリシー接続L7攻撃・ボットスクレイピング対策
⑤ 人間の目視依存DataDog error_rateポーリング+自動昇格/ロールバック検知速度向上・判断基準の再現性確保
⑥ 非推奨Ingress上に新機能を作り込みGateway APIへの移行を機会に切替技術的負債の抑制
⑦ 重み・閾値が暗黙的Terraform変数/Workflowパラメータ化リリースごとの調整をレビュー可能に

ポイント解説

1「アノテーションが黙って無視される」は最も気づきにくい種類のバグである — エラーも警告も出ないため、設定した本人は「効いているはず」と信じたまま本番へ進んでしまう。コントローラーが対応していない機能をアノテーションで指定する構成はCIやTerraform planでは検出できない典型的な落とし穴であり、機能がそのコントローラーのネイティブ機能かどうかを都度確認する習慣が必要になる。
2「切替」と「分割」は別の概念である — Service selectorの書き換えは対象を入れ替えるスイッチであって、比率で分けるロードバランシングではない。段階的カナリアを実現するには、ロードバランサ自身が重み付け分割をネイティブサポートしている道具(Gateway APIのHTTPRoute)を選定する必要がある。
3昇格・ロールバックの判断を人間の目視から切り離すと、検知速度と再現性の両方が改善する — 「Slackで苦情が来たら気づく」運用は、検知までの時間が担当者の注意力に依存し、しかも毎回同じ基準で判断されているとは限らない。DataDogのメトリクスを機械的な閾値でポーリングする設計にすることで検知速度が向上するだけでなく、「何%を超えたら止めるか」というリスク許容度の議論をコードレビューの対象にできる。
4移行のタイミングは「新機能を作るとき」が最もコストが低い — 既存の枯れた構成をそのまま使い続けたい心理は自然だが、新機能追加のたびにIngressベースで作り込むと、後でGateway APIへ移行する際の書き換え対象がどんどん増えていく。ロードマップ上非推奨とされた道具の上に新機能を積む前に、その機能追加自体を移行のきっかけにする判断が技術的負債を抑制する。

実務への応用

banner-apiに限らず、MOps 販促システムチームが持つ他の公開API(クーポンAPI・キャンペーン対象判定API等)も同様にGCE Ingressベースで稼働している場合、同じ「カナリアアノテーションが無視される」落とし穴を抱えている可能性が高い。今回のGateway API移行パターン(Gateway + HTTPRouteの重み付け + HealthCheckPolicy + GCPBackendPolicy)はテンプレート化でき、他APIへの横展開時にTerraformモジュールとして共通化すれば、移行のたびに同じ7つの問題を再発見する手間を省ける。また、DataDogメトリクスに基づく自動昇格・自動ロールバックの仕組みは、Argo Workflows上で「カナリア分析ステップ」として汎用化しておけば、将来的にArgo Rolloutsのようなプログレッシブデリバリー専用ツールへの移行を検討する際の要件定義にもそのまま使える。

weightの書き換え頻度に注意する: Gateway APIのHTTPRouteでweightを書き換える運用は、Terraform applyで行うと1ステップごとにCI/CDパイプラインを通す必要がありレイテンシが生じる。10分単位で重みを動かす自動カナリアでは、Terraform stateを介さずKubernetes APIへ直接kubectl patch(もしくはArgo WorkflowsからのKubernetes API呼び出し)で書き換え、Terraformの管理下にある初期値(デプロイのたびの基準値)とは役割を分離するのが実務上の勘所になる。

今日のまとめ

カナリアリリース事故の根本原因は「重みを10に設定した」というコード上の記述ではなく、その設定を実際に解釈するコントローラーが存在しなかったという構成選定のミスマッチにあった。 Gateway APIのようにコントローラー非依存で重み付け分割・ヘルスチェック・セキュリティポリシーをネイティブサポートする道具に置き換え、さらに昇格・ロールバックの判断をメトリクスベースの自動化に委ねることで、「アノテーションが無視されていることに気づかない」「人間が気づくまでエラーが流れ続ける」という2つのリスクを構造的に排除できる。

次のステップ

  • 発展問題: カナリア昇格の判断基準をerror_rate単一指標ではなく、レイテンシ(p99)・リクエスト数(サンプルサイズ不足による誤判定防止)を組み合わせた複合判定に拡張し、「トラフィックが少ない深夜帯にカナリアを開始した場合、error_rateのサンプル数が少なすぎて偶然1件のエラーで自動ロールバックしてしまう」という誤検知シナリオを防ぐ設計を検討せよ
  • 参考: Gateway API(HTTPRoute weighted backendRefs) / GKE Gateway Controller / HealthCheckPolicy / GCPBackendPolicy / Cloud Armor rate limiting / Argo Workflows によるカナリア分析パターン

自己評価(あとで記入)