Skip to content

Guide to Migrating From Retired Ingress Nginx

Published: at 12:00 PM

Guide to Migrating From Retired Ingress Nginx

Table of Contents

Open Table of Contents

Introduction

If you’re a DevOps engineer maintaining Kubernetes clusters, you’ve likely heard the news: ingress-nginx is being retired in March 2026. For many of us, this isn’t just an inconvenience - it’s a significant undertaking that requires careful planning and execution.

I get it. You’ve spent time configuring ingress-nginx, tuning its annotations, setting up TLS termination, and building automation around it. Now you’re being told to migrate to something new on someone else’s timeline. This guide is here to help you navigate that transition with as little pain as possible (and also for me to use as reference at my job when I have to do this for dozens of applications, including a very not-well-documented UDP tunnel…)

The good news? The Kubernetes Gateway API represents a genuine improvement in how we handle ingress traffic. The concepts are cleaner, the API is more expressive, and the ecosystem of implementations gives you real choice. The bad news? We all still have to do the migration.

Let’s walk through what’s changing, why, and how to get your clusters onto a supported solution before the deadline.

What is Ingress Nginx and How You’re Probably Using It

Before we talk about migration, let’s establish a baseline. Ingress-nginx is (was?) the most popular ingress controller in the Kubernetes ecosystem. It wraps the battle-tested NGINX reverse proxy and integrates it with Kubernetes through the Ingress resource.

If you’re running ingress-nginx, your setup probably looks something like this production ingress from my work, anonymized for this post:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: test-ingress
  namespace: my-namespace
  annotations:
    # Rewrite the name so my-host.com/direct/myservice/myPath passes
    # only /myPath to the backend service
    nginx.ingress.kubernetes.io/rewrite-target: /$2
    # Works with cert manager to provision certificates and force SSL
    cert-manager.io/cluster-issuer: my-issuer
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
spec:
  ingressClassName: "nginx"
  tls:
    - hosts:
        - my-host.com
      secretName: my-tls
  rules:
    - host: my-host.com
      http:
        paths:
          # /api/myservice(/|$)(.*) matches /api/myservice/myPath
          # /myPath is forwarded to the backend service
          - path: /api/myservice(/|$)(.*)
            pathType: Prefix
            backend:
              service:
                name: "my-service"
                port:
                  number: 8080

You might also be using:

The more customization you’ve done, the more work you’ll have ahead. But don’t worry - we’ll cover strategies for all of these scenarios.

Why is ingress-nginx Being Retired?

The official announcement from the Kubernetes project explains the reasoning, but here’s the practical summary:

The Ingress API has fundamental limitations. The Ingress resource was designed years ago and hasn’t aged well. It lacks support for:

Annotations became a crutch. Every ingress controller implemented its own annotation scheme to work around Ingress limitations. This led to:

Gateway API is the future. The Kubernetes project has invested heavily in Gateway API as the successor to Ingress. It addresses all the limitations above with a clean, extensible design.

Maintenance burden. The ingress-nginx project has struggled with maintainer capacity. Rather than let it languish, the decision was made to retire it and direct users toward actively maintained Gateway API implementations.

What “Retired” Actually Means

Let’s be clear about the timeline:

DateWhat Happens
November 2025Retirement announced
March 2026Final release, security patches only
September 2026End of security patches

After March 2026, you won’t get new features. After September 2026, you won’t get security fixes. You can technically keep running it forever, but you’ll be accumulating technical debt and security risk.

What is the Gateway API?

The Gateway API is a collection of Kubernetes resources that provide a more expressive and extensible way to configure ingress traffic. It’s developed by the Kubernetes SIG-Network community and is designed to be the long-term replacement for the Ingress resource.

Core Concepts

Gateway API introduces a clear separation of concerns through three main resource types:

GatewayClass - Defines the controller implementation (similar to IngressClass)

Very often, this is provided by your chosen implementation provider.

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

Gateway - Represents the actual load balancer/proxy infrastructure

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: example-gateway
  namespace: gateway-system
spec:
  gatewayClassName: example-gateway-class
  listeners:
    - name: http
      port: 80
      protocol: HTTP
    - name: https
      port: 443
      protocol: HTTPS
      tls:
        mode: Terminate
        certificateRefs:
          - name: example-cert

HTTPRoute (and other Route types) - Defines how traffic is routed to services

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: example-route
spec:
  parentRefs:
    - name: example-gateway
      namespace: gateway-system
  hostnames:
    - "app.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api
      backendRefs:
        - name: api-service
          port: 8080

Here’s the official diagram that lays out the architecture clearly:

Gateway API Architecture Diagram

Why This Design is Better

