All Articles

How to Migrate from Ingress-NGINX to Kubernetes Gateway API in 2026

Step-by-step guide to migrating from Ingress-NGINX to Kubernetes Gateway API. Includes YAML examples, implementation choices, testing strategy, and cutover plan.

DevOpsBoysMar 27, 20269 min read
Share:Tweet

Ingress-NGINX has been the default Kubernetes ingress controller for years. It's been reliable, well-documented, and understood by nearly every DevOps engineer. But in 2026, the Kubernetes ecosystem is moving decisively toward the Gateway API, and Ingress-NGINX is entering maintenance mode.

The Kubernetes Gateway API (graduated to GA in Kubernetes 1.29) is not just a replacement for Ingress — it's a complete rethinking of how traffic enters and routes through a Kubernetes cluster. It's more expressive, more portable, and designed for the multi-team, multi-tenant reality of modern Kubernetes.

If you're still running Ingress-NGINX, now is the time to migrate. Here's exactly how to do it.

Why Migrate Now?

Three reasons:

  1. Ingress-NGINX maintenance is winding down. The project announced reduced maintenance focus in early 2026. Security patches will continue, but new features have stopped.

  2. Gateway API is GA and production-ready. It's no longer experimental. Major implementations (Envoy Gateway, Traefik, Cilium, Istio, NGINX Gateway Fabric) are stable.

  3. Gateway API solves real problems that Ingress never could — role-based configuration, cross-namespace routing, traffic splitting, header-based matching, and more — all without vendor-specific annotations.

Understanding the Key Differences

Before we migrate, let's understand what's changing conceptually.

Ingress Model

Ingress resource → Ingress Controller (NGINX) → Backend Services

Everything is in one resource. The Ingress object defines hosts, paths, TLS, and backends all in a single YAML file. Fine for simple cases, but it leads to annotation sprawl for anything complex.

Gateway API Model

GatewayClass → Gateway → HTTPRoute → Backend Services

The configuration is split across three layers:

  • GatewayClass: Defines which controller handles the traffic (like StorageClass for volumes). Managed by the infrastructure team.
  • Gateway: Defines the actual listener — ports, protocols, TLS certificates. Managed by the cluster/platform team.
  • HTTPRoute: Defines routing rules — hosts, paths, headers, backends. Managed by application teams.

This separation is the killer feature. Application teams can manage their own routing without touching gateway infrastructure. No more "please add this annotation to the shared Ingress controller" tickets.

Step 0: Audit Your Current Ingress Resources

Before you change anything, inventory what you have. Run this across your cluster:

bash
# List all Ingress resources
kubectl get ingress -A -o wide
 
# Export all Ingress resources for reference
kubectl get ingress -A -o yaml > ingress-backup.yaml
 
# Count unique annotations (these are the tricky part)
kubectl get ingress -A -o json | jq -r '.items[].metadata.annotations // {} | keys[]' | sort | uniq -c | sort -rn

The annotations are what make migration interesting. Common Ingress-NGINX annotations and their Gateway API equivalents:

Ingress-NGINX AnnotationGateway API Equivalent
nginx.ingress.kubernetes.io/rewrite-targetHTTPRoute URLRewrite filter
nginx.ingress.kubernetes.io/ssl-redirectHTTPRoute RequestRedirect filter
nginx.ingress.kubernetes.io/proxy-body-sizeImplementation-specific policy
nginx.ingress.kubernetes.io/rate-limitingImplementation-specific policy
nginx.ingress.kubernetes.io/cors-*HTTPRoute ResponseHeaderModifier or policy
nginx.ingress.kubernetes.io/auth-urlImplementation-specific (ExtAuth)
nginx.ingress.kubernetes.io/canary-*HTTPRoute backendRefs with weights
nginx.ingress.kubernetes.io/affinityImplementation-specific policy

Some annotations map directly to Gateway API features. Others require implementation-specific policies (which means they depend on which Gateway API controller you choose).

Step 1: Choose a Gateway API Implementation

You need a controller that implements the Gateway API. Here are the major options in 2026:

Envoy Gateway

The official Envoy-based Gateway API implementation, backed by the Envoy Proxy community.

Pros: Strong community, excellent L7 features, extensible with Envoy filters
Cons: Heavier resource footprint than NGINX
Best for: Teams already using Envoy or Istio

Traefik

Traefik has supported Gateway API since v3.0 and is one of the most mature implementations.

