Skip to content
Back to Tutorials

How to Migrate from Ingress NGINX to Gateway API Before Retirement

Intermediate · 1 hour · 16 min read · Byte Smith ·

Before you begin

  • Cluster-admin or equivalent access to each Kubernetes cluster you plan to migrate
  • An inventory of current Ingress resources, IngressClasses, TLS secrets, and ingress-nginx annotations
  • A test or staging environment that mirrors production routing behavior closely enough to validate edge cases
  • A rollback plan that can restore traffic to the existing ingress-nginx path quickly
  • A target Gateway API implementation or controller selected for your environment

What you'll learn

  • Audit ingress-nginx usage before converting anything
  • Choose a realistic migration target based on features instead of marketing claims
  • Install Gateway API resources and a compatible controller safely
  • Translate a working Ingress into Gateway and HTTPRoute resources
  • Run ingress-nginx and Gateway API side by side while comparing behavior
  • Handle advanced cases like rewrites, auth, header manipulation, and weighted routing
  • Cut traffic over with rollback triggers and monitoring in place
1
2
3
4
5
6
7
8
9
On this page

This tutorial is for teams using the community ingress-nginx controller and needing a practical migration path before they are stuck carrying unsupported edge infrastructure. The urgent part is not the Kubernetes Ingress API itself. The urgent part is that many clusters rely on controller-specific ingress-nginx behavior, and those behaviors do not translate cleanly just by changing one manifest.

What makes this migration tricky is that Ingress and Gateway API model the problem differently. With Ingress, a lot of advanced behavior gets packed into annotations on one object. With Gateway API, entry points, TLS, and routing are split across multiple resources and often across different personas. That is a better long-term model, but it means migration is part translation, part cleanup, and part behavior re-design.

By the end of this tutorial, you will have a repeatable migration workflow: audit what ingress-nginx is really doing today, install Gateway API safely, translate one ingress end to end, run both paths side by side, migrate advanced cases intentionally, and cut traffic over with rollback ready. For broader context on the retirement timeline and what it means for your clusters, see Ingress NGINX Retirement: What It Means and How to Migrate.

Step 1: Audit current ingress usage

Do not start by converting YAML. Start by finding out what your cluster is actually using. The most dangerous migrations are the ones where teams assume they only use host/path routing and TLS, then discover late that auth, rewrites, or controller-specific defaults were critical. Gateway API’s own migration guide warns that implementation-specific Ingress features are the part that needs the most manual work, and the Kubernetes ingress-nginx migration guidance specifically calls out hidden defaults and side effects that can cause outages if you translate too literally.

Export current ingress state

Run a cluster-level export first.

kubectl get ingress -A -o yaml > ingress-all.yaml
kubectl get ingressclass -A -o yaml > ingressclass-all.yaml
kubectl get secret -A -o yaml > secrets-all.yaml
kubectl get configmap -A -o yaml | grep -A 40 -B 10 ingress-nginx
kubectl get pods -A -l app.kubernetes.io/name=ingress-nginx -o wide

That last command is useful because Kubernetes explicitly recommends it as a quick way to determine whether you rely on ingress-nginx at all.

Inventory hosts, paths, and TLS

Create a migration inventory file that captures behavior, not just objects.

File: ingress-migration-inventory.csv

namespace,ingress_name,host,path,path_type,tls_secret,service,service_port,ingress_class,notes
payments,payments-web,payments.example.com,/,Prefix,payments-tls,payments-web,8080,nginx,standard http app
identity,oauth-gateway,login.example.com,/oauth,Prefix,identity-tls,oauth-proxy,4180,nginx,auth headers and redirects
api,customer-api,api.example.com,/v1,Prefix,api-tls,customer-api,9000,nginx,rewrite and rate limiting
marketing,landing,www.example.com,/,Prefix,public-tls,landing,80,nginx,static content and redirect rules

Inventory annotations and controller-specific behavior

Now extract annotations because that is where most migration pain lives.

kubectl get ingress -A -o jsonpath='{range .items[*]}{.metadata.namespace},{.metadata.name},{.metadata.annotations}{"\n"}{end}' > ingress-annotations.txt

