Pavan Rangani

HomeBlogMigrating from Ingress to the Kubernetes Gateway API

Migrating from Ingress to the Kubernetes Gateway API

By Pavan Rangani · July 17, 2026 · DevOps & Cloud

Migrating from Ingress to the Kubernetes Gateway API

Why the Kubernetes Gateway API Exists at All

Ingress solved one problem well in 2016 and then stopped evolving. Its spec covers hostnames, paths, and TLS, and absolutely nothing else — no header matching, no traffic splitting, no method routing, no timeouts. Every vendor filled those gaps with annotations, which is how a supposedly portable resource turned into a NGINX-specific config file wearing a Kubernetes costume.

The Kubernetes Gateway API is the SIG-Network answer to that mess. It is not Ingress v2 with more fields bolted on; it is a different shape entirely, built around the observation that three different people with three different jobs were all editing the same YAML and stepping on each other.

Three Resources, Three Job Titles

The core insight is role separation, and it maps onto how platform teams actually work. A GatewayClass is cluster-scoped and belongs to the infrastructure provider — it says “this is what an Envoy-backed gateway means here”. A Gateway belongs to the platform or cluster operator and declares actual listeners: ports, protocols, TLS certificates, and crucially which routes are allowed to attach.

An HTTPRoute belongs to the application developer, lives in their own namespace, and describes only how their service’s traffic should be matched and forwarded. So the developer never needs edit access to the shared load balancer, and the platform team never becomes a ticket queue for “please add my path”. That boundary is the whole point, and it is why the migration is worth doing even if you never use a single new routing feature.

Network of connected light points representing Kubernetes Gateway API routing
Gateway API splits one shared Ingress object into three resources owned by three different roles.

What a Gateway and an HTTPRoute Look Like

The Gateway is written once by whoever owns the cluster edge. Note allowedRoutes — that is the platform team granting permission, declaratively, instead of reviewing pull requests.

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

The HTTPRoute then lives with the application. It attaches to the Gateway by reference, and everything in it is spec — not an annotation.

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: checkout
  namespace: payments
spec:
  parentRefs:
    - name: prod-edge
      namespace: gateway-system
  hostnames:
    - "shop.example.com"
  rules:
    # Send internal beta users to the new build
    - matches:
        - path:
            type: PathPrefix
            value: /checkout
          headers:
            - name: x-beta-cohort
              value: "internal"
      backendRefs:
        - name: checkout-v2
          port: 8080

    # Everyone else: 90/10 canary
    - matches:
        - path:
            type: PathPrefix
            value: /checkout
      timeouts:
        request: 3s
      backendRefs:
        - name: checkout-v1
          port: 8080
          weight: 90
        - name: checkout-v2
          port: 8080
          weight: 10

Header matching and weighted splitting are the two features people leave Ingress for. Both were vendor annotations before; both are now typed fields that any conformant implementation must honour. That means kubectl explain works, your admission policies can validate them, and swapping implementations does not mean rewriting your routing.

Cross-Namespace References Are Explicit

Gateway API refuses to let a route in one namespace point at a Service in another just because it feels like it. The receiving namespace must publish a ReferenceGrant saying it consents. This is the fix for a real Ingress weakness, where a careless route could quietly borrow someone else’s backend.

apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
  name: allow-payments-routes
  namespace: legacy-billing
spec:
  from:
    - group: gateway.networking.k8s.io
      kind: HTTPRoute
      namespace: payments
  to:
    - group: ""
      kind: Service
      name: billing-adapter

It is more YAML, and it is worth it. Consent flows from the namespace that owns the resource, which is the same principle behind the policies in our Kubernetes network policies zero trust guide — nothing is reachable because it happens to be adjacent.

Rows of servers in a data centre running Kubernetes Gateway API workloads
ReferenceGrant makes cross-namespace routing an opt-in from the owning namespace.

Migrating Without a Cutover Weekend

You do not have to choose. Ingress and Gateway API controllers coexist happily because they are different resources reconciled by different controllers, so the sane migration is incremental and reversible at every step.

Start by installing the Gateway API CRDs and a controller, then stand up a Gateway on a separate load balancer IP. Nothing in production is touched yet. Next, translate one low-risk Ingress into an HTTPRoute and test it against the new IP directly, bypassing DNS entirely — this is where you find out which annotations you were silently depending on. The ingress2gateway tool will do a mechanical first pass of that translation, but treat its output as a draft, because it cannot know what your vendor annotations meant.

Then move traffic with DNS, not with a redeploy. Point a weighted record at the new gateway, start at a few percent, and watch your error rates. If it goes wrong, the old Ingress is still running and reverting is a DNS change rather than a rollback. Only after everything has been migrated and has sat quiet for a while do you delete the Ingress objects. If your services already publish their routing declaratively through GitOps, this staged approach slots into the pattern described in our ArgoCD ApplicationSet multi-cluster GitOps guide.

One warning about running both at once: watch what your DNS and certificates are doing. Two load balancers mean two IPs, and if the same hostname is served by both, your certificate must be valid on both or a fraction of users will hit a TLS error that never appears in your own testing. Issue the certificate for the new Gateway before you shift a single percent of traffic, and verify it by resolving directly against the new IP rather than trusting DNS to send you there.

Keep an eye on the status block of your route objects while you migrate, because it is more informative than people expect. An HTTPRoute reports back whether its parentRef accepted it and why not, so a route that silently serves nothing usually has its explanation sitting right there in kubectl describe httproute — most often a namespace label that does not match the Gateway’s allowedRoutes selector.

Circuit board close-up representing Kubernetes Gateway API infrastructure
Run both controllers side by side and shift traffic with weighted DNS — reverting stays a DNS change.

Where It Is Still Rough

Be honest with yourself about the maturity gradient. HTTPRoute and the core Gateway resources are stable and well covered by the conformance suite, but the ecosystem thins out fast at the edges — GRPCRoute, TCPRoute, and the policy attachment mechanism for things like rate limiting are at very different levels of support depending on which controller you pick. Check the Gateway API conformance reports for your specific implementation rather than assuming the spec means your controller does it.

There is also an honest case for not migrating at all. If you run three Ingress rules, none of them use annotations beyond TLS, and one team owns everything, Gateway API buys you complexity and hands back nothing. Its value is proportional to how many teams share your cluster edge. And if you already run a service mesh, understand where the overlap sits before you add a third layer — our API gateway versus service mesh decision guide works through that boundary.

Ingress is not being deleted, but it is finished — it will not gain features, and every gap it has will stay filled by annotations that lock you to a vendor. The Kubernetes Gateway API replaces that with typed fields, a conformance suite that makes portability real, and a role split that stops your platform team from being a routing helpdesk. Migrate the way it is designed to be migrated: parallel gateway, one route at a time, DNS-weighted cutover, delete the old objects last. The features are nice, but the ownership boundary is the reason to do it.

← Back to all articles