Pros: Lightweight, great dashboard, easy to configure, middleware support
Cons: Some advanced features require Traefik-specific CRDs
Best for: Teams wanting a lightweight, batteries-included solution

NGINX Gateway Fabric

NGINX's own Gateway API implementation. If you're comfortable with NGINX, this is the closest migration path.

Pros: Familiar NGINX engine, straightforward migration from Ingress-NGINX
Cons: Smaller community than Envoy Gateway or Traefik
Best for: Teams that want to stay in the NGINX ecosystem

Cilium Gateway API

If you're running Cilium as your CNI, it has a built-in Gateway API implementation powered by eBPF.

Pros: No additional proxy pods, eBPF-powered performance, integrated with Cilium policies
Cons: Requires Cilium as CNI
Best for: Teams already running Cilium

For this guide, I'll use Envoy Gateway as the example, but the Gateway API resources (GatewayClass, Gateway, HTTPRoute) are the same regardless of implementation.

Step 2: Install Gateway API CRDs

The Gateway API CRDs are maintained separately from any implementation. Install them first:

bash
# Install the standard Gateway API CRDs (v1.2.0 as of March 2026)
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.0/standard-install.yaml
 
# Verify CRDs are installed
kubectl get crd | grep gateway

You should see:

gatewayclasses.gateway.networking.k8s.io
gateways.gateway.networking.k8s.io
httproutes.gateway.networking.k8s.io
referencegrants.gateway.networking.k8s.io

Step 3: Install Your Gateway API Controller

For Envoy Gateway:

bash
# Install Envoy Gateway using Helm
helm repo add envoy-gateway https://charts.envoyproxy.io
helm repo update
 
helm install envoy-gateway envoy-gateway/gateway-helm \
  --namespace envoy-gateway-system \
  --create-namespace \
  --version v1.3.0

For Traefik:

bash
helm repo add traefik https://traefik.github.io/charts
helm repo update
 
helm install traefik traefik/traefik \
  --namespace traefik-system \
  --create-namespace \
  --set providers.kubernetesGateway.enabled=true

Verify the GatewayClass is available:

bash
kubectl get gatewayclass
NAME            CONTROLLER                         ACCEPTED   AGE
envoy-gateway   gateway.envoyproxy.io/gatewayclass  True       30s

Step 4: Create a Gateway

The Gateway resource replaces the "listener" part of your Ingress controller. This is where you define which ports and protocols to accept, and which TLS certificates to use.

Before (Ingress-NGINX):

Your Ingress controller was configured via Helm values or a ConfigMap to listen on ports 80 and 443.

After (Gateway API):

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

Key differences:

  • allowedRoutes.namespaces.from: All lets any namespace attach HTTPRoutes to this Gateway. You can restrict this to specific namespaces or use label selectors.
  • TLS is configured on the Gateway listener, not on individual routes.
  • The Gateway gets its own LoadBalancer Service automatically.

Apply it:

bash
kubectl apply -f gateway.yaml
 
# Check the Gateway status
kubectl get gateway main-gateway -n gateway-system
NAME           CLASS           ADDRESS        PROGRAMMED   AGE
main-gateway   envoy-gateway   34.120.55.88   True         45s

Note the external IP — this is the new entry point for your traffic.

Step 5: Convert Ingress Resources to HTTPRoutes

This is the core of the migration. Let's convert real-world examples.

Example 1: Simple Host-Based Routing

Before (Ingress):

yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app-ingress
  namespace: default
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - myapp.example.com
      secretName: myapp-tls
  rules:
    - host: myapp.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-app
                port:
                  number: 80

After (HTTPRoute):

yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: my-app-route
  namespace: default
spec:
  parentRefs:
    - name: main-gateway
      namespace: gateway-system
  hostnames:
    - myapp.example.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: my-app
          port: 80