Look specifically for patterns like these:

  • nginx.ingress.kubernetes.io/rewrite-target
  • nginx.ingress.kubernetes.io/use-regex
  • nginx.ingress.kubernetes.io/auth-url
  • nginx.ingress.kubernetes.io/auth-signin
  • nginx.ingress.kubernetes.io/configuration-snippet
  • nginx.ingress.kubernetes.io/server-snippet
  • nginx.ingress.kubernetes.io/limit-rps
  • nginx.ingress.kubernetes.io/canary
  • nginx.ingress.kubernetes.io/upstream-vhost

These are not all equivalent in Gateway API. Some map cleanly to HTTPRoute features. Others require implementation-specific policies or a redesign.

Find custom behavior tied to ingress-nginx

The official Gateway API migration guide notes that Ingress annotations are implementation-specific and that conversion depends on both your current controller and your chosen Gateway implementation. Kubernetes’ ingress-nginx migration guidance also highlights surprising behaviors such as regex semantics that can differ from what many teams assume.

Create a worksheet for each risky ingress.

File: risky-ingress-checklist.md

# Risky Ingress Review

## Identity / oauth-gateway
- Uses external auth
- Adds auth request headers
- Redirects unauthenticated users
- Depends on custom nginx snippets
- Needs exact redirect and header behavior

## API / customer-api
- Uses rewrite-target
- Uses rate limits
- Uses regex path matching
- Exposes public API clients that may rely on path quirks
Warning

Treat every annotation as a migration decision, not a syntax conversion. Some ingress-nginx behaviors are defaults or quirks rather than portable features.

You should now have a list of all Ingress objects, their hosts and paths, their TLS bindings, and the annotations or behaviors most likely to break during migration.

Step 2: Decide on a migration target

Do not assume Gateway API is one thing. Gateway API is a Kubernetes API plus an implementation. Your real migration target is a Gateway API implementation or another supported controller, and behavior varies across implementations. The official Gateway API docs describe the API as role-oriented, portable, expressive, and extensible, while the conformance docs and implementations list exist specifically because not every implementation supports the same features or profiles.

Why Gateway API is usually the default target

Gateway API exists to solve several Ingress limitations: limited features, overreliance on annotations, and an insufficient permission model for shared infrastructure. It also provides standard support for common features like header matching, request/response manipulation, traffic splitting, and more expressive routing.

That makes it the best target for most Kubernetes teams because it gives you:

  • explicit entry points with Gateway
  • routing logic in HTTPRoute
  • cleaner ownership boundaries between platform and app teams
  • less annotation sprawl
  • better portability across controllers, at least for core features

When another supported controller may still be the better short-term choice

If your estate depends heavily on controller-specific features that are not yet portable in Gateway API, or you already run a vendor-supported edge stack tightly integrated with your platform, another controller could still be the shortest safe path. Kubernetes’ own retirement statement explicitly says alternatives include Gateway API or one of the many third-party ingress controllers, and also warns that none are drop-in replacements.

Choose based on concrete requirements

Make a short decision matrix.

File: gateway-target-matrix.yaml

requirements:
  standard_http_routing: required
  tls_termination: required
  weighted_routing: required
  request_header_modification: required
  auth_policy: required
  rate_limiting: required
  regex_path_matching: required
  cross_namespace_backend_refs: maybe

candidates:
  gateway_api_envoy:
    core_conformance: yes
    extended_features_needed: yes
    policy_extensions_for_auth: yes
    rate_limit_extension: yes
  gateway_api_istio:
    core_conformance: yes
    extended_features_needed: yes
    policy_extensions_for_auth: yes
    rate_limit_extension: yes
  existing_vendor_ingress:
    direct_feature_match: maybe
    long_term_portability: lower
    migration_effort: maybe_lower_short_term

Prefer conformant implementations

The Gateway API project maintains a list of conformant and partially conformant implementations, and conformance exists because portability only works when implementations actually pass the same tests. Use that list instead of vendor claims.

Tip

Pick your controller by testing the exact features you use today, not by counting how many features appear on a comparison page.

You should now know whether your target is a Gateway API implementation, which one it is, and which advanced behaviors will require implementation-specific policies or redesign.

Step 3: Install Gateway API components

Gateway API is an add-on, not something every cluster has by default. The official docs describe it as an add-on family of API kinds, and the getting-started guidance says you can either install a Gateway controller that installs the CRDs for you or install the CRDs manually. The CRD management guide also recommends that cluster admins or providers manage Gateway API CRDs because they are highly privileged, cluster-scoped resources.

Install the Gateway API CRDs

