Go K8s Gateway APIプロダクションガイド:Ingressからの移行の6つの重要ステップ
Ingressがボトルネックになる時:K8sトラフィック管理の至暗の刻
深夜2時、運用チームがカナリアリリースを緊急デプロイ。Ingressのnginxアノテーションを3回修正したが、nginx.ingress.kubernetes.io/canary設定が一向に有効にならない。さらに問題なのは、3つのチームが1つのIngressコントローラーを共有しており、アノテーションの変更が互いに上書きされる危険があること。最終的にカナリアリリースはロールバックされ、デプロイは6時間遅延した。
これは決して例外ではない。Ingressのアノテーション地獄、クロスネームスペースの制限、マルチチーム協作の衝突は、K8sトラフィック管理の最大のペインポイントとなっている。Kubernetes Gateway APIはIngressの後継として、ロールベース設計、拡張可能なルーティング、ネイティブトラフィック分割により、これらの問題を根本的に解決する。本記事では6つの重要ステップで、IngressからGateway APIへのプロダクション級移行を案内する。
コア概念クイックリファレンス
| リソース | ロール | 責務 | 例え |
|---|---|---|---|
| GatewayClass | インフラプロバイダー | コントローラータイプとパラメータを定義 | IngressClass |
| Gateway | クラスタ運用者 | エントリリスナーとインフラを定義 | Ingressコントローラー自体 |
| HTTPRoute | アプリ開発者 | HTTPルーティングルールを定義 | Ingressルール |
| TCPRoute | アプリ開発者 | TCPルーティングルールを定義 | Ingressにネイティブサポートなし |
| GRPCRoute | アプリ開発者 | gRPCルーティングルールを定義 | Ingressにネイティブサポートなし |
| ReferenceGrant | クラスタ運用者 | クロスネームスペース参照を許可 | 該当なし |
| ParentRef | アプリ開発者 | RouteをGatewayにバインド | IngressのingressClassName |
目次
- 問題分析:Ingressの5つの課題
- ステップ1:GatewayClassとインフラ設定
- ステップ2:Gatewayリソース定義
- ステップ3:HTTPRouteルーティングルール設定
- ステップ4:IngressからGateway APIへの移行スクリプト(Go)
- ステップ5:カナリアリリースとトラフィック分割
- ステップ6:マルチクラスターゲートウェイ設定
- 5つのよくある落とし穴
- 10のエラートラブルシューティング
- 高度な最適化テクニック
- 比較:Ingress vs Gateway API vs カスタムゲートウェイ
- おすすめツール
- まとめと参考資料
問題分析:Ingressの5つの課題
課題1:アノテーションの不整合。Nginx Ingressはnginx.ingress.kubernetes.io/rewrite-targetを使用し、Traefikはtraefik.ingress.kubernetes.io/rewrite-targetを使用する。アノテーションの非互換性により、コントローラー移行のコストが極めて高い。
課題2:クロスネームスペースルーティング。Ingressのバックエンドは同じネームスペースのServiceのみ参照可能。クロスネームスペースのトラフィックにはExternalName Serviceのハックが必要で、安全でなく制御も困難。
課題3:マルチクラスターゲートウェイ。Ingressは単一クラスターのみを考慮して設計されている。マルチクラスターのシナリオでは、Federationやカスタムコントロールプレーンの追加が必要。
課題4:カナリアリリースの複雑さ。Nginx IngressのCanaryアノテーションでは2つのIngressオブジェクトの作成が必要。ウェイト設定が直感的でなく、通常ルーティングと混在して管理が困難。
課題5:プロトコルサポートの制限。IngressはHTTP/HTTPSのみサポート。TCP/UDPには追加設定が必要、gRPCはアノテーションハックが必要、WebSocketサポートはコントローラーにより異なる。
ステップ1:GatewayClassとインフラ設定
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: production-gateway-class
annotations:
gateway.networking.k8s.io/controller: nginx
spec:
controllerName: k8s.io/nginx-gateway-controller
parametersRef:
group: ""
kind: ConfigMap
name: gateway-params
namespace: gateway-system
---
apiVersion: v1
kind: ConfigMap
metadata:
name: gateway-params
namespace: gateway-system
data:
workerProcesses: "auto"
maxConnections: "10240"
proxyBufferSize: "16k"
GatewayClassはインフラチームが管理し、コントローラータイプとグローバルパラメータを定義する。異なる環境で異なるGatewayClassを使用し、開発/ステージング/プロダクションの分離を実現する。
ステップ2:Gatewayリソース定義
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: production-gateway
namespace: gateway-system
annotations:
cert-manager.io/issuer: letsencrypt-prod
spec:
gatewayClassName: production-gateway-class
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
shared-gateway-access: "true"
- name: https
protocol: HTTPS
port: 443
tls:
mode: Terminate
certificateRefs:
- name: wildcard-tls
namespace: gateway-system
allowedRoutes:
namespaces:
from: All
addresses:
- type: IPAddress
value: 10.0.1.100
Gatewayは運用チームが管理し、リスナー、TLS終端、許可ルートネームスペースを定義する。allowedRoutesはきめ細かいネームスペースアクセス制御を実現し、Ingressの暗黙的なグローバルアクセスに代わる。
ステップ3:HTTPRouteルーティングルール設定
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: api-service-route
namespace: app-team-a
spec:
parentRefs:
- name: production-gateway
namespace: gateway-system
sectionName: https
hostnames:
- "api.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /v2
- headers:
- type: Exact
name: X-API-Version
value: "2"
backendRefs:
- name: api-v2-service
port: 8080
weight: 90
- name: api-v2-canary-service
port: 8080
weight: 10
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: api-v1-service
port: 8080
HTTPRouteはアプリチームが管理し、パスマッチング、ヘッダーマッチング、ウェイトベースのトラフィック分割をサポートする。parentRefsは特定のGatewayリスナーにバインドし、weightはネイティブトラフィック分割を可能にする。
ステップ4:IngressからGateway APIへの移行スクリプト(Go)
package main
import (
"context"
"fmt"
"os"
networkingv1 "k8s.io/api/networking/v1"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
)
type IngressMigrator struct {
clientset *kubernetes.Clientset
gatewayClass string
gatewayName string
gatewayNS string
}
func NewIngressMigrator(kubeconfig, gatewayClass, gatewayName, gatewayNS string) (*IngressMigrator, error) {
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
return nil, fmt.Errorf("build kubeconfig: %w", err)
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, fmt.Errorf("create clientset: %w", err)
}
return &IngressMigrator{
clientset: clientset,
gatewayClass: gatewayClass,
gatewayName: gatewayName,
gatewayNS: gatewayNS,
}, nil
}
func (m *IngressMigrator) Migrate(ctx context.Context, namespace string) ([]*gatewayv1.HTTPRoute, error) {
ingresses, err := m.clientset.NetworkingV1().Ingresses(namespace).List(ctx, metav1.ListOptions{})
if err != nil {
return nil, fmt.Errorf("list ingresses: %w", err)
}
var routes []*gatewayv1.HTTPRoute
for _, ing := range ingresses.Items {
route, err := m.convertIngressToHTTPRoute(&ing)
if err != nil {
fmt.Fprintf(os.Stderr, "skip ingress %s: %v\n", ing.Name, err)
continue
}
routes = append(routes, route)
}
return routes, nil
}
func (m *IngressMigrator) convertIngressToHTTPRoute(ing *networkingv1.Ingress) (*gatewayv1.HTTPRoute, error) {
route := &gatewayv1.HTTPRoute{
TypeMeta: metav1.TypeMeta{
APIVersion: "gateway.networking.k8s.io/v1",
Kind: "HTTPRoute",
},
ObjectMeta: metav1.ObjectMeta{
Name: ing.Name + "-route",
Namespace: ing.Namespace,
Annotations: filterGatewayAnnotations(ing.Annotations),
},
Spec: gatewayv1.HTTPRouteSpec{
ParentRefs: []gatewayv1.ParentReference{
{
Name: gatewayv1.ObjectName(m.gatewayName),
Namespace: (*gatewayv1.Namespace)(&m.gatewayNS),
},
},
},
}
for _, rule := range ing.Spec.Rules {
if rule.Host != "" {
hostname := gatewayv1.PreciseHostname(rule.Host)
route.Spec.Hostnames = append(route.Spec.Hostnames, hostname)
}
for _, path := range rule.HTTP.Paths {
matchType := gatewayv1.PathMatchPathPrefix
if path.PathType != nil && *path.PathType == networkingv1.PathTypeExact {
matchType = gatewayv1.PathMatchExact
}
route.Spec.Rules = append(route.Spec.Rules, gatewayv1.HTTPRouteRule{
Matches: []gatewayv1.HTTPRouteMatch{
{
Path: &gatewayv1.HTTPPathMatch{
Type: &matchType,
Value: &path.Path,
},
},
},
BackendRefs: []gatewayv1.HTTPBackendRef{
{
BackendRef: gatewayv1.BackendRef{
BackendObjectReference: gatewayv1.BackendObjectReference{
Name: gatewayv1.ObjectName(path.Backend.Service.Name),
Port: (*gatewayv1.PortNumber)(&path.Backend.Service.Port.Number),
},
},
},
},
})
}
}
return route, nil
}
func filterGatewayAnnotations(annotations map[string]string) map[string]string {
filtered := make(map[string]string)
for k, v := range annotations {
if k == "kubernetes.io/ingress.class" || k == "nginx.ingress.kubernetes.io/rewrite-target" {
continue
}
filtered[k] = v
}
return filtered
}
func main() {
kubeconfig := os.Getenv("KUBECONFIG")
if kubeconfig == "" {
kubeconfig = clientcmd.RecommendedHomeFile
}
migrator, err := NewIngressMigrator(kubeconfig, "production-gateway-class", "production-gateway", "gateway-system")
if err != nil {
fmt.Fprintf(os.Stderr, "init migrator: %v\n", err)
os.Exit(1)
}
routes, err := migrator.Migrate(context.Background(), "default")
if err != nil {
fmt.Fprintf(os.Stderr, "migrate: %v\n", err)
os.Exit(1)
}
for _, route := range routes {
fmt.Printf("Generated HTTPRoute: %s/%s\n", route.Namespace, route.Name)
}
fmt.Printf("Total: %d routes migrated\n", len(routes))
}
ステップ5:カナリアリリースとトラフィック分割
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: api-canary-route
namespace: app-team-a
spec:
parentRefs:
- name: production-gateway
namespace: gateway-system
hostnames:
- "api.example.com"
rules:
- matches:
- headers:
- type: Exact
name: X-Canary
value: "true"
backendRefs:
- name: api-v2-service
port: 8080
weight: 100
- backendRefs:
- name: api-v1-service
port: 8080
weight: 95
- name: api-v2-service
port: 8080
weight: 5
Gateway APIはヘッダーベースとウェイトベースのトラフィック分割をネイティブサポートし、NginxのCanaryアノテーションは不要。5%のトラフィックから開始し、徐々に100%まで増加させてカナリアリリースを完了する。
ステップ6:マルチクラスターゲートウェイ設定
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: multi-cluster-gateway-class
spec:
controllerName: k8s.io/nginx-gateway-controller
parametersRef:
group: multicluster.x-k8s.io
kind: ServiceImport
name: multi-cluster-params
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: cross-cluster-route
namespace: app-team-a
spec:
parentRefs:
- name: production-gateway
namespace: gateway-system
hostnames:
- "api.example.com"
rules:
- backendRefs:
- name: api-service-cluster-east
port: 8080
weight: 70
- name: api-service-cluster-west
port: 8080
weight: 30
マルチクラスターゲートウェイはServiceImportとMulti-Cluster Service APIを使用してクラスタ間トラフィック分散を実現し、ウェイト制御でリージョンアフィニティとフェイルオーバーを実現する。
5つのよくある落とし穴
❌ 落とし穴1:GatewayとRouteを同じネームスペースに配置
✅ Gatewayはgateway-systemに、Routeはアプリケーションネームスペースに配置し、allowedRoutesでアクセス権を制御する。ロール分離はGateway APIのコア設計原則。
❌ 落とし穴2:クロスネームスペース参照のReferenceGrant設定を忽略 ✅ クロスネームスペースでServiceやSecretを参照する場合、対象ネームスペースにReferenceGrantを作成する必要がある。さもなくばルートは有効にならない。
❌ 落とし穴3:Gatewayを作成する前にIngressを削除 ✅ まずGatewayとHTTPRouteを作成し、ルートの動作を確認してからIngressを削除する。スムーズな移行のために。
❌ 落とし穴4:HTTPRouteのウェイト合計が100にならない ✅ ウェイトは相対値であり、合計は100である必要はないが、理解と計算を容易にするため合計100を維持することを推奨。
❌ 落とし穴5:GatewayClassにparametersRefを指定しない ✅ プロダクションでは必ずparametersRefでコントローラーパラメータを設定し、デフォルト値によるパフォーマンスボトルネックを回避する。
10のエラートラブルシューティング
| エラー症状 | 考えられる原因 | デバッグコマンド | 解決策 |
|---|---|---|---|
| HTTPRouteステータスが空 | parentRefが存在しないGatewayを指している | kubectl get gateway -A |
Gateway名とネームスペースを確認 |
| ルートが有効にならない | allowedRoutesがRouteネームスペースを許可していない | kubectl describe gateway |
ネームスペースラベルを追加またはAllに変更 |
| TLS証明書がロードされない | certificateRefsのSecretが存在しない | kubectl get secret -n gateway-system |
TLS Secretを作成 |
| クロスネームスペースルートが失敗 | ReferenceGrantが不足 | kubectl get referencegrant -A |
ReferenceGrantを作成して参照を許可 |
| カナリアトラフィックが分割されない | weightが0または未設定 | kubectl describe httproute |
backendRefsのweight値を確認 |
| GatewayClassが利用不可 | コントローラーPodが実行されていない | kubectl get pods -n gateway-system |
Gatewayコントローラーを起動 |
| TCPRouteが動作しない | GatewayがTCPポートでリッスンしていない | kubectl describe gateway |
TCPプロトコルのリスナーを追加 |
| GRPCRouteルーティングが失敗 | バックエンドServiceがgRPCをサポートしていない | kubectl describe svc |
ServiceがgRPCプロトコルをサポートしているか確認 |
| 移行スクリプトが404を返す | client-goバージョンがクラスターと非互換 | kubectl version |
client-goとK8sバージョンを一致させる |
| マルチクラスタールートが不均衡 | ServiceImportの設定ミス | kubectl get serviceimport -A |
Multi-Cluster Service設定を確認 |
高度な最適化テクニック
1. Gatewayインフラ分離。異なるSLAのサービスに対して異なるGatewayClassを作成する。コアサービスは専用Gatewayインスタンスを使用し、共有インフラの干渉を回避する。
2. ルートポリシーの組み合わせ。HTTPRouteはネストされたmatchesをサポートし、パス、ヘッダー、クエリパラメータを組み合わせてきめ細かいルーティングを実現。例:/api/v2パスとX-Feature-Flag: enabledヘッダーの同時マッチ。
3. タイムアウトとリトライポリシー。backendReqTimeoutとリトライポリシーアノテーションを使用して、ルーティングレイヤーでタイムアウト制御と自動リトライを実装し、アプリケーションレベルのコードを削減。
4. オブザーバビリティ統合。Gateway APIはPolicyAttachmentを通じてOpenTelemetry統合をサポートし、ルートレベルのTraceとMetrics収集を実現。
5. 段階的移行。kubectl apply --dry-run=serverでYAMLを検証し、kubectl diffで変更をプレビューして、ゼロリスク移行を確保。
比較:Ingress vs Gateway API vs カスタムゲートウェイ
| 特徴 | Ingress | Gateway API | カスタムゲートウェイ |
|---|---|---|---|
| ロール分離 | ❌ 全て混在 | ✅ インフラ/運用/開発の分離 | ⚠️ 実装による |
| クロスネームスペース | ❌ サポートなし | ✅ ReferenceGrant | ✅ 自由設定 |
| トラフィック分割 | ⚠️ アノテーションハック | ✅ ネイティブweight | ✅ 自由設定 |
| TCP/gRPC | ❌ アノテーションハック | ✅ ネイティブサポート | ✅ 自由設定 |
| マルチクラスター | ❌ サポートなし | ⚠️ MCS APIが必要 | ✅ 自由設定 |
| ポータビリティ | ⚠️ アノテーション非互換 | ✅ 標準化API | ❌ 完全に結合 |
| 学習コスト | 低 | 中 | 高 |
| 運用コスト | 中 | 低 | 高 |
| コミュニティサポート | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| プロダクション推奨度 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
おすすめツール
- JSONフォーマッター — Gateway APIのYAML/JSON設定をフォーマット、リソース定義の問題を素早く特定
- ハッシュ計算ツール — TLS証明書とConfigMapのチェックサムを計算、ゲートウェイ設定データの整合性を確保
- cURL to Code — cURLテストコマンドをGoコードに変換、Gateway APIクライアント開発を加速
まとめ
Gateway APIはIngressの単なるアップグレードではなく、K8sトラフィック管理のパラダイムシフトである。「1つのIngressで全てを管理」から「ロール分離、責務の明確化」へ、「アノテーション地獄」から「標準化API」へ、「単一クラスター」から「ネイティブマルチクラスター対応」へ。6つの移行ステップ——GatewayClass定義、Gateway設定、HTTPRouteルーティング、Go移行スクリプト、カナリアトラフィック分割、マルチクラスターゲートウェイ——がプロダクション移行の完全なチェーンをカバーする。覚えておくべきは:先に作成してから削除、段階的移行、各ステップを検証——これがゼロダウンタイム移行の鍵である。
参考資料
ブラウザローカルツールを無料で試す →