弱点補強: Kubernetes Gateway API移行 × NetworkPolicy — Ingress単一YAML集中→Gateway(基盤)+HTTPRoute(各チーム)責務分離 × カナリアService複製→backendRefs weight付き宣言的カナリア × NetworkPolicy不在(暗黙全許可)→default-deny+明示allow × 自己署名TLS手動更新→Certificate Manager自動更新 × サイドカー起動順序制御なし→Native Sidecar Containers(MOps campaign-api/coupon-api GKE Autopilot Bad→Good 7点)

2026-07-26 (Day 109) 日曜 弱点補強 ★★★★☆ GKE Autopilot 1.30 / Gateway API NetworkPolicy / Native Sidecar Containers

概要

🚪

Gateway API は「入口の所有者」と「ルールの所有者」を分離する

従来型 Ingress は1つのYAMLに全チームの設定が同居する。Gateway API では Gateway(基盤チームが管理する物理的な入口)と HTTPRoute(各アプリチームが自分の Namespace で管理するルーティングルール)が別リソースになり、変更の競合と越境権限のリスクをAPIレベルで排除できる。

⚖️

重み付け backendRefs がカナリアの複製地獄を終わらせる

Ingress でのカナリアは「専用Service+専用パスを都度作り、丸ごと切り替える」しかない。HTTPRoutebackendRefs[].weight を変えるだけで、1%→10%→50%→100%という宣言的な段階的ロールアウトが実現できる。

🔒

NetworkPolicy 未設定は「安全側のデフォルト」ではない

Kubernetesは何も設定しなければ Namespace 内の全Pod間通信を暗黙的に許可する。podSelector: {} の default-deny を先に敷き、必要な通信だけを明示的に allow するホワイトリスト方式にしない限り、1つのPodの侵害が即座に他サービスへの侵入経路になる

🧩

Native Sidecar Containers がサイドカーの起動順序を保証する

OTel Collector を通常の containers に入れると、本体との起動・終了順序は保証されない。initContainersrestartPolicy: Always(K8s 1.29+ 安定機能)を付けることで、本体より先に起動し、後に終了するサイドカーとして扱われる。

問題

ECサイト MOps チームの GKE Autopilot クラスタ(1.30+)には campaign-api / coupon-api / notification-worker の3サービスが同居している。現在の入口構成は従来型の networking.k8s.io/v1 Ingress(GCE Ingress)1枚に全ルーティングルールを集約する形になっており、7つの構造的な問題が潜んでいる。

先週、coupon-api に「割引計算ロジックの変更」をリリースする際、Ingressのpathを丸ごと新Serviceへ向け替える形でしか切り替えられず、1%だけ試すといった段階的なカナリアができないまま全トラフィックを新ロジックに晒すことになった。さらに障害対応中に判明したのは、同一Namespace内では campaign-api のPodから coupon-api の内部Podへ任意のポートで直接通信できてしまう状態だったこと(NetworkPolicy未設定)。加えて、新しく導入した OTel Collector サイドカーが coupon-api 本体より遅れて起動するため、リリース直後数分間のトレース・メトリクスが欠損するという別の問題も見つかった。

制約・前提条件

  • GKE Autopilot 1.30+、Gateway API 有効化済み(GatewayClass: gke-l7-global-external-managed 利用可)
  • Namespace: アプリは mops-campaign、Gateway 基盤は gateway-infra(基盤チームが管理・分離したい)
  • Service: campaign-api, coupon-api, coupon-api-canary, notification-worker
  • Cloud Certificate Manager と連携した TLS 証明書自動更新が利用可能
  • OTel Collector サイドカーを各Podに同居させる方針(K8s 1.29+ Native Sidecar Containers が安定機能として利用可能)
  • 現状: 単一Ingressに全パスルールが集中/NetworkPolicy不在(全Pod間通信が暗黙許可)/TLS証明書は自己署名Secretの手動更新/カナリアはService複製+Ingress丸ごと切替/OTel Collectorが通常コンテナで起動順序制御なし/全ルーティング設定が単一Ingressリソースに集中
