DEV Community

Oleksandr Kuryzhev
Oleksandr Kuryzhev

Posted on Originally published at kuryzhev.cloud

Kubernetes Ingress Manifest or Gateway API: Which to Ship

Originally published on kuryzhev.cloud


A team has a working Deployment and a ClusterIP Service, and now needs a public hostname with TLS. The familiar route is a Kubernetes Ingress manifest, but the ecosystem has been moving toward Gateway API, and the most widely copied Ingress controller has been announced for retirement. The Deployment and Service stay almost identical either way. What changes is the object that exposes them, and that choice is worth making deliberately.

When this choice matters

For a single app on a single cluster with one hostname, both approaches work, and the difference is mostly about future maintenance. The choice starts to matter when several teams share a cluster, when you need weighted traffic splits or header-based routing, or when your current controller is going out of support.

The ingress-nginx project announced its retirement, with best-effort maintenance planned to end in March 2026. Verify the current status in the project repository before building on it. A manifest full of controller-specific annotations is the part that hurts most during a migration, because those annotations are not portable.

Whichever you pick, the first two objects are the same. Here is the shared base: a Deployment and a Service using stable APIs.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web              # must match the pod template labels exactly
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: registry.example.com/web:1.4.2   # pin a tag or digest, not :latest
          ports:
            - name: http
              containerPort: 8080
          readinessProbe:    # gates Service endpoints; without it traffic hits cold pods
            httpGet:
              path: /healthz
              port: http
          resources:
            requests: { cpu: 100m, memory: 128Mi }
            limits: { memory: 256Mi }
---
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  type: ClusterIP
  selector:
    app: web                # a typo here yields a Service with zero endpoints
  ports:
    - name: http
      port: 80
      targetPort: http      # refers to the named container port above

Watch out for: a Service selector that does not match the pod labels fails silently. The Service exists, DNS resolves, and every request errors. Verify with kubectl get endpointslices -l kubernetes.io/service-name=web.

Option A: A classic Kubernetes Ingress manifest

Ingress (networking.k8s.io/v1) is the established path. One object maps a host and path to a Service, and a controller you install separately implements it. The documented behavior is that the API is stable but frozen; the Kubernetes docs point new feature work to Gateway API.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web
spec:
  ingressClassName: public      # replaces the old kubernetes.io/ingress.class annotation
  tls:
    - hosts: [app.example.com]
      secretName: app-example-tls   # kubernetes.io/tls Secret in the same namespace
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix    # required field in v1
            backend:
              service:
                name: web
                port:
                  name: http

Pros:

  • Small and widely understood. Most tutorials, Helm charts, and cert-manager examples assume it.
  • One object per app is easy to review in a pull request.
  • Mature support across managed cloud load balancer controllers and self-hosted proxies.

Cons:

  • The spec covers only host and path routing. Everything else, such as rewrites, timeouts, and canaries, lives in controller-specific annotations that do not carry over to another controller.
  • Roles are blurred. The app team that writes the Ingress also effectively touches shared edge behavior.
  • No new features are planned for the API itself.

Watch out for: an Ingress with a missing or wrong ingressClassName is simply ignored by every controller. It looks valid, kubectl apply succeeds, and nothing is routed.

Option B: Gateway API with HTTPRoute

Gateway API (gateway.networking.k8s.io/v1) splits the problem into objects owned by different roles. A platform team defines a GatewayClass and a Gateway (listeners, ports, TLS); app teams attach HTTPRoutes to it. The Deployment and Service above do not change.

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: web
spec:
  parentRefs:
    - name: shared-gateway       # Gateway owned by the platform team; its allowedRoutes must permit this namespace
      namespace: infra
  hostnames: [app.example.com]
  rules:
    - matches:
        - path: { type: PathPrefix, value: / }
      backendRefs:
        - name: web
          port: 80
          weight: 100            # add a second backendRef to split traffic natively

Pros:

  • Header matching, redirects, request header modification, and weighted backends are part of the standard API, not annotations.
  • Role separation: the Gateway can restrict which namespaces may attach routes.
  • Portable across conformant implementations, though implementation-specific extensions still exist.

Cons:

  • The CRDs are not built into Kubernetes. You install them, pick a channel (standard or experimental), and must keep versions compatible with your controller.
  • More objects to understand, and cross-namespace references need explicit grants such as ReferenceGrant.
  • Implementation coverage varies. Check your controller's conformance report for the features you need.

Watch out for: an HTTPRoute can be created successfully yet not be attached to the Gateway, for example when the Gateway's allowedRoutes excludes your namespace or a backend reference does not resolve. Always read status.parents[].conditions on the HTTPRoute for Accepted and ResolvedRefs instead of assuming apply means working.

Decision matrix

The table compares the two approaches on the factors that usually decide the question. It describes classes of approach, not specific products.

Factor Ingress Gateway API
One app, one host, simple TLS Fits well Works, more setup
Multiple teams on shared edge Weak isolation Designed for it
Canary or header routing Annotations, non-portable Standard fields
Cluster add-ons required Controller only Controller plus CRDs
Lock-in on migration High if annotation-heavy Lower, extensions aside
Long-term API direction Frozen Actively developed

A quick checklist to run against your own cluster before deciding:

# 1. What implements edge traffic today? (gatewayclass errors if the CRDs are absent)
kubectl get ingressclass,gatewayclass

# 2. How many lines of controller-specific annotations are you carrying?
kubectl get ingress -A -o yaml | grep -c 'nginx.ingress.kubernetes.io'

# 3. Are Gateway API CRDs installed, and which version?
kubectl get crd gateways.gateway.networking.k8s.io \
  -o jsonpath='{.metadata.annotations.gateway\.networking\.k8s\.io/bundle-version}'

A high count in step 2 is a rough indicator of how much annotation-specific work a migration will involve, since each one needs an equivalent or a deliberate replacement.

Evidence-based recommendation

For new clusters, start with Gateway API if your chosen controller documents conformance for the features you need. The API is where development is happening, and it keeps routing logic out of annotations. Platform teams serving several app teams get the most benefit, because the Gateway/HTTPRoute split matches how responsibility already divides.

Keep a plain Kubernetes Ingress manifest when the cluster is small, the routing is host and path only, and your controller's Gateway API support is immature or unverified. It remains valid and supported as an API. Just keep annotations to a minimum and write down each one, so a later move is a mechanical task. The Gateway API project provides an ingress2gateway tool for converting existing resources; verify its coverage of your controller and annotations before relying on it.

If you already run an Ingress controller that is retiring or unmaintained, treat that as the deciding factor. Plan the move to a maintained controller, and use the moment to adopt Gateway API instead of swapping one annotation dialect for another. Migrate one low-risk hostname first, compare behavior, then shift the rest.

Whatever you choose, keep the Deployment and Service boring: pinned image, readiness probe, named ports, matching selectors. Many "my Ingress is broken" problems trace back to those, not the edge object. For the authoritative field reference, see the Kubernetes Ingress documentation and the Gateway API overview. More practical Kubernetes and DevOps notes are on kuryzhev.cloud.

Related

Top comments (0)