The separation might seem like more YAML at first, but it enables:

  1. Role-based access control - Platform teams manage GatewayClass and Gateway resources, application teams manage Routes
  2. Multi-tenancy - Multiple teams can attach Routes to shared Gateways without stepping on each other
  3. Portability - Routes work across different Gateway implementations with minimal changes
  4. Extensibility - New route types (GRPCRoute, TCPRoute, UDPRoute) can be added without changing the core API

Comparing Ingress Nginx with Gateway API Concepts

Let’s map the concepts you know from ingress-nginx to their Gateway API equivalents:

Ingress Nginx ConceptGateway API EquivalentNotes
IngressClassGatewayClassDefines which controller handles resources
Ingress Controller (deployment)GatewayThe actual proxy/load balancer
Ingress resourceHTTPRoute, GRPCRoute, TCPRoute, etc.Routing rules
nginx.ingress.kubernetes.io/* annotationsRoute filters, Policy resourcesNative API instead of annotations
TLS secret referenceGateway listener TLS configCleaner TLS configuration
Rewrite rulesHTTPRoute URLRewrite filterBuilt into the API
Rate limiting annotationsBackendTrafficPolicy (impl-specific)Varies by implementation
Custom NGINX snippetsImplementation-specific CRDsNo direct equivalent

What’s Harder in Gateway API

Let’s be honest - some things that were easy with annotations are more verbose in Gateway API:

What’s Easier in Gateway API

Converting from Ingress Nginx to Gateway API

You have two approaches: manual conversion or using a migration tool.

Manual Conversion

For smaller deployments or when you want precise control, manual conversion is straightforward. Here’s a typical ingress-nginx resource and its Gateway API equivalent:

Before (Ingress):

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: test-ingress
  namespace: my-namespace
  annotations:
    # Rewrite the name so my-host.com/direct/myservice/myPath passes
    # only /myPath to the backend service
    nginx.ingress.kubernetes.io/rewrite-target: /$2
    # Works with cert manager to provision certificates and force SSL
    cert-manager.io/cluster-issuer: my-issuer
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
spec:
  ingressClassName: "nginx"
  tls:
    - hosts:
        - my-host.com
      secretName: my-tls
  rules:
    - host: my-host.com
      http:
        paths:
          # /api/myservice(/|$)(.*) matches /api/myservice/myPath
          # /myPath is forwarded to the backend service
          - path: /api/myservice(/|$)(.*)
            pathType: Prefix
            backend:
              service:
                name: "my-service"
                port:
                  number: 8080

After (Gateway API):

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: nginx
  namespace: my-namespace
spec:
  gatewayClassName: nginx
  listeners:
    # HTTP Listener explicitly configured, as consumed by the gateway
    - hostname: my-host.com
      name: my-host-com-http
      port: 80
      protocol: HTTP
    # HTTPS Listener explicitly configured, as consumed by the gateway
    - hostname: my-host.com
      name: my-host-com-https
      port: 443
      protocol: HTTPS
      tls:
        certificateRefs:
          - group: null
            kind: null
            name: my-tls
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: test-ingress-my-host-com
  namespace: my-namespace
spec:
  hostnames:
    - my-host.com
  # ParentRef is required to associate the route with the gateway
  parentRefs:
    - name: nginx
  rules:
    - backendRefs:
        - name: my-service
          port: 8080
      matches:
        - path:
            type: PathPrefix
            value: /api/myservice(/|$)(.*)

Using ingress2gateway

For larger deployments, the ingress2gateway tool can automate much of the conversion:

# Install ingress2gateway
go install github.com/kubernetes-sigs/ingress2gateway@latest

# Convert all Ingress resources in a namespace
kubectl get ingress -n my-namespace -o yaml | ingress2gateway print

# Convert and apply directly
kubectl get ingress -n my-namespace -o yaml | ingress2gateway print | kubectl apply -f -

# Work on an external file
ingress2gateway print --input-file my-ingress.yaml --providers=nginx

Caveats with ingress2gateway:

Choosing Your Gateway API Implementation

Here’s where the real decision-making happens. You have several excellent options, each with different strengths.

Implementation Comparison

FeatureTraefikNGINX Gateway FabricEnvoy GatewayIstio Gateway
MaturityHighMediumMediumHigh
Gateway API ConformanceFullPartialFullFull
gRPC SupportYesYesYesYes
UDP RoutesYesNoYesYes
Learning CurveLowLow (if coming from NGINX)MediumHigh
Additional FeaturesMiddleware, Let’s EncryptNGINX familiarityEnvoy ecosystemFull service mesh
Resource OverheadLowLowMediumHigh

The first piece of using the Gateway API is simply installing the Gateway API CRDs.

kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yaml

This is our test application, a simple HTTP application:

Sample Target Application

Deployment & Service

# httpbin - Echo server for Gateway API testing
apiVersion: apps/v1
kind: Deployment
metadata:
  name: httpbin
  labels:
    app: httpbin
spec:
  replicas: 1
  selector:
    matchLabels:
      app: httpbin
  template:
    metadata:
      labels:
        app: httpbin
    spec:
      containers:
        - name: httpbin
          image: kong/httpbin:latest
          ports:
            - containerPort: 80
          readinessProbe:
            httpGet:
              path: /get
              port: 80
            initialDelaySeconds: 5
            periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: httpbin
  labels:
    app: httpbin
spec:
  selector:
    app: httpbin
  ports:
    - name: http
      port: 80
      targetPort: 80
  type: ClusterIP

Next, the best part. We can simply create a single Gateway resource and attach our HTTPRoute to it.

These will work (once we link them via the gatewayClassName) across all of our different examples:

Universal Gateway API Resources

We’ll be updated the Gateway resource based on our chosen implementation, but here is the basic structure as part of the traefik example.

# Traefik Gateway
# The "traefik" GatewayClass is auto-created by the Helm chart
# when providers.kubernetesGateway.enabled=true
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: gateway
spec:
  # Connects to our gateway implementation
  gatewayClassName: traefik
  listeners:
    - name: http
      port: 8000
      protocol: HTTP
      allowedRoutes:
        namespaces:
          from: Same

And our HTTPRoute:

# HTTPRoute 1: Exact path routing
# /api/get -> httpbin /get
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: httpbin-get
spec:
  parentRefs:
    - name: test-gateway
  rules:
    - matches:
        - path:
            type: Exact
            value: /api/get
      filters:
        - type: URLRewrite
          urlRewrite:
            path:
              type: ReplaceFullPath
              replaceFullPath: /get
      backendRefs:
        - name: httpbin
          port: 80
---
# HTTPRoute 2: Path prefix matching
# /api/anything/* -> httpbin /anything/*
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: httpbin-anything
spec:
  parentRefs:
    - name: test-gateway
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api/anything
      filters:
        - type: URLRewrite
          urlRewrite:
            path:
              type: ReplacePrefixMatch
              replacePrefixMatch: /anything
      backendRefs:
        - name: httpbin
          port: 80
---
# HTTPRoute 3: Header-based routing
# Requests with X-Test-Route: canary -> httpbin /headers
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: httpbin-header-canary
spec:
  parentRefs:
    - name: test-gateway
  rules:
    - matches:
        - headers:
            - name: X-Test-Route
              value: canary
          path:
            type: PathPrefix
            value: /api
      filters:
        - type: URLRewrite
          urlRewrite:
            path:
              type: ReplacePrefixMatch
              replacePrefixMatch: /headers
      backendRefs:
        - name: httpbin
          port: 80

Here’s our HTTPRoute resources in visualized by Headlamp

HTTPRoutes visualized in Headlamp

Traefik

Traefik is an excellent choice if you want a straightforward migration path with minimal operational overhead.

Pros:

Cons:

Installation:

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

Deploying our resources laid out above is as simple as:

kubectl apply -f ./mylocal/gateway.yaml
kubectl apply -f ./mylocal/httproutes.yaml

Just that like, with a basic Traefik installation and the configuration of the Gateway API components, we have a working Gateway system that can easily replace Ingress-Nginx.

Traefik Dashboard showing HTTPRoutes

The rules are implemented exactly as a native Traefik ingress resource.

NGINX Gateway Fabric

NGINX Gateway Fabric is the official NGINX-backed Gateway API implementation. If your team is comfortable with NGINX concepts, this might be the smoothest transition.

Pros:

Cons:

Installation:

helm install ngf oci://ghcr.io/nginx/charts/nginx-gateway-fabric --create-namespace -n nginx-gateway --set nginx.service.type=NodePort --set-json 'nginx.service.nodePorts=[{"port":31437,"listenerPort":80}, {"port":30478,"listenerPort":8443}]'

Now that we’ve created our Nginx Gateway Fabric deployment and we already have our resources, we’ll update our gateway to point to the gateway-class. It’ll change from “traefik” to “nginx”.

Let’s validate everything set up properly:

# This is our gateway resource, so we can use kubectl port-forward to test it
kubectl port-forward svc/test-gateway-nginx 8080:8000
 curl -s -H "Host: httpbin.example.com" http://localhost:8080/api/get
{
  "args":{},
  "headers": {
      "Accept":"*/*",
      "Host":"httpbin.example.com",
      "User-Agent":"curl/8.7.1",
      "X-Forwarded-Host":"httpbin.example.com"
  },
  "origin":"127.0.0.1",
  "url":"http://httpbin.example.com/get"
}

