Skip to content
Back to blog Kubernetes Gateway API vs Ingress - When to Migrate and How

Kubernetes Gateway API vs Ingress - When to Migrate and How

K8sNetworking

Kubernetes Gateway API vs Ingress - When to Migrate and How

Ingress has been the standard for HTTP routing in Kubernetes since 2015. It works, but it’s showing its age. Advanced features require controller-specific annotations, there’s no native traffic splitting, and multi-team setups often become a mess of conflicting configurations.

Gateway API is the official successor - a complete redesign that addresses these limitations while remaining portable across implementations. It went GA in October 2023 with v1.0, and most major ingress controllers now support it.

This post compares both APIs, explains when migration makes sense, and provides practical migration patterns.

TL;DR

  • Gateway API is the successor to Ingress, not a replacement for service meshes
  • It’s GA since v1.0 (October 2023) with HTTPRoute, Gateway, and GatewayClass stable
  • Key improvements: role-oriented design, native traffic splitting, header-based routing, cross-namespace support
  • Migrate when you need features Ingress can’t provide natively
  • Both can coexist - migrate incrementally

Code Repository: All code from this post is available at github.com/moabukar/blog-code/gateway-api-vs-ingress


The Problem with Ingress

Ingress was designed for simple HTTP routing. It handles the basics well:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: simple-ingress
spec:
  ingressClassName: nginx
  rules:
  - host: app.example.com
    http:
      paths:
      - path: /api
        pathType: Prefix
        backend:
          service:
            name: api-service
            port:
              number: 80

But real-world requirements quickly exceed what Ingress can express natively:

Traffic splitting for canary deployments? Annotations.

# NGINX-specific - won't work with Traefik or HAProxy
metadata:
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "20"

Header-based routing? Annotations.

# Again, controller-specific
metadata:
  annotations:
    nginx.ingress.kubernetes.io/canary-by-header: "X-Canary"

Request/response header modification? You guessed it - annotations.

The result is configuration that’s:

  • Not portable - switch controllers and rewrite everything
  • Not discoverable - no schema, no validation, just strings
  • Not composable - one Ingress per namespace, no cross-team sharing

What Gateway API Changes

Gateway API redesigns the model with three key resources:

┌─────────────────────────────────────────────────────────────────┐
│                        GatewayClass                             │
│            (Managed by Infrastructure Provider)                 │
│                 Defines controller + config                     │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│                          Gateway                                │
│               (Managed by Cluster Operator)                     │
│         Defines listeners, ports, TLS, allowed routes           │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│                    HTTPRoute / GRPCRoute                        │
│              (Managed by Application Developer)                 │
│         Defines routing rules, backends, traffic policies       │
└─────────────────────────────────────────────────────────────────┘

This separation isn’t just aesthetic - it enables different teams to manage different layers without stepping on each other.

GatewayClass - The Infrastructure Layer

apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: istio
spec:
  controllerName: istio.io/gateway-controller

GatewayClass is like StorageClass for networking. It defines which controller handles Gateways of this class. Platform teams deploy this once; application teams reference it.

Gateway - The Cluster Operator Layer

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: shared-gateway
  namespace: infra
spec:
  gatewayClassName: istio
  listeners:
  - name: https
    protocol: HTTPS
    port: 443
    hostname: "*.example.com"
    tls:
      mode: Terminate
      certificateRefs:
      - name: wildcard-cert
    allowedRoutes:
      namespaces:
        from: Selector
        selector:
          matchLabels:
            gateway-access: "true"

Key points:

  • Listeners define what traffic the Gateway accepts
  • allowedRoutes controls which namespaces can attach routes
  • hostname can use wildcards, enabling multi-tenant setups
  • The Gateway owner controls who can expose services through it

HTTPRoute - The Application Developer Layer

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: api-route
  namespace: team-a
spec:
  parentRefs:
  - name: shared-gateway
    namespace: infra
  hostnames:
  - "api.example.com"
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /v1
    backendRefs:
    - name: api-v1
      port: 80
      weight: 90
    - name: api-v2
      port: 80
      weight: 10

Application developers create HTTPRoutes in their namespace. They reference a Gateway (potentially in another namespace) and define routing rules. The Gateway owner already approved their namespace via allowedRoutes.


Feature Comparison

Here’s what each API supports natively (without annotations):

FEATURE                          INGRESS         GATEWAY API
=======                          =======         ===========
Path-based routing               ✓               ✓
Host-based routing               ✓               ✓
TLS termination                  ✓               ✓
Traffic splitting (weights)      ✗ (annotation)  ✓
Header-based routing             ✗ (annotation)  ✓
Header modification              ✗ (annotation)  ✓
Request mirroring                ✗ (annotation)  ✓
Cross-namespace routing          ✗               ✓
Role-based resource model        ✗               ✓
gRPC-native routing              ✗               ✓ (GRPCRoute)
TCP/UDP routing                  ✗               ✓ (TCPRoute/UDPRoute)
Multiple controllers per cluster ✗ (awkward)     ✓