Use the standard channel unless you have a specific reason to depend on experimental resources. Check the Gateway API releases page for the latest stable version.

# Replace v1.4.0 with the latest stable release
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yaml

Then verify:

kubectl get crd | grep gateway.networking.k8s.io

Install your chosen controller

Follow your implementation’s install guide. This tutorial stays vendor-neutral on purpose because the controller determines the actual data plane behavior.

After installation, verify the GatewayClass objects that the controller registered:

kubectl get gatewayclass

The Kubernetes docs note that a Gateway must reference a GatewayClass, and that GatewayClass identifies the controller responsible for managing that Gateway.

Plan namespaces and permissions

A key difference from Ingress is that entry points and routes are separated. Gateway API’s migration guide describes explicit personas: cluster operators or app admins define the entry points and TLS termination on Gateway, while application developers define routing on HTTPRoute. The API also uses a bidirectional trust model between Gateways and attached routes.

Create a namespace plan up front:

File: gateway-namespace-plan.yaml

platform_namespace: edge-system
app_namespaces:
  - payments
  - identity
  - api
  - marketing

ownership:
  gatewayclass: platform-team
  gateway: platform-team
  httproute: application-teams
  tls-secrets: platform-team

Create a baseline Gateway

File: gateway.yaml

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

Apply it:

kubectl apply -f gateway.yaml
kubectl get gateway -n edge-system

Verify the Gateway is programmed

kubectl get gateway public-gateway -n edge-system -o yaml

Look for ready or programmed status and assigned addresses.

Note

Install Gateway API first as shared platform infrastructure. Do not start by letting every app team create arbitrary Gateways with their own entry points.

You should now have Gateway API CRDs installed, a controller running, and a baseline Gateway ready for routes to attach.

Step 4: Translate one Ingress to Gateway resources

Now migrate one representative ingress. Keep the first translation boring: standard host/path routing with TLS termination and one backend. The official migration guide maps Ingress entry points to Gateway, TLS termination to Gateway listeners, and path-based routing rules to HTTPRoute. It also notes that Gateway API does not have a direct equivalent of an Ingress default backend; you must define that routing behavior explicitly.

Start from a real ingress

File: ingress.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: payments-web
  namespace: payments
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - payments.example.com
      secretName: payments-tls
  rules:
    - host: payments.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: payments-web
                port:
                  number: 8080

Create the matching HTTPRoute

File: payments-httproute.yaml

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: payments-web
  namespace: payments
spec:
  parentRefs:
    - name: public-gateway
      namespace: edge-system
      sectionName: https
  hostnames:
    - "payments.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: payments-web
          port: 8080

Apply it:

kubectl apply -f payments-httproute.yaml
kubectl get httproute -n payments

Add HTTP to HTTPS redirect explicitly

Gateway API has standard request redirect support, including TLS redirects, but you usually express that behavior on routes or listeners rather than relying on controller conventions. The migration guide lists request redirects as one of the features now available through Gateway API rather than ingress annotations.

File: payments-http-redirect.yaml

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: payments-http-redirect
  namespace: payments
spec:
  parentRefs:
    - name: public-gateway
      namespace: edge-system
      sectionName: http
  hostnames:
    - "payments.example.com"
  rules:
    - filters:
        - type: RequestRedirect
          requestRedirect:
            scheme: https
            statusCode: 301

Verify the translated route

kubectl describe httproute payments-web -n payments
kubectl describe httproute payments-http-redirect -n payments

Then send test requests to the Gateway address:

export GATEWAY_IP=$(kubectl get gateway public-gateway -n edge-system -o jsonpath='{.status.addresses[0].value}')
curl -I -H "Host: payments.example.com" http://$GATEWAY_IP/
curl -k -I -H "Host: payments.example.com" https://$GATEWAY_IP/

You should now see working routing through HTTPRoute, with explicit HTTPS redirection and the backend service receiving traffic.

Step 5: Run both side by side

Do not delete ingress-nginx after the first conversion. Gateway API’s migration guide explains the API mapping, but it explicitly says it does not prepare you for a live migration. Kubernetes’ ingress-nginx migration blog goes even further: seemingly correct translations can still fail because of ingress-nginx quirks.

Use separate entry points during validation

The easiest side-by-side model is separate load balancer addresses.

  • ingress-nginx keeps serving production traffic
  • Gateway API gets a new load balancer or address
  • test traffic goes to the new address with the same Host header