期待する回答形式: 問題点の列挙(番号付き)+ 改善後 Kubernetes マニフェスト(Gateway / HTTPRoute / NetworkPolicy / Deployment)+ 適用・検証コマンド + 設計意図の説明

悪い構成 (Before)

この構成には 7つの構造的な問題 が隠れています。見つけてみてください。
mops-campaign-ingress(現状)— 単一Ingress・NetworkPolicy不在
# 問題①⑥⑦: 単一Ingressに全パスルールが集中し、権限分離不可・DSLがベンダー固有
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: mops-campaign-ingress
  annotations:
    kubernetes.io/ingress.class: "gce"
    kubernetes.io/ingress.global-static-ip-name: "mops-campaign-ip"
    # 問題②: カナリアは重み付けなし、パスを新設して丸ごと切り替えるしかない
spec:
  tls:
    - secretName: mops-campaign-tls   # 問題④: 手動更新の自己署名証明書Secret
  rules:
    - host: campaign.mops.example.com
      http:
        paths:
          - path: /api/campaign
            backend:
              service: { name: campaign-api, port: { number: 80 } }
          - path: /api/coupon
            backend:
              service: { name: coupon-api, port: { number: 80 } }
          - path: /api/coupon-canary   # 問題②: カナリア専用パスを都度手動作成
            backend:
              service: { name: coupon-api-canary, port: { number: 80 } }
Pod間通信・サイドカー起動(現状)— 暗黙の全許可・起動順序なし
# 問題③: NetworkPolicyが一切存在しない
# → mops-campaign Namespace内の全Podが任意ポートで自由に通信可能
# → campaign-api のPodが侵害された場合、coupon-api の内部ポートにも直接到達できる

apiVersion: apps/v1
kind: Deployment
metadata:
  name: coupon-api
spec:
  template:
    spec:
      containers:
        - name: otel-collector      # 問題⑤: サイドカーのつもりが通常コンテナ定義
          image: otel/opentelemetry-collector-contrib:0.98.0
        - name: coupon-api
          image: .../coupon-api:1.4.0
          # otel-collectorの起動完了を待たずに起動するため、
          # リリース直後数分間のトレース・メトリクスが欠損する
問題点サマリー(7点)
1単一Ingressに全ルールが集中 — チーム越境の変更リスク。Gateway+HTTPRouteで責務分離
2カナリアがService複製+丸ごと切替 — ヒューマンエラーの温床。weight付きbackendRefsへ
3NetworkPolicy不在(暗黙の全許可) — 侵害の被害範囲が無制限。default-deny+明示allowへ
4TLS証明書が自己署名Secretの手動更新 — 期限切れ事故リスク。Certificate Manager自動更新へ
5サイドカーが通常コンテナ定義 — 起動順序保証なし。Native Sidecar Containersへ
6基盤/アプリの権限分離ができない — Ingressは単一リソース。Gateway/HTTPRouteのNamespace分離へ
7パスルーティングがベンダー固有アノテーションDSL — レビュー困難。型付きmatches/backendRefsへ

ヒント(段階的開示)

ヒント1 — 方向性
問題は3層に分類できる。(1) トラフィック表現力の限界 — 従来型Ingressは「パスごとに1つの固定Backendへ全量転送」しかできず、重み付けカナリアやチームごとの権限分離を表現できない。(2) ネットワークの信頼境界 — NetworkPolicyが存在しないKubernetesクラスタは「Namespace内は暗黙的に全通信許可」がデフォルトであり、これは明示的に選んだ設定ではなく「何もしていない」状態にすぎない。(3) コンテナのライフサイクル制御 — サイドカーを普通の containers に入れると、本体との起動順序・終了順序の保証がない。Gateway API は「入口そのもの(Gateway)は基盤チームが持ち、ルーティングルール(HTTPRoute)は各アプリチームが自分のNamespaceで持つ」というロールの分離をAPIレベルで表現できる設計であることを意識する。
ヒント2 — アプローチ
  • 問題①⑥: 従来型 IngressGateway(基盤チームが gateway-infra Namespaceで所有)+ HTTPRoute(各アプリチームが mops-campaign Namespaceで所有)に分離する
  • 問題②: カナリア用の別Service+Ingress丸ごと切替 → HTTPRoutebackendRefsweight を指定し、宣言的な重み付けルーティングにする(Service複製は不要)
  • 問題③: NetworkPolicy不在(暗黙の全許可)→ podSelector: {} の default-deny NetworkPolicy を先に敷き、必要な通信だけを明示的に allow する
  • 問題④: 自己署名TLS証明書の手動更新 → Gatewaylisteners[].tls.certificateRefs に Certificate Manager 連携の証明書を指定し自動更新にする
  • 問題⑤: OTel Collectorが通常コンテナで起動順序制御なし → initContainers に入れて restartPolicy: Always を付与する Native Sidecar パターン(K8s 1.29+ 安定機能)にする
  • 問題⑦: パスルーティングがIngressアノテーションのDSL(ベンダー固有記法)でレビューしづらい → HTTPRoutematches / backendRefs という型付きフィールドで宣言的に表現する
