Cloud ComputingMicroservices

From Ingress to Gateway API for Kubernetes Microservices

Ingress worked fine until it didn’t. Add one more hostname, pile on another controller-specific annotation, let one team add a path rule in good faith, and suddenly your shared Kubernetes edge reads like a contract drafted by six different lawyers. For production-grade traffic management in Kubernetes microservices, that’s usually the moment Gateway API stops feeling optional.

Gateway API v1.0, announced as generally available in October 2023 by Kubernetes SIG-Network, matters because it replaces the flat, underspecified Ingress model with a cleaner contract. Platform teams own the entry points. Application teams own routing intent. Conformant implementations — controllers like Envoy Gateway and NGINX Gateway Fabric, plus the managed Gateway controllers on GKE and AKS — get a stable schema and conformance targets instead of an annotation free-for-all.

My view is simple: if you’re building a shared microservices platform today, sticking with annotation-heavy Ingress as the default is the wrong default. Ingress still has a place in small clusters and single-team setups. Once you have multiple namespaces, shared endpoints, compliance pressure, and a few dozen services, Gateway API is the model that actually fits the way the platform is run.

Why Ingress breaks down in real Kubernetes microservices

Picture a regulated platform running in Frankfurt and Paris. Mobile APIs, partner endpoints, internal admin UIs, maybe a shared identity service in another namespace. Classic Ingress can expose all of that, but it doesn’t give you a first-class ownership model. You get host, path, backend, and then a long tail of annotations whose behavior depends on the controller.

That’s where the trouble starts. Two teams define overlapping hosts. TLS settings drift between environments. A migration from one ingress controller to another turns into archaeology because critical behavior lives in annotations rather than portable API fields. You can make it work, sure, but people burn a lot of time diffing YAML and controller docs when they should be shipping safer routing rules.

The original Ingress API was intentionally small. For modern platforms, it’s too small. It has no strong native model for role separation, no clean cross-namespace delegation story, and no consistent path for richer traffic policy across implementations. That’s why Gateway API exists. Not as prettier Ingress, but as a better boundary between platform operations and application delivery.

The mental model shift

Gateway API is a set of CRDs in the gateway.networking.k8s.io group, but they don’t all sit at the same maturity. The GA core is v1: GatewayClass defines the type of data plane, Gateway defines the actual listener surface (addresses, ports, protocols, TLS), and HTTPRoute defines how HTTP traffic is matched and forwarded. GRPCRoute graduated to v1 in a later release; TCPRoute, TLSRoute, and UDPRoute are still experimental at v1alpha2; and helpers like ReferenceGrant ship at v1beta1. Pin the version each resource actually uses instead of assuming a uniform v1 surface.

That role split is the feature. Cluster operators manage infrastructure-facing resources. Product teams manage route resources in their namespaces. The API was designed this way by Kubernetes SIG-Network and industry contributors for shared, multi-tenant platforms, and it shows.

Gateway API v1.0 in practice: the minimal production shape

Let’s use a realistic pattern. A platform namespace owns a shared external Gateway. The payments team and orders team attach their own HTTPRoute objects from their namespaces. TLS terminates at the edge with a Secret managed by cert-manager. The controller could be Envoy Gateway, or a managed implementation on GKE or AKS. The point is that the contract stays stable.

apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: eg
spec:
  controllerName: gateway.envoyproxy.io/gatewayclass-controller
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: shared-edge
  namespace: platform
spec:
  gatewayClassName: eg
  listeners:
    - name: https
      protocol: HTTPS
      port: 443
      hostname: api.example.com
      tls:
        mode: Terminate
        certificateRefs:
          - kind: Secret
            name: api-example-com-tls
      allowedRoutes:
        namespaces:
          from: Selector
          selector:
            matchLabels:
              shared-gateway-access: "true"

Now the application routes. Each team owns only the routing logic relevant to its service. No team edits the public listener, IP binding, or TLS certificate unless the platform team explicitly delegates that responsibility.

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: payments-api
  namespace: payments
spec:
  parentRefs:
    - name: shared-edge
      namespace: platform
  hostnames:
    - api.example.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /payments
      backendRefs:
        - name: payments-api
          port: 8080
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: orders-api
  namespace: orders
spec:
  parentRefs:
    - name: shared-edge
      namespace: platform
  hostnames:
    - api.example.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /orders
      backendRefs:
        - name: orders-api
          port: 8080

The request flow is clean. Client hits api.example.com:443. The Gateway listener terminates TLS. The implementation reconciles attached routes. Path /payments goes to the payments-api Service. Path /orders goes to orders-api. One edge, separate ownership, explicit attachment rules.

This is where Gateway API feels better than Ingress. You can explain it to an app team in two minutes, and you can audit it without reverse-engineering annotation semantics.

Status and debugging are better

When a route doesn’t attach, you don’t want folklore. You want conditions. Gateway and Route resources publish structured status that tells you whether a route was accepted, whether listener binding succeeded, and whether references are valid. For incident response, that’s a material improvement over the vague failure modes many teams have learned to tolerate with Ingress.

A practical workflow is boring in the best way: kubectl describe gateway, kubectl describe httproute, then proxy logs and metrics from Envoy or the managed load balancer. If there’s a hostname collision or a forbidden cross-namespace reference, the resource status usually tells you before your users do.

Production-grade Gateway API patterns: TLS, tenancy, policy

TLS is where many platforms lose their composure. Gateway API gives you a more disciplined place to hold the line. In shared clusters, I’d rather centralize external certificate ownership and TLS policy at the Gateway unless there’s a strong product boundary that justifies per-team edge ownership. Certificate sprawl is expensive, operationally and mentally.

