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.
Top comments (0)