概要
Gateway API は「入口の所有者」と「ルールの所有者」を分離する
従来型 Ingress は1つのYAMLに全チームの設定が同居する。Gateway API では Gateway(基盤チームが管理する物理的な入口)と HTTPRoute(各アプリチームが自分の Namespace で管理するルーティングルール)が別リソースになり、変更の競合と越境権限のリスクをAPIレベルで排除できる。
重み付け backendRefs がカナリアの複製地獄を終わらせる
Ingress でのカナリアは「専用Service+専用パスを都度作り、丸ごと切り替える」しかない。HTTPRoute の backendRefs[].weight を変えるだけで、1%→10%→50%→100%という宣言的な段階的ロールアウトが実現できる。
NetworkPolicy 未設定は「安全側のデフォルト」ではない
Kubernetesは何も設定しなければ Namespace 内の全Pod間通信を暗黙的に許可する。podSelector: {} の default-deny を先に敷き、必要な通信だけを明示的に allow するホワイトリスト方式にしない限り、1つのPodの侵害が即座に他サービスへの侵入経路になる。
Native Sidecar Containers がサイドカーの起動順序を保証する
OTel Collector を通常の containers に入れると、本体との起動・終了順序は保証されない。initContainers に restartPolicy: 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リソースに集中
悪い構成 (Before)
# 問題①⑥⑦: 単一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 } }
# 問題③: 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の起動完了を待たずに起動するため、
# リリース直後数分間のトレース・メトリクスが欠損する
ヒント(段階的開示)
ヒント1 — 方向性
containers に入れると、本体との起動順序・終了順序の保証がない。Gateway API は「入口そのもの(Gateway)は基盤チームが持ち、ルーティングルール(HTTPRoute)は各アプリチームが自分のNamespaceで持つ」というロールの分離をAPIレベルで表現できる設計であることを意識する。
ヒント2 — アプローチ
- 問題①⑥: 従来型
Ingress→Gateway(基盤チームがgateway-infraNamespaceで所有)+HTTPRoute(各アプリチームがmops-campaignNamespaceで所有)に分離する - 問題②: カナリア用の別Service+Ingress丸ごと切替 →
HTTPRouteのbackendRefsにweightを指定し、宣言的な重み付けルーティングにする(Service複製は不要) - 問題③: NetworkPolicy不在(暗黙の全許可)→
podSelector: {}の default-deny NetworkPolicy を先に敷き、必要な通信だけを明示的に allow する - 問題④: 自己署名TLS証明書の手動更新 →
Gatewayのlisteners[].tls.certificateRefsに Certificate Manager 連携の証明書を指定し自動更新にする - 問題⑤: OTel Collectorが通常コンテナで起動順序制御なし →
initContainersに入れてrestartPolicy: Alwaysを付与する Native Sidecar パターン(K8s 1.29+ 安定機能)にする - 問題⑦: パスルーティングがIngressアノテーションのDSL(ベンダー固有記法)でレビューしづらい →
HTTPRouteのmatches/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付き宣言的カナリア |
| 3 | NetworkPolicy不在(暗黙の全許可) | セキュリティ | default-deny + 明示allow(ゼロトラスト) |
| 4 | TLS証明書が自己署名Secretの手動更新 | セキュリティ | Certificate Manager連携の自動更新 |
| 5 | サイドカーが通常コンテナ定義(起動順序なし) | ライフサイクル | Native Sidecar Containers(restartPolicy: Always) |
| 6 | 基盤/アプリチームの権限分離ができない | ガバナンス | Gateway/HTTPRouteのNamespace分離 + RBAC |
| 7 | パスルーティングがベンダー固有アノテーションDSL | 可読性 | 型付きmatches/backendRefsフィールド |
構成図 — Bad vs Good(SVG)
模範解答
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 } }
# 修正①⑥: 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%。数値を変えるだけで段階拡大できる
# 問題③: 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
# 修正③: 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 + 明示allow | 1Podの侵害範囲(blast radius)を構造的に限定 |
| ④ 自己署名TLS手動更新 | Certificate Manager連携 | 証明書期限切れによる事故を防止 |
| ⑤ サイドカー起動順序なし | Native Sidecar Containers | リリース直後の可観測性欠損を解消 |
| ⑦ ベンダー固有アノテーションDSL | 型付きmatches/backendRefs | レビュー容易性・型安全性の向上 |
ポイント解説
Gateway は基盤チームが gateway-infra Namespaceで一元管理し、HTTPRoute は各アプリチームが自分のNamespaceで所有する。従来のIngressは1つのYAMLに全チームの変更が集中し、レビュー競合・誤削除のリスクが高かったが、Gateway APIはこの責務境界をAPIレベルで表現できる。HTTPRoute の backendRefs[].weight を変えるだけで、カナリア比率を1%→10%→50%→100%と段階的に拡大できる。専用Service・専用パスの複製が不要になり、カナリア用の設定漏れ(TTL・ヘルスチェック未設定など)というヒューマンエラーの発生源自体をなくせる。podSelector: {} の default-deny を先に敷き、必要な通信だけを allow するホワイトリスト方式にすることで、1つのPodが侵害された際の被害範囲を構造的に制限できる。Gateway の listeners[].tls.certificateRefs にCertificate Manager管理の証明書を指定すると、更新期限切れによるサービス停止事故を構造的に防止できる。自己署名Secretの手動更新運用と違い、期限管理を人間の記憶に依存しない。initContainers に restartPolicy: Always を付けると、通常のinitContainerと違い本体と並走し続け、かつ本体より先に起動・後に終了する。OTel Collectorのようなサイドカーで「本体の最初のトレースが欠損する」問題を構造的に解消できる。gke-l7-global-external-managed はGKEにおけるGateway APIの実装の1つであり、内部向けなら gke-l7-rilb のような別のGatewayClassを選べる。Ingressの kubernetes.io/ingress.class アノテーションという緩い文字列合意と違い、GatewayClassは型として明示される。実務への応用
MOpsチームのキャンペーン配信基盤では、クーポン計算ロジックの変更のような「金額に直結し失敗が許されない」リリースほど、段階的な検証が必要になる。従来のIngressベースの運用では「切り替えるか、しないか」の2択しかなく、結果的に「深夜に全量切り替えて朝まで祈る」ような運用になりがちだった。HTTPRoute の weight を使えば、まず1%のトラフィックだけ新ロジックに晒し、DataDogでエラー率・レイテンシを確認しながら10%→50%→100%と拡大する、という安全なロールアウトが宣言的な設定変更だけで実現できる。
NetworkPolicyのdefault-deny化は、単体では地味な変更に見えるが、「あるサービスの脆弱性が他サービスへの侵入経路にならない」という前提を作る基盤的な防御層になる。特に外部ベンダーAPI(SMS/プッシュ通知)と通信する notification-worker のような、外部との接点を持つサービスをEgress側でも制限しておくことは、サプライチェーン攻撃・依存ライブラリの脆弱性が実害に発展するリスクを構造的に下げる。
Gateway を gateway-infra Namespaceに作成しStatusがProgrammedになることを確認 → (2) 既存Ingressと並行して各サービスの HTTPRoute を作成し、内部テスト用の別ホスト名で先に動作確認 → (3) DNSのTTLを短く設定した上で本番ホスト名を新GatewayのIPへ切り替え → (4) 問題なければ数日後に旧Ingressを削除 → (5) NetworkPolicyはまずlogging-onlyで影響範囲を可視化してからenforceに切り替える。
今日のまとめ
次のステップ
- 発展問題: マルチクラスタ構成(Multi-cluster Services + Gateway API)で、リージョン障害時にトラフィックを自動的に別クラスタへフェイルオーバーさせる設計を行え。また
HTTPRouteのmatches[].headersを使い、社内テスターのリクエストだけを常にカナリア版に固定ルーティングする「ヘッダーベースの段階的リリース」も設計せよ。 - 参考: Gateway API公式ドキュメント(gateway-api.sigs.k8s.io)/ GKE Gateway controller / Kubernetes NetworkPolicy / KEP-753(Sidecar Containers)/ Certificate Manager(GCP)