Native Traffic Splitting

This is often the killer feature that drives migration. In Ingress, canary deployments require controller-specific annotations. In Gateway API, it’s a first-class concept:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: canary-route
spec:
  parentRefs:
  - name: production-gateway
  hostnames:
  - "app.example.com"
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /
    backendRefs:
    - name: app-stable
      port: 80
      weight: 95
    - name: app-canary
      port: 80
      weight: 5

Shift traffic by changing weights. No annotations, no controller-specific syntax.

Header-Based Routing

Route specific users to canary based on headers:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: header-canary
spec:
  parentRefs:
  - name: production-gateway
  hostnames:
  - "app.example.com"
  rules:
  # Internal testers go to canary
  - matches:
    - headers:
      - name: X-Internal-Tester
        value: "true"
    backendRefs:
    - name: app-canary
      port: 80
  # Everyone else gets stable
  - matches:
    - path:
        type: PathPrefix
        value: /
    backendRefs:
    - name: app-stable
      port: 80

Cross-Namespace Routing

In large organisations, platform teams manage ingress infrastructure while application teams manage their services. Gateway API supports this natively.

Platform team (infra namespace):

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: shared-gateway
  namespace: infra
spec:
  gatewayClassName: envoy
  listeners:
  - name: https
    protocol: HTTPS
    port: 443
    hostname: "*.prod.example.com"
    allowedRoutes:
      namespaces:
        from: Selector
        selector:
          matchLabels:
            environment: production

Team A (team-a namespace):

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: team-a-api
  namespace: team-a
  labels:
    environment: production
spec:
  parentRefs:
  - name: shared-gateway
    namespace: infra
  hostnames:
  - "api.prod.example.com"
  rules:
  - backendRefs:
    - name: api-service
      port: 80

Team B (team-b namespace):

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: team-b-dashboard
  namespace: team-b
  labels:
    environment: production
spec:
  parentRefs:
  - name: shared-gateway
    namespace: infra
  hostnames:
  - "dashboard.prod.example.com"
  rules:
  - backendRefs:
    - name: dashboard-service
      port: 80

Teams don’t need to coordinate or share manifests. The Gateway controls access via namespace selectors.


Request/Response Modification

Header manipulation is native in Gateway API:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: modified-route
spec:
  parentRefs:
  - name: api-gateway
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /api
    filters:
    - type: RequestHeaderModifier
      requestHeaderModifier:
        add:
        - name: X-Request-ID
          value: "${request_id}"
        set:
        - name: X-Forwarded-Proto
          value: https
        remove:
        - X-Debug-Header
    - type: ResponseHeaderModifier
      responseHeaderModifier:
        add:
        - name: X-Frame-Options
          value: DENY
        - name: Strict-Transport-Security
          value: "max-age=31536000; includeSubDomains"
    backendRefs:
    - name: api-service
      port: 80

When to Migrate

Migrate when:

  • You need traffic splitting for canary/blue-green deployments
  • Multiple teams share ingress infrastructure
  • You’re tired of controller-specific annotations
  • You need gRPC routing (GRPCRoute is cleaner than Ingress hacks)
  • You want header-based routing without annotations
  • You’re deploying new clusters and want to start clean

Stay with Ingress when:

  • Your current setup works and you don’t need advanced features
  • Your ingress controller doesn’t support Gateway API yet
  • You have heavy investment in Ingress tooling (GitOps, policies)
  • You’re running older Kubernetes (Gateway API needs 1.24+)

Reality check: Both can coexist. You don’t need to migrate everything at once. Run Gateway API for new services, keep Ingress for existing ones.


Migration Strategy

Step 1: Check Controller Support

Most major controllers support Gateway API:

CONTROLLER              GATEWAY API STATUS    NOTES
==========              ==================    =====
NGINX Gateway Fabric    GA                    Separate product from NGINX Ingress
Istio                   GA                    Full support since 1.16
Envoy Gateway           GA                    CNCF project
Traefik                 GA                    Since v3.0
Contour                 GA                    Full support
Cilium                  GA                    With Cilium Gateway API
Kong                    GA                    Kong Gateway Operator
AWS ALB Controller      Partial               Basic support
GKE Gateway Controller  GA                    GCP native

Step 2: Install Gateway API CRDs

Gateway API resources are CRDs, not built into Kubernetes:

# Install standard channel (stable resources)
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.0/standard-install.yaml

# Or include experimental resources (TLSRoute, TCPRoute, UDPRoute)
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.0/experimental-install.yaml

Step 3: Deploy a GatewayClass

Your controller provides this, or you create one:

apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: nginx
spec:
  controllerName: gateway.nginx.org/nginx-gateway-controller

Step 4: Create a Gateway

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: main-gateway
  namespace: infra