The common design is edge termination at the Gateway, certificate material in a Kubernetes Secret, and cert-manager handling issuance and rotation. In EU financial or healthcare contexts, teams often require TLS 1.2+ and approved cipher policy. The exact knobs depend on the implementation, because policy attachment for advanced TLS settings may be implementation-specific, but the control plane shape is cleaner: listener owns edge TLS, routes own matching and forwarding.

Cross-namespace delegation with ReferenceGrant

Abstract metal and glass ribbons resolving from clutter into ordered layers

Gateway API turns flat, annotation-heavy sprawl into explicit layered ownership.

Large platforms rarely keep every dependency in one namespace. Shared auth, logging, and compliance services often live centrally. Gateway API’s ReferenceGrant makes those relationships explicit. Instead of allowing arbitrary cross-namespace references, the target namespace grants permission to specific source namespaces and kinds.

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

Plain English: the shared-services namespace allows an HTTPRoute from payments to reference a Service in shared-services. That’s a much better security posture than letting any route point anywhere. It’s also easier to explain to auditors, which matters when your platform spans Frankfurt and Dublin with different residency and control requirements.

Policy attachment is where the model starts to shine

Gateway API encourages policy as a separate layer rather than hiding behavior inside route definitions. The exact CRDs differ by implementation — Envoy Gateway exposes its own extension policies, cloud vendors have theirs — but the pattern is consistent. Attach rate limits, auth filters, retries, observability settings, timeout controls, and header policies at the Gateway, route, or rule level.

That separation matters. Platform teams can define organization-wide defaults once, then let service teams override only where policy allows. You don’t need every microservice team hand-authoring timeout values or external auth filters. Frankly, that’s how retry storms and inconsistent auth edges happen, and I’d avoid that in production.

For zero-trust designs, the layering is attractive: strong TLS and external auth at the Gateway, then mTLS and service authorization inside Istio, Linkerd, or Kuma for east-west traffic. Gateway API handles north-south well. A mesh still owns service-to-service identity and policy. Trying to make one replace the other is usually a category error.

Migrating from Ingress to Gateway API without drama

The safest migration is incremental. Don’t rip out Ingress on Friday and congratulate yourself on architectural purity. Stand up Gateway API alongside the current ingress path, ideally using the same operational domain where your controller supports both, then move traffic in slices.

A sensible sequence has six phases. First, install the Gateway API CRDs and a conformant implementation, then create a non-critical Gateway in test or staging. Second, translate a small number of low-risk Ingress rules into HTTPRoute. Third, validate status conditions and listener attachment before any user traffic moves. Fourth, use header-based routing, canaries, or mirrored traffic where the implementation supports it to validate behavior. Fifth, cut over host by host or path by path. Sixth, retire the old Ingress surface once it’s empty enough to remove without surprises. It’s fine for a prototype, not for a fleet, to skip the validation step.

What actually gets translated

Basic host and path rules map cleanly. This old NGINX Ingress is typical:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: payments
  namespace: payments
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/proxy-read-timeout: "30"
spec:
  tls:
    - hosts:
        - api.example.com
      secretName: api-example-com-tls
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /payments
            pathType: Prefix
            backend:
              service:
                name: payments-api
                port:
                  number: 8080

The host, path, and backend move naturally into HTTPRoute. TLS usually moves up to the Gateway. The annotations are the hard part. Some map to filters or implementation-specific policy resources. Some don’t have a perfect 1:1 equivalent. Rewrite behavior, timeout controls, header manipulation, custom auth hooks — these need design decisions, not blind translation.

This is where teams get stuck, because their Ingress YAML is often carrying years of controller-specific sediment. Treat migration as a cleanup exercise, not just a syntax conversion. If a timeout was added during an incident in 2021 and nobody remembers why, don’t cargo-cult it into the new model.

GitOps and validation

Gateway API fits GitOps naturally. Keep platform-owned resources such as GatewayClass, shared Gateway objects, and cluster-wide policies in one repository or top-level directory. Keep application-owned HTTPRoute resources with the service manifests or in team-scoped config repos. Argo CD and Flux both work well here because ownership boundaries are visible in the resource model itself.

Validation should be stricter than “kubectl apply succeeded.” Run schema checks, custom policy checks, pre-production conformance testing for the implementation you picked, and at least one rollback rehearsal. The Gateway API project’s conformance profiles are one of the most underrated parts of the story. They reduce the chance that “works on one controller” becomes your next migration tax.

Why Gateway API is winning, and where Ingress still fits

Gateway API is winning because it matches how real platforms are run. Shared clusters. Separate teams. Different trust zones. Multiple implementations. Need for auditability. Need for portability. Ingress never really solved those concerns; controllers papered over them with annotations and custom behavior.

Kubernetes SIG-Network made the right call by treating this as a role-oriented API rather than a bigger Ingress. Gateway API v1.0 brought stable core resources, stronger conformance guarantees, and a credible path across implementations like Envoy Gateway and the managed Gateway controllers on GKE and AKS. For serious Kubernetes microservices, that’s enough to move it from “interesting” to “default candidate.”

Ingress still fits in small, single-team clusters where one controller, one hostname set, and a thin feature surface are enough. If that’s your world, keep it simple. Don’t manufacture complexity for sport. If you’re already living with overlapping hosts, inconsistent TLS policy, shared edge ownership, or controller lock-in, waiting longer usually just increases the cleanup bill.

Minimal corridor of aligned archways suggesting orderly paths and governance

The end state is not complexity, but consistent, auditable traffic management at scale.

The concrete takeaway is blunt: create one non-critical Gateway API path in a lower environment this week, wire it into your existing GitOps flow, and inspect the operational difference. Check status conditions. Model a cross-namespace reference. Move TLS ownership to the Gateway. Then answer the only question that matters: which annotation in your current Ingress setup are you still afraid to touch?