ヒント3 — コードの骨格
# Gateway(gateway-infra Namespace・基盤チーム所有)のスケルトン
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: mops-platform-gateway
  namespace: gateway-infra
spec:
  gatewayClassName: gke-l7-global-external-managed   # 修正①
  listeners:
    - name: https
      protocol: HTTPS
      port: 443
      tls:
        certificateRefs:
          - kind: Certificate                        # 修正④: Certificate Manager連携
            name: mops-campaign-managed-cert

---
# HTTPRoute(mops-campaign Namespace・アプリチーム所有)のスケルトン
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: coupon-api-route
  namespace: mops-campaign
spec:
  parentRefs:
    - name: mops-platform-gateway
      namespace: gateway-infra
  rules:
    - backendRefs:
        - { name: coupon-api, weight: 90 }    # 修正②: 重み付けカナリア
        - { name: coupon-api-canary, weight: 10 }

---
# default-deny NetworkPolicy(修正③の起点)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: mops-campaign
spec:
  podSelector: {}
  policyTypes: ["Ingress", "Egress"]

問題点分析(7点)

#問題点分類改善方法
1単一Ingressに全ルールが集中構造設計Gateway(基盤)+ HTTPRoute(各チーム)の責務分離
2カナリアがService複製+丸ごと切替表現力backendRefs weight付き宣言的カナリア
3NetworkPolicy不在(暗黙の全許可)セキュリティdefault-deny + 明示allow(ゼロトラスト)
4TLS証明書が自己署名Secretの手動更新セキュリティCertificate Manager連携の自動更新
5サイドカーが通常コンテナ定義(起動順序なし)ライフサイクルNative Sidecar Containers(restartPolicy: Always)
6基盤/アプリチームの権限分離ができないガバナンスGateway/HTTPRouteのNamespace分離 + RBAC
7パスルーティングがベンダー固有アノテーションDSL可読性型付きmatches/backendRefsフィールド

構成図 — Bad vs Good(SVG)

