0%
Containers and Kubernetes: From cgroups to Operators
What a Container Actually Is
Working with Docker
Kubernetes Architecture
Workloads
Scheduling and Resources
Extending Kubernetes
Ingress and the Gateway API
An ingress object is a routing table, not a router. Something must read it and configure a proxy, and almost every surprising behaviour at the edge comes from the gap between what the object says and what the controller does with it.
The object and the controller are separate things
One separation explains most portability problems.
The ingress resource declares host and path rules pointing at services. A controller watches those resources and configures a proxy accordingly, and the proxy is what terminates connections and forwards requests. The resource specifies intent; the controller decides semantics.
Because the specification leaves a great deal unstated, controllers differ in path matching details, header handling, timeout defaults and much else. Two clusters with identical ingress objects and different controllers behave differently, which is why ingress configuration is not portable in practice even though the object is.
The class selects the controller
One field routes an object to a controller and has a history worth knowing.
A class resource names a controller, and an ingress object references a class. A controller only acts on objects naming its own class, which is what allows several controllers in one cluster: an internal one and an external one, or one per tenant.
The historical form was an annotation, which still works in many controllers and is the cause of a specific failure: an object with neither a class nor the annotation is picked up by whichever controller is configured as the default, or by none, so it either goes to the wrong proxy or is silently ignored. A missing class is the first thing to check when an ingress object appears to do nothing.
Path types and their matching rules
Three path types exist and the difference between two of them matters constantly.
An exact path matches the request path precisely. A prefix path matches on whole path segments, so a prefix of a slash followed by a word matches that segment and anything below it, but not a longer word sharing the same characters. An implementation-specific type hands the string to the proxy to interpret, which usually means a regular expression.
The segment rule is the one people get wrong: a prefix intended to match a path and everything under it does so correctly, while a prefix intended as a string prefix does not, because a trailing partial segment is not a match. Reaching for the implementation-specific type to get regular expressions is where portability is usually lost.
Rule precedence when several match
One ordering decides the winner and it is not file order.
Matching is by longest matching path within the matching host, with exact matches preferred over prefix matches of the same length. Host matching prefers an exact host over a wildcard host, and a wildcard matches only one label, so a wildcard host does not match a name with an extra label in it.
Where several objects in different namespaces declare overlapping rules, behaviour is controller-specific and frequently resolves by creation time, which is a genuinely bad property: a new object can take over a path from an existing one. Avoiding overlap by convention is the only reliable defence.
The default backend
One fallback catches unmatched traffic and is worth setting deliberately.
An object may declare a backend used when no rule matches, and the controller usually also has a cluster-wide default. Without either, an unmatched request gets whatever the proxy's own default is, typically a bare error page from the proxy itself.
That matters because the proxy's default response carries no application context, no request identifier and often a proxy-identifying header, which is both unhelpful and mildly informative to anyone probing the edge. A deliberate default backend returning a controlled response is a small change that improves both debugging and discretion.
Backend ports and readiness
One indirection decides which pods receive traffic.
A rule names a service and a port, so requests go to that service, which means through the service's endpoint slices and therefore only to ready pods. That gives the ingress path readiness awareness for free.
Many controllers bypass the service address and send directly to endpoint addresses, which they watch themselves. That is faster, avoids the kernel translation layer, and allows per-request balancing across endpoints rather than per-connection. It also means the controller's view of readiness is its own, so a controller with a stale endpoint view keeps sending to removed pods even though the service machinery has moved on.
Annotations as the real configuration surface
One mechanism carries everything the specification omits.
Timeouts, body size limits, buffering, redirects, rewrite rules, rate limits, authentication, cross-origin policy, session affinity, header manipulation and load balancing algorithm are all configured by annotations, whose names and semantics are specific to the controller.
The result is that a production ingress object is mostly annotations, which means the configuration is both unvalidated and untyped: a misspelled annotation is silently ignored, there is no schema to check it against, and a change in controller version can change its meaning. This is the single largest practical problem with ingress and the main motivation for its successor.
Rewrites and the trap in them
One common requirement has a persistent pitfall.
Serving an application under a path prefix usually requires stripping that prefix before forwarding, which is a rewrite. The application then generates absolute links and redirects without the prefix, so the browser follows them to a path the edge does not route.
The robust answers are to make the application prefix-aware through a base path setting, or to give it its own host rather than a path prefix. Rewriting at the edge and hoping the application never emits an absolute path works until it does, usually in a redirect after a form submission, and the failure is confusing because the edge configuration is correct.
How a request traverses the edge
One walkthrough makes the layers explicit.
A client resolves a public name to an external balancer. The balancer forwards to a node port or directly to the controller's pods. The proxy terminates the connection, reads the host and path, selects a backend by the matching rules, opens or reuses a connection to a pod, forwards the request, reads the response and returns it. Along the way it may add headers identifying the original client and protocol.
Each hop is a place where a timeout, a buffer limit or a header policy applies, which is why edge debugging means knowing which component produced a given error rather than only that an error occurred.
Identifying who produced an error
One discipline saves hours during an incident.
A proxy-generated error looks different from an application error: the status may be a gateway timeout or bad gateway, the body is the proxy's own, and the access log on the controller records an upstream status of zero or a connection failure. An application error has an upstream status and a response the application produced.
So the first question in any edge incident is whether the controller's access log shows the request reaching a backend at all. If it does not, the problem is in matching, backend selection or connectivity; if it does, the problem is the application or the timeout between them.
What ingress is good at
One fair assessment closes the section.
For the common case, a handful of hosts and paths routing to services with TLS, it is concise, widely supported and well understood. A small ingress object does a lot of useful work and the ecosystem of controllers is mature.
It becomes painful exactly when requirements exceed host and path routing: traffic splitting, header-based routing, cross-namespace delegation, typed policy. Those are the requirements that drove the design of its replacement, and recognising which category a requirement falls into is how you decide which to use.
A strong answer separates the resource from the controller: the object declares host and path rules, and a controller configures a proxy with semantics the specification leaves largely unstated. That is why identical objects behave differently across controllers, why production configuration is mostly untyped annotations where a misspelling is silently ignored, and why path matching trips people up, since prefix matching is on whole segments rather than strings.