Go K8s Gateway API生产实战:从Ingress迁移的6个关键步骤

云原生

当Ingress成为瓶颈:K8s流量管理的至暗时刻

凌晨2点,运维团队紧急上线一个灰度发布。Ingress的nginx注解改了3遍,nginx.ingress.kubernetes.io/canary 配置始终不生效。更头疼的是,3个团队共用一个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

目录

  1. 问题分析:Ingress的5大挑战
  2. 步骤1:GatewayClass与基础设施配置
  3. 步骤2:Gateway资源定义
  4. 步骤3:HTTPRoute路由规则配置
  5. 步骤4:Ingress到Gateway API迁移脚本(Go)
  6. 步骤5:金丝雀发布与流量切分
  7. 步骤6:多集群网关配置
  8. 5大常见陷阱
  9. 10大错误排查
  10. 高级优化技巧
  11. 方案对比:Ingress vs Gateway API vs 自建网关
  12. 推荐工具
  13. 总结与延伸阅读

问题分析:Ingress的5大挑战

挑战1:注解不一致。Nginx Ingress用nginx.ingress.kubernetes.io/rewrite-target,Traefik用traefik.ingress.kubernetes.io/rewrite-target,注解不兼容导致控制器迁移成本极高。

挑战2:跨命名空间路由。Ingress的backend只能引用同命名空间的Service,跨命名空间流量必须用ExternalName Service hack,安全且不可控。

挑战3:多集群网关。Ingress设计上只考虑单集群,多集群场景需要额外引入Federation或自建控制面。

挑战4:金丝雀发布复杂。Nginx Ingress的Canary注解需要创建两个Ingress对象,权重配置不直观,且与常规路由混在一起难以管理。

挑战5:协议支持有限。Ingress只支持HTTP/HTTPS,TCP/UDP需要额外配置,gRPC需要注解hack,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的特定listener,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原生支持基于Header和权重的流量切分,无需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:直接删除Ingress再创建Gateway ✅ 应先创建Gateway和HTTPRoute,验证路由生效后再删除Ingress,实现平滑迁移。

❌ 陷阱4:HTTPRoute的weight总和不为100 ✅ weight是相对权重,总和不需要为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协议的listener
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 ✅ 自由配置
流量切分 ⚠️ 注解hack ✅ 原生weight ✅ 自由配置
TCP/gRPC ❌ 注解hack ✅ 原生支持 ✅ 自由配置
多集群 ❌ 不支持 ⚠️ 需要MCS API ✅ 自由配置
可移植性 ⚠️ 注解不兼容 ✅ 标准化API ❌ 完全绑定
学习成本
运维成本
社区支持 ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐
生产推荐度 ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐

推荐工具

  • JSON格式化工具 — 格式化Gateway API的YAML/JSON配置,快速排查资源定义问题
  • 哈希计算工具 — 计算TLS证书和ConfigMap校验值,确保网关配置数据完整性
  • cURL转代码工具 — 将cURL测试命令转为Go代码,加速Gateway API客户端开发

总结

Gateway API不是Ingress的简单升级,而是K8s流量管理的范式转变。从"一个Ingress打天下"到"角色分离、职责清晰",从"注解地狱"到"标准化API",从"单集群"到"多集群原生支持"。6个迁移步骤——GatewayClass定义、Gateway配置、HTTPRoute路由、Go迁移脚本、金丝雀流量切分、多集群网关——覆盖了生产迁移的完整链路。记住:先创建后删除、渐进式迁移、验证每一步,才能确保零停机迁移。


延伸阅读

本站提供浏览器本地工具,免注册即可试用 →

#Kubernetes Gateway API#K8s网关#Ingress迁移#Go#流量管理#2026#云原生