Bad(変更前)— 単一Ingress・全許可・起動順序なし Ingress(単一YAML・GCEアノテーション) 問題①⑥⑦: 全ルール集中・ベンダー固有DSL・権限分離不可 問題④: mops-campaign-tls(自己署名・手動更新) coupon-api / coupon-api-canary(別Service) 問題②: 重み付けなし。パスを丸ごと切り替えるカナリア → 1%だけ試すといった段階的検証ができない mops-campaign Namespace(NetworkPolicyなし) 問題③: campaign-api → coupon-api へ任意ポートで直接到達可能 (暗黙の全許可がデフォルト) coupon-api Pod: containers[otel-collector, coupon-api] 問題⑤: 起動順序保証なし(並行起動) → リリース直後数分間のトレース・メトリクス欠損 終了時もflush前にotel-collectorが落ちる場合がある × カナリアがヒューマンエラーの温床(複製・丸ごと切替) × 1Podの侵害が同一Namespace内で無制限に横展開できる × TLS証明書の期限切れをうっかり見逃すリスク × リリース直後の可観測性が欠損し障害対応が遅れる × 基盤チームとアプリチームの変更が同じYAMLで競合 Good(変更後)— Gateway API + default-deny + Native Sidecar Gateway(gateway-infra Namespace・基盤チーム所有) 修正①⑥: 入口の一元管理・型付きAPI 修正④: Certificate Manager連携で証明書自動更新 HTTPRoute(mops-campaign Namespace・アプリチーム所有) 修正②⑦: backendRefs weight 90/10 の宣言的カナリア 数値変更だけで1%→10%→50%→100%と拡大できる mops-campaign Namespace(default-deny + 明示allow) 修正③: podSelector:{} でdefault-deny → gateway-infraからの Ingressのみ明示的にallow(他Podからの直接到達は拒否) campaign-api → coupon-api の直接通信は拒否される coupon-api Pod: initContainers[otel-collector(restartPolicy: Always)] 修正⑤: Native Sidecar Containers(K8s 1.29+ 安定機能) readinessProbe確立後にcoupon-api本体が起動 終了時もflush完了までotel-collectorが残る ✓ 基盤/アプリの責務分離でチーム越境の変更競合を解消 ✓ 宣言的カナリアでヒューマンエラーの発生源自体を排除 ✓ default-denyで1Podの侵害範囲を構造的に限定 ✓ 証明書の期限切れ事故を自動更新で防止 ✓ 起動順序保証でリリース直後の可観測性欠損を解消 修正

模範解答

Before — 単一Ingress・カナリア丸ごと切替・自己署名TLS
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: mops-campaign-ingress
  annotations:
    kubernetes.io/ingress.class: "gce"        # 問題①⑥⑦
spec:
  tls:
    - secretName: mops-campaign-tls            # 問題④: 自己署名・手動更新
  rules:
    - host: campaign.mops.example.com
      http:
        paths:
          - path: /api/coupon
            backend:
              service: { name: coupon-api, port: { number: 80 } }
          - path: /api/coupon-canary          # 問題②: 専用パスを手動作成
            backend:
              service: { name: coupon-api-canary, port: { number: 80 } }
After — Gateway(基盤) + HTTPRoute(アプリ) + weight付きカナリア
# 修正①⑥: Gateway(基盤チームが gateway-infra Namespaceで所有)
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: mops-platform-gateway
  namespace: gateway-infra
spec:
  gatewayClassName: gke-l7-global-external-managed
  listeners:
    - name: https
      protocol: HTTPS
      port: 443
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Certificate            # 修正④: Certificate Managerが自動更新
            name: mops-campaign-managed-cert
      allowedRoutes:
        namespaces:
          from: Selector
          selector:
            matchLabels: { gateway-access: "mops-campaign" }

---
# 修正⑥⑦: HTTPRoute(アプリチームが mops-campaign Namespaceで所有)
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: coupon-api-route
  namespace: mops-campaign
  labels:
    gateway-access: "mops-campaign"
spec:
  parentRefs:
    - name: mops-platform-gateway
      namespace: gateway-infra
  hostnames: ["campaign.mops.example.com"]
  rules:
    - matches:
        - path: { type: PathPrefix, value: /api/coupon }
      backendRefs:
        - name: coupon-api        # 修正②: Service複製・パス増設が不要
          port: 80
          weight: 90              # 通常90%
        - name: coupon-api-canary
          port: 80
          weight: 10              # カナリア10%。数値を変えるだけで段階拡大できる
Before — NetworkPolicyなし・サイドカー起動順序なし
# 問題③: NetworkPolicyが一切存在しない
# → mops-campaign Namespace内の全Podが自由に通信可能

apiVersion: apps/v1
kind: Deployment
metadata:
  name: coupon-api
  namespace: mops-campaign
spec:
  template:
    spec:
      containers:
        - name: otel-collector       # 問題⑤: 通常コンテナ定義
          image: otel/opentelemetry-collector-contrib:0.98.0
        - name: coupon-api
          image: .../coupon-api:1.4.0
After — default-deny + 明示allow + Native Sidecar
# 修正③: default-deny NetworkPolicy(ゼロトラストの起点)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: mops-campaign
spec:
  podSelector: {}
  policyTypes: ["Ingress", "Egress"]