Notice how much cleaner this is. No annotations. No TLS config (it's on the Gateway). The HTTPRoute just says "for this hostname, route to this backend."

Example 2: Path-Based Routing with Rewrite

Before (Ingress):

yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-ingress
  namespace: default
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
  ingressClassName: nginx
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /v1(/|$)(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: api-v1
                port:
                  number: 8080
          - path: /v2(/|$)(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: api-v2
                port:
                  number: 8080

After (HTTPRoute):

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

The rewrite is now a first-class URLRewrite filter instead of a regex-heavy annotation. Much easier to read and debug.

Example 3: Canary Deployment (Traffic Splitting)

Before (Ingress-NGINX canary annotations):

yaml
# Primary Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app-primary
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  rules:
    - host: myapp.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-app-stable
                port:
                  number: 80
---
# Canary Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app-canary
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
  ingressClassName: nginx
  rules:
    - host: myapp.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-app-canary
                port:
                  number: 80

After (HTTPRoute with traffic splitting):

yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: my-app-route
  namespace: default
spec:
  parentRefs:
    - name: main-gateway
      namespace: gateway-system
  hostnames:
    - myapp.example.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: my-app-stable
          port: 80
          weight: 90
        - name: my-app-canary
          port: 80
          weight: 10

One resource instead of two. Weights are explicit. No annotation magic.

Example 4: HTTP to HTTPS Redirect

Before (Ingress):

yaml
annotations:
  nginx.ingress.kubernetes.io/ssl-redirect: "true"
  nginx.ingress.kubernetes.io/force-ssl-redirect: "true"

After (HTTPRoute):

yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: http-redirect
  namespace: default
spec:
  parentRefs:
    - name: main-gateway
      namespace: gateway-system
      sectionName: http  # Attach to HTTP listener only
  hostnames:
    - myapp.example.com
  rules:
    - filters:
        - type: RequestRedirect
          requestRedirect:
            scheme: https
            statusCode: 301

Step 6: Cross-Namespace Routing with ReferenceGrant

One of Gateway API's best features is cross-namespace routing. If your HTTPRoute in namespace app-team needs to reference a backend in namespace shared-services, you need a ReferenceGrant:

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

This says: "HTTPRoutes in the app-team namespace are allowed to reference Services in the shared-services namespace." This is a security improvement over Ingress, where any namespace could route to any service.

Step 7: Test Side-by-Side

Don't cut over immediately. Run both Ingress-NGINX and the Gateway API controller simultaneously.

  1. Deploy the Gateway with a separate LoadBalancer IP
  2. Create HTTPRoutes that mirror your existing Ingress resources
  3. Test with curl using the Gateway's IP directly:
bash
# Test against the new Gateway
curl -H "Host: myapp.example.com" https://34.120.55.88/
 
# Compare with the old Ingress controller
curl -H "Host: myapp.example.com" https://35.200.10.20/
  1. Run automated tests against the new Gateway
  2. Monitor error rates and latency on both paths

Step 8: DNS Cutover

Once you've validated the Gateway API routes:

  1. Update your DNS records to point to the new Gateway's LoadBalancer IP
  2. Keep the old Ingress-NGINX controller running for 24-48 hours (DNS TTL + cache clearing)
  3. Monitor for any traffic still hitting the old controller
  4. Once traffic drops to zero on the old controller, decommission it:
bash
# Remove old Ingress resources
kubectl delete ingress --all -A
 
# Uninstall Ingress-NGINX
helm uninstall ingress-nginx -n ingress-nginx
kubectl delete namespace ingress-nginx

Common Migration Pitfalls

1. Forgetting to Create ReferenceGrants

If your HTTPRoute references a Gateway in another namespace, you need a ReferenceGrant. Without it, the route will show Accepted: False in its status.

2. TLS Certificate Namespace

The Gateway references TLS secrets. If the secret is in a different namespace than the Gateway, you need a ReferenceGrant for that too.

3. Default Backend

Ingress has a defaultBackend concept. Gateway API handles this with a catch-all HTTPRoute:

yaml
rules:
  - backendRefs:
      - name: default-backend
        port: 80

No matches means "match everything."

4. Rate Limiting and Auth

These features are implementation-specific in Gateway API. Check your chosen controller's documentation for their policy CRDs.

If you're new to Kubernetes networking concepts, KodeKloud has excellent hands-on labs that cover Ingress, Services, and now Gateway API. Building muscle memory with real clusters is the fastest way to learn. For a test cluster to practice the migration, DigitalOcean Kubernetes clusters are affordable and quick to spin up.

Final Thoughts

Migrating from Ingress-NGINX to Gateway API isn't just about following the ecosystem — it's about getting access to a genuinely better networking model. The separation of concerns (GatewayClass → Gateway → HTTPRoute), first-class traffic splitting, cross-namespace routing, and the end of annotation-driven configuration are real improvements.

The migration is straightforward: audit your Ingress resources, install a Gateway API controller, convert your Ingress YAML to HTTPRoutes, test side-by-side, and cut over DNS. Most teams complete the migration in 1-2 weeks.

Start now. Your future self will thank you when you need canary deployments, header-based routing, or multi-team gateway management — and it just works.

Newsletter

Stay ahead of the curve

Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.

Related Articles

Comments