It worked! Just as easy as the Traefik example. We simply deployed the Gateway API resources and the Gateway Fabric controller took care of the rest.

The only important part was updating our gatewayClassName to nginx from traefik.

Envoy Gateway

Envoy Gateway brings the power of Envoy proxy with a focus on simplicity and Gateway API compliance.

Pros:

Cons:

Installation:

helm install eg oci://docker.io/envoyproxy/gateway-helm \
  --version v1.6.3 -n envoy-gateway-system \
  --create-namespace

If you’ve already created the gateway-api CRDs, you can add --skip-crds to skip the CRD installation.

Next, apply our Gateway resource using kubectl apply -f

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

We update our existing Gateway to point to “eg” instead of “nginx”:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: test-gateway
spec:
  gatewayClassName: eg
  listeners:
    - name: http
      port: 8000
      protocol: HTTP
      allowedRoutes:
        namespaces:
          from: Same

We can see, just like with Traefik, Envoy Gateway automatically set up HTTP route mappings internally for the Gateway API components.

Gateway API set up in envoy admin interface

My Recommendation

For most teams migrating from ingress-nginx:

  1. Start with Traefik if you want the easiest path forward with minimal operational overhead and like an easily deployable dashboard.
  2. Choose NGINX Gateway Fabric if your team knows NGINX well already or you’re using NGINX Plus
  3. Pick Envoy Gateway if you want strong Envoy ecosystem integration and full conformance