---
# 修正③: Gateway基盤からcoupon-apiへのIngressだけを明示的に許可
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-gateway-to-coupon-api
  namespace: mops-campaign
spec:
  podSelector:
    matchLabels: { app: coupon-api }
  policyTypes: ["Ingress"]
  ingress:
    - from:
        - namespaceSelector:
            matchLabels: { kubernetes.io/metadata.name: gateway-infra }
      ports:
        - protocol: TCP
          port: 8080
  # → campaign-apiからcoupon-apiへの直接通信は拒否される(同一Namespaceでも)

---
# 修正⑤: Native Sidecar Containers(K8s 1.29+ 安定機能)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: coupon-api
  namespace: mops-campaign
spec:
  template:
    spec:
      initContainers:
        - name: otel-collector
          image: otel/opentelemetry-collector-contrib:0.98.0
          restartPolicy: Always     # これでinitContainerでも「サイドカー」扱いになる
          readinessProbe:
            httpGet: { path: /healthz, port: 13133 }
      containers:
        - name: coupon-api
          image: .../coupon-api:1.5.0
          # otel-collectorのreadiness確立後に起動し、
          # 終了時もflush完了までotel-collectorが残る
# 適用
kubectl apply -f gateway.yaml -f httproute.yaml -f networkpolicy.yaml -f deployment.yaml

# Gateway/HTTPRouteの状態確認(Programmed=Trueになっていることを確認)
kubectl get gateway mops-platform-gateway -n gateway-infra -o wide
kubectl get httproute -n mops-campaign

# カナリア比率の疎通確認(100回叩いて概ね9:1になることを確認)
for i in $(seq 1 100); do
  curl -s -o /dev/null -w "%{http_code}\n" https://campaign.mops.example.com/api/coupon
done

# NetworkPolicyの効果確認(campaign-apiからcoupon-apiへの直接通信が拒否されることを確認)
kubectl exec -n mops-campaign deploy/campaign-api -- curl -s -m 3 http://coupon-api:8080/healthz
# → default-deny + 未許可のためタイムアウトすることを確認
問題修正内容効果
①⑥ 単一Ingress・権限分離不可Gateway(基盤)+ HTTPRoute(各チーム)チーム越境の変更競合を構造的に解消
② カナリアがService複製backendRefs weight付き宣言的カナリア段階的ロールアウトを設定変更だけで実現
③ NetworkPolicy不在default-deny + 明示allow1Podの侵害範囲(blast radius)を構造的に限定
④ 自己署名TLS手動更新Certificate Manager連携証明書期限切れによる事故を防止
⑤ サイドカー起動順序なしNative Sidecar Containersリリース直後の可観測性欠損を解消
⑦ ベンダー固有アノテーションDSL型付きmatches/backendRefsレビュー容易性・型安全性の向上

ポイント解説

1Gateway と HTTPRoute のロール分離Gateway は基盤チームが gateway-infra Namespaceで一元管理し、HTTPRoute は各アプリチームが自分のNamespaceで所有する。従来のIngressは1つのYAMLに全チームの変更が集中し、レビュー競合・誤削除のリスクが高かったが、Gateway APIはこの責務境界をAPIレベルで表現できる。
2weight付き backendRefs による宣言的カナリアHTTPRoutebackendRefs[].weight を変えるだけで、カナリア比率を1%→10%→50%→100%と段階的に拡大できる。専用Service・専用パスの複製が不要になり、カナリア用の設定漏れ(TTL・ヘルスチェック未設定など)というヒューマンエラーの発生源自体をなくせる。
3NetworkPolicy の default-deny は「何もしない」ではなく「明示的に選ぶ」設計判断 — Kubernetesのデフォルトは全通信許可であり、これは安全側ではない。podSelector: {} の default-deny を先に敷き、必要な通信だけを allow するホワイトリスト方式にすることで、1つのPodが侵害された際の被害範囲を構造的に制限できる。
4Certificate Manager 連携で TLS 証明書のライフサイクル管理から手離れするGatewaylisteners[].tls.certificateRefs にCertificate Manager管理の証明書を指定すると、更新期限切れによるサービス停止事故を構造的に防止できる。自己署名Secretの手動更新運用と違い、期限管理を人間の記憶に依存しない。
5Native Sidecar Containers(K8s 1.29+ 安定機能)で起動・終了順序を保証するinitContainersrestartPolicy: Always を付けると、通常のinitContainerと違い本体と並走し続け、かつ本体より先に起動・後に終了する。OTel Collectorのようなサイドカーで「本体の最初のトレースが欠損する」問題を構造的に解消できる。
6GatewayClass はクラウドプロバイダの実装詳細を抽象化するgke-l7-global-external-managed はGKEにおけるGateway APIの実装の1つであり、内部向けなら gke-l7-rilb のような別のGatewayClassを選べる。Ingressの kubernetes.io/ingress.class アノテーションという緩い文字列合意と違い、GatewayClassは型として明示される。
7移行は「共存期間」を前提に段階的に設計する — 既存のIngressを即座に削除せず、DNSレコードを新しいGatewayのIPへ段階的に切り替える期間を設ける。Ingressと Gateway が同時に同じBackend Serviceを指す期間がありうるため、ロールバック手順(DNSを旧Ingressに戻す)を先に用意してから移行を始める。