spec:
  gatewayClassName: nginx
  listeners:
  - name: http
    protocol: HTTP
    port: 80
    allowedRoutes:
      namespaces:
        from: All
  - name: https
    protocol: HTTPS
    port: 443
    tls:
      mode: Terminate
      certificateRefs:
      - name: tls-secret
    allowedRoutes:
      namespaces:
        from: All

Step 5: Convert Ingress to HTTPRoute

Before (Ingress):

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /v1
        pathType: Prefix
        backend:
          service:
            name: api-v1
            port:
              number: 80

After (HTTPRoute):

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: api-route
spec:
  parentRefs:
  - name: main-gateway
    namespace: infra
  hostnames:
  - "api.example.com"
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /v1
    filters:
    - type: URLRewrite
      urlRewrite:
        path:
          type: ReplacePrefixMatch
          replacePrefixMatch: /
    backendRefs:
    - name: api-v1
      port: 80

Step 6: Run Both, Then Cut Over

During migration, both Ingress and Gateway API can route traffic. Test the new Gateway API routes with a subset of traffic or internal DNS, then switch production DNS once validated.


ReferenceGrant - Cross-Namespace Backend Access

By default, HTTPRoutes can only reference backends in the same namespace. To reference backends in other namespaces, the target namespace must grant permission:

apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
  name: allow-infra-routes
  namespace: backend-services
spec:
  from:
  - group: gateway.networking.k8s.io
    kind: HTTPRoute
    namespace: infra
  to:
  - group: ""
    kind: Service

This allows HTTPRoutes in the infra namespace to reference Services in backend-services.


Terraform Example

Deploy Gateway API infrastructure with Terraform:

# Install Gateway API CRDs
resource "kubernetes_manifest" "gateway_api_crds" {
  manifest = yamldecode(file("${path.module}/gateway-api-crds.yaml"))
}

# GatewayClass
resource "kubernetes_manifest" "gateway_class" {
  manifest = {
    apiVersion = "gateway.networking.k8s.io/v1"
    kind       = "GatewayClass"
    metadata = {
      name = "nginx"
    }
    spec = {
      controllerName = "gateway.nginx.org/nginx-gateway-controller"
    }
  }
}

# Gateway
resource "kubernetes_manifest" "main_gateway" {
  manifest = {
    apiVersion = "gateway.networking.k8s.io/v1"
    kind       = "Gateway"
    metadata = {
      name      = "main-gateway"
      namespace = "infra"
    }
    spec = {
      gatewayClassName = "nginx"
      listeners = [
        {
          name     = "https"
          protocol = "HTTPS"
          port     = 443
          hostname = "*.example.com"
          tls = {
            mode = "Terminate"
            certificateRefs = [{
              name = "wildcard-cert"
            }]
          }
          allowedRoutes = {
            namespaces = {
              from = "Selector"
              selector = {
                matchLabels = {
                  "gateway-access" = "true"
                }
              }
            }
          }
        }
      ]
    }
  }
}

Troubleshooting

Route Not Attaching to Gateway

Check the HTTPRoute status:

kubectl describe httproute api-route

Look for conditions:

Status:
  Parents:
  - ControllerName: gateway.nginx.org/nginx-gateway-controller
    ParentRef:
      Name: main-gateway
      Namespace: infra
    Conditions:
    - Type: Accepted
      Status: "False"
      Reason: NotAllowedByListeners

Common causes:

  • Namespace not allowed by Gateway’s allowedRoutes
  • Hostname doesn’t match Gateway’s listener hostname
  • Missing ReferenceGrant for cross-namespace backends

Gateway Not Ready

kubectl describe gateway main-gateway -n infra

Check for:

  • Missing TLS secrets
  • Port conflicts
  • Controller not running

Verify Routes Are Programmed

# Check Gateway status
kubectl get gateway main-gateway -n infra -o yaml

# Check HTTPRoute status  
kubectl get httproute -A -o wide

What’s Next for Gateway API

Gateway API is actively evolving:

  • GRPCRoute - GA since v1.1.0 for native gRPC routing
  • Service Mesh (GAMMA) - East-west traffic management, GA since v1.1.0
  • BackendTLSPolicy - Configure TLS to backends
  • Timeouts - Native request timeout configuration
  • Gateway infrastructure - Cloud-specific infrastructure attachment

The API is designed for extension. Implementations can add custom policies while maintaining portability for core features.


Conclusion

Gateway API isn’t just “Ingress v2” - it’s a fundamental redesign that acknowledges how organisations actually operate Kubernetes networking. The role-oriented model, native traffic management, and cross-namespace support solve real problems that Ingress can only address through controller-specific hacks.

If your current Ingress setup works and you don’t need advanced features, there’s no rush to migrate. But for new deployments or when you hit Ingress limitations, Gateway API is the clear path forward.

Start with one service, validate the workflow, then expand. Both APIs coexist peacefully.


References

Found this helpful?

Comments