export OLD_IP=$(kubectl get svc -n ingress-nginx ingress-nginx-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
export NEW_IP=$(kubectl get gateway public-gateway -n edge-system -o jsonpath='{.status.addresses[0].value}')

curl -sS -H "Host: payments.example.com" http://$OLD_IP/health
curl -sS -H "Host: payments.example.com" http://$NEW_IP/health

Compare behavior, not just 200 responses

Build a comparison checklist:

File: validation-checklist.md

# Validation Checklist

## Functional
- Host routing matches expected service
- Path routing matches expected service
- HTTP redirects to HTTPS
- TLS certificate is correct
- Response headers match expectations
- Auth flow still works
- Backend sees expected forwarded headers

## Operational
- Logs appear in the expected controller
- Metrics scrape works
- Access logs preserve useful fields
- WAF or rate limits still behave as expected

Watch for hidden annotation dependencies

This is the part many teams underestimate. The Gateway migration docs say annotation-heavy Ingress setups are the least portable, and the ingress-nginx migration post specifically highlights non-obvious regex and matching behavior.

If any ingress used:

  • regex paths
  • rewrite-target
  • auth annotations
  • custom snippets
  • canary annotations

treat the translated route as suspicious until proven equivalent.

Warning

A passing smoke test is not enough. Compare redirects, headers, cookies, auth behavior, and path semantics before you trust the new path.

You should now have ingress-nginx and Gateway API running side by side, with test traffic proving whether the translated behavior is truly equivalent.

Step 6: Migrate advanced cases

Advanced migrations are where Gateway API becomes powerful, but also where implementation differences matter most. The official migration guide notes that some features previously expressed through Ingress annotations now map to Gateway API features such as redirects, request/response manipulation, traffic splitting, and header/query/method matching. It also makes clear that advanced features like auth or rate limiting often rely on implementation-specific extensions or policy attachments.

Rewrites

If you used nginx.ingress.kubernetes.io/rewrite-target, translate it using URLRewrite if your implementation supports it.

File: api-rewrite-route.yaml

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: customer-api
  namespace: api
spec:
  parentRefs:
    - name: public-gateway
      namespace: edge-system
      sectionName: https
  hostnames:
    - "api.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /v1
      filters:
        - type: URLRewrite
          urlRewrite:
            path:
              type: ReplacePrefixMatch
              replacePrefixMatch: /
      backendRefs:
        - name: customer-api
          port: 9000

Header manipulation

Gateway API standardizes request and response header modification through filters.

File: header-route.yaml

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: oauth-gateway
  namespace: identity
spec:
  parentRefs:
    - name: public-gateway
      namespace: edge-system
      sectionName: https
  hostnames:
    - "login.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /oauth
      filters:
        - type: RequestHeaderModifier
          requestHeaderModifier:
            add:
              - name: X-Forwarded-Proto
                value: https
              - name: X-Auth-Source
                value: gateway
      backendRefs:
        - name: oauth-proxy
          port: 4180

Canary or weighted routing

Gateway API supports weighted routing through multiple backend refs.

File: canary-route.yaml

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: payments-canary
  namespace: payments
spec:
  parentRefs:
    - name: public-gateway
      namespace: edge-system
      sectionName: https
  hostnames:
    - "payments.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: payments-web
          port: 8080
          weight: 90
        - name: payments-web-canary
          port: 8080
          weight: 10

Auth and rate limiting

This is where you should expect implementation-specific policy resources or companion projects. Gateway API’s policy attachment model exists for this kind of extension, and the docs explicitly describe policy attachment as the preferred extension model for custom capabilities.

Do not assume your old ingress annotations have one-line equivalents. For auth and rate limiting, document the new mechanism explicitly.

File: edge-policy-notes.md

# Edge Policy Notes

## Auth
- Previous model: ingress-nginx auth-url and auth-signin annotations
- New model: implementation-specific AuthPolicy attached to Gateway or HTTPRoute
- Validation required: redirects, headers, cookie behavior, unauthenticated responses

## Rate limiting
- Previous model: ingress-nginx limit-rps annotations
- New model: implementation-specific RateLimitPolicy
- Validation required: burst behavior, headers, status code, client IP handling

Cross-namespace concerns

Gateway API uses explicit parent and reference relationships, and cross-namespace references may require ReferenceGrant depending on what is being referenced. Keep routes and backends in the same namespace when possible during the first migration wave. If you need cross-namespace designs, add them intentionally after the basics are stable.

Note

Standard Gateway API features carry well across implementations. Auth, rate limits, and some advanced policy behavior usually do not.

You should now have patterns for rewrites, header manipulation, weighted routing, and a plan for auth and rate limiting without pretending they are fully portable.

Step 7: Cut over safely

By this stage, you should have proven that the new path behaves correctly. The cutover is now an operational change, not a YAML exercise.

Choose a traffic shift plan

Common safe options are:

  • DNS cutover to the new load balancer
  • weighted DNS if your DNS platform supports it
  • upstream traffic manager shift
  • header-based or canary exposure for internal testers first

Document the plan:

File: cutover-plan.yaml

service: payments.example.com
current_entrypoint: ingress-nginx
target_entrypoint: gateway-api
cutover_method: dns
stages:
  - stage: internal-validation
    percent: 0
  - stage: pilot-traffic
    percent: 10
  - stage: partial-cutover
    percent: 50
  - stage: full-cutover
    percent: 100

rollback:
  trigger_window_minutes: 30
  action: restore_old_dns_target

Monitor the right signals

Create a cutover checklist.

File: cutover-monitoring.md

# Cutover Monitoring

## Gateway metrics
- 4xx rate
- 5xx rate
- latency p50/p95/p99
- listener and route status

## App metrics
- upstream 4xx/5xx
- auth failures
- login or checkout success rates
- backend saturation

## UX checks
- login redirects
- session persistence
- API client compatibility
- certificate and SNI behavior

Set rollback triggers before you start

A rollback plan should be numeric, not emotional.

Examples:

  • 5xx rate doubles and stays elevated for 5 minutes
  • login success drops more than 3%
  • canary backend error rate exceeds threshold
  • auth redirects fail or loop
  • TLS handshake failures spike unexpectedly

Delete old ingress only after observation

After full cutover, leave the old ingress objects and controller path untouched for one observation window. Only delete them after you are confident that traffic, auth, TLS, and application-level behavior are stable.

Warning

Do not delete ingress-nginx objects during the same window in which you first move user traffic. Cut over first, observe, then clean up.

You should now have a safe cutover model with staged traffic movement, monitoring, and explicit rollback triggers.

Common Setup Problems

No drop-in replacement

Symptom: The team expects a manifest translation to preserve behavior automatically.

Cause: Kubernetes explicitly warns there is no direct drop-in replacement for ingress-nginx.

Fix: Treat migration as a behavior review. Inventory annotations, choose a controller deliberately, and validate behavior side by side.

Annotation behavior changes

Symptom: Routes compile, but path matching or rewrites behave differently.

Cause: Ingress annotations were implementation-specific, and ingress-nginx had quirks that may not exist in your Gateway implementation.

Fix: Translate each risky annotation intentionally. Test regex, rewrites, redirects, and auth separately.

TLS differences

Symptom: TLS terminates, but certificate selection or HTTPS behavior differs from the old path.

Cause: In Gateway API, TLS is configured on Gateway listeners, and downstream/upstream TLS are modeled independently.

Fix: Validate listener hostnames, certificate refs, redirects, and any backend TLS behavior explicitly.

Controller-specific features

Symptom: Auth, rate limiting, or WAF features have no obvious Gateway YAML equivalent.

Cause: Many of these are implementation-specific extensions or policy attachments rather than portable core features.

Fix: Use your implementation’s policy model. Keep those policies separate from the base Gateway and HTTPRoute resources so the boundary is obvious.

Wrap-Up

The safe way to migrate from ingress-nginx to Gateway API is not “convert everything and hope.” It is: audit current behavior, choose a target controller based on the features you really use, install Gateway API as shared platform infrastructure, translate one ingress cleanly, validate both paths side by side, then cut over with rollback ready.

If you are migrating many clusters, standardize the process in waves. Start with low-risk standard routes, then move teams that only need host/path/TLS, then tackle annotation-heavy services, and leave the most customized edge cases for last. After the move, standardize three things across clusters: a default Gateway pattern, a route ownership model, and a documented policy model for auth, TLS, and rate limiting.

This migration is not just about replacing one controller. It is a chance to reduce annotation debt, make routing ownership clearer, and move your edge configuration toward a more portable Kubernetes-native model.