Step-by-Step Migration Tutorial

Let’s walk through a complete migration from ingress-nginx to Gateway API using [PLACEHOLDER: chosen implementation]. This tutorial assumes you have a working Kubernetes cluster with ingress-nginx currently deployed.

Prerequisites

Step 1: Audit Your Current Ingress Configuration

First, let’s see what we’re working with:

# List all Ingress resources
kubectl get ingress --all-namespaces

# Export all Ingress resources for analysis
kubectl get ingress --all-namespaces -o yaml > current-ingress.yaml

# Check for annotation usage
kubectl get ingress --all-namespaces -o json | \
  jq -r '.items[].metadata.annotations | keys[]' | \
  sort | uniq -c | sort -rn

Document any custom annotations, snippets, or ConfigMap customizations you’re using.

Step 2: Install Gateway API CRDs

# Install Gateway API CRDs
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yaml

# Verify CRDs are installed
kubectl get crds | grep gateway

Step 3: Deploy Your Chosen Gateway Implementation

We’ll use Traefik as an example here

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

Step 4: Create Your Gateway Resource

# Traefik Gateway
# The "traefik" GatewayClass is auto-created by the Helm chart
# when providers.kubernetesGateway.enabled=true
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: gateway
spec:
  # Connects to our gateway implementation
  gatewayClassName: traefik
  listeners:
    - name: http
      port: 8000
      protocol: HTTP
      allowedRoutes:
        namespaces:
          from: Same

Step 5: Convert Your Routes

kubectl get ingress -n my-namespace -o yaml | ingress2gateway print | kubectl apply -f -

Step 6: Test the New Configuration

# Test HTTP endpoint
curl -v http://your-gateway-ip/api/health

# Test HTTPS endpoint
curl -v https://app.example.com/api/health

# Test with specific headers
curl -v -H "X-Custom-Header: test" https://app.example.com/api/test

Step 7: Migrate Traffic

Once testing is complete, you can migrate traffic:

Option A: DNS Cutover

  1. Update DNS to point to the new Gateway’s external IP
  2. Monitor for errors
  3. Roll back DNS if issues arise

Option B: Gradual Migration with Traffic Splitting If both controllers are running, use weighted DNS or an external load balancer to gradually shift traffic.

Step 9: Decommission ingress-nginx

After successful migration:

# Remove ingress-nginx
helm uninstall ingress-nginx -n ingress-nginx

# Clean up old Ingress resources (optional, after verification)
kubectl delete ingress --all-namespaces --all

Troubleshooting Common Issues

Route Not Attaching to Gateway

Symptom: HTTPRoute exists but traffic isn’t being routed.

Check:

kubectl describe httproute my-route

Look for conditions in the status. Common issues:

TLS Certificate Not Working

Symptom: HTTPS connections fail or show wrong certificate.

Check:

404 Errors

Symptom: All requests return 404.

Check:

Gateway Stuck in “Pending”

Symptom: Gateway never gets an external IP.

Check:

Summary

Migrating from ingress-nginx to Gateway API is not optional - the retirement timeline is real and the clock is ticking. But this migration is also an opportunity to adopt a cleaner, more powerful API for managing ingress traffic.

Key takeaways:

  1. Audit your current setup before starting migration
  2. Choose an implementation that matches your team’s expertise and requirements
  3. Test thoroughly before cutting over production traffic
  4. Don’t wait - start planning and testing now

The Gateway API ecosystem is mature enough for production use. The tooling exists, the documentation is solid, and the community is active. You’ve got this.

Resources


Previous Post
Spring Boot 4 and Logbook Now Work Together
Next Post
UUID4 Shouldn't Be Your Primary Key