実務への応用

MOpsチームのキャンペーン配信基盤では、クーポン計算ロジックの変更のような「金額に直結し失敗が許されない」リリースほど、段階的な検証が必要になる。従来のIngressベースの運用では「切り替えるか、しないか」の2択しかなく、結果的に「深夜に全量切り替えて朝まで祈る」ような運用になりがちだった。HTTPRoute の weight を使えば、まず1%のトラフィックだけ新ロジックに晒し、DataDogでエラー率・レイテンシを確認しながら10%→50%→100%と拡大する、という安全なロールアウトが宣言的な設定変更だけで実現できる。

NetworkPolicyのdefault-deny化は、単体では地味な変更に見えるが、「あるサービスの脆弱性が他サービスへの侵入経路にならない」という前提を作る基盤的な防御層になる。特に外部ベンダーAPI(SMS/プッシュ通知)と通信する notification-worker のような、外部との接点を持つサービスをEgress側でも制限しておくことは、サプライチェーン攻撃・依存ライブラリの脆弱性が実害に発展するリスクを構造的に下げる。

段階移行の現実的なステップ: (1) Gatewaygateway-infra Namespaceに作成しStatusがProgrammedになることを確認 → (2) 既存Ingressと並行して各サービスの HTTPRoute を作成し、内部テスト用の別ホスト名で先に動作確認 → (3) DNSのTTLを短く設定した上で本番ホスト名を新GatewayのIPへ切り替え → (4) 問題なければ数日後に旧Ingressを削除 → (5) NetworkPolicyはまずlogging-onlyで影響範囲を可視化してからenforceに切り替える。

今日のまとめ

従来型Ingressは「1つの巨大YAMLに全ルーティングルールが集中し、重み付けカナリアも表現できない」という構造的な限界を持つが、Gateway APIは「基盤チームのGateway」と「アプリチームのHTTPRoute」という責務分離と、weight付きbackendRefsによる宣言的カナリアを同時に実現する。あわせてNetworkPolicyのdefault-deny化とNative Sidecar Containersによる起動順序制御を組み合わせることで、トラフィック管理・ネットワークの信頼境界・コンテナのライフサイクルという3つの弱点を一度に底上げできる。

次のステップ

  • 発展問題: マルチクラスタ構成(Multi-cluster Services + Gateway API)で、リージョン障害時にトラフィックを自動的に別クラスタへフェイルオーバーさせる設計を行え。また HTTPRoutematches[].headers を使い、社内テスターのリクエストだけを常にカナリア版に固定ルーティングする「ヘッダーベースの段階的リリース」も設計せよ。
  • 参考: Gateway API公式ドキュメント(gateway-api.sigs.k8s.io)/ GKE Gateway controller / Kubernetes NetworkPolicy / KEP-753(Sidecar Containers)/ Certificate Manager(GCP)

自己評価(あとで記入)

自分の回答

気づき・メモ