13.3 Ingress Rules: Host-Based and Path-Based Routing
Right, so you’ve got a cluster, and you’ve got pods running your app. Wonderful. But users aren’t going to type http://10.244.2.15:8080 into their browser to see your masterpiece, are they? This is where the Ingress resource comes in. Think of it as the world’s most configurable, slightly fussy bouncer for your club. Its job is to look at incoming HTTP/HTTPS requests and, based on rules you give it, decide which service inside the cluster gets to handle it. We’re going to talk about the two main ways you instruct this bouncer: by the host you’re asking for, and by the path on that host.
The Absolute Basics: It’s Just a Fancy Reverse Proxy
Before we get lost in YAML, let’s be crystal clear: an Ingress isn’t magic. It’s just a set of rules. Something has to enforce those rules. That “something” is an Ingress Controller (like ingress-nginx, Traefik, or Ambassador). You install the controller, and it watches for Ingress resources you create. When it finds one, it translates your abstract rules into actual configuration for its underlying proxy software (like Nginx or Envoy). This is the most common point of failure—people write a perfect Ingress resource but forget they never actually installed a controller to implement it. The resource just sits there, uselessly.
Here’s the most minimal, almost trivial example to get us started. It just routes everything to one service.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: minimal-ingress
spec:
defaultBackend:
service:
name: main-app-service
port:
number: 80
This is the “catch-all” or default backend. Any request that hits the Ingress controller’s IP, regardless of the hostname or path, gets sent to main-app-service. It’s fine for a single-app cluster, but we usually need more nuance.
Host-Based Routing: The Virtual Host Special
This is the most common method, and it’s a direct parallel to how web servers have handled multiple sites on one IP for decades (virtual hosts). The rule is simple: “If the Host header in the request is shop.example.com, send it to the shopping cart service. If it’s blog.example.com, send it to the WordPress service.”
It’s brilliantly straightforward. Here’s how you define it:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: host-based-ingress
spec:
rules:
- host: shop.myawesomeapp.com
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: shop-service
port:
number: 80
- host: api.myawesomeapp.com
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: api-service
port:
number: 8000
Notice the path: "/" and pathType: Prefix here. This is essentially saying “for this host, match any path starting with /” which is… all of them. So it’s pure host-based routing. The api-service is listening on port 8000, proving your backend services don’t even need to be on the standard web ports; the Ingress handles the translation.
Path-Based Routing: The Organizer
Now, let’s say you don’t have different hostnames. You have myapp.com, and you want myapp.com/shop to go to one service and myapp.com/api to go to another. This is path-based routing. You’re now carving up a single hostname into different segments.
The critical thing to understand here is the pathType. There are three, but you’ll mostly care about Prefix and Exact. Prefix is the workhorse. It matches based on, you guessed it, a URL path prefix.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: path-based-ingress
spec:
rules:
- host: myapp.com
http:
paths:
- pathType: Prefix
path: "/shop"
backend:
service:
name: shop-service
port:
number: 80
- pathType: Prefix
path: "/api"
backend:
service:
name: api-service
port:
number: 8000
- pathType: Prefix
path: "/"
backend:
service:
name: main-app-service
port:
number: 8080
See the order? This is where the bouncer’s rulebook gets specific. The rules are evaluated in order, and the first match wins. A request to myapp.com/shop/cart will match the /shop rule first and get routed to the shop-service. A request to myapp.com/ will only match the last rule. If you put the / rule first, it would match everything, and your /shop and /api rules would never get a chance. This is a classic “why isn’t my routing working” pitfall. Always list your most specific paths first.
The Gotchas: Where This All Gets Weird
The designers, in their infinite wisdom, decided that a path like /shop should, by default, also match /shopping and /shopify because they share the same prefix. This is, to put it technically, bonkers. This is why the pathType field is so important.
Prefix: Matches based on a split-by-/path segment./shopmatches/shop,/shop/, and/shop/cart. It does not match/shopping. This is usually what you want.Exact: Exactly matches the specified path. Case-sensitive./shopis/shop, not/shop/and certainly not/Shop.ImplementationSpecific: This is the “I don’t care, you figure it out” option. Its behavior depends on the Ingress controller you’re using. Just avoid it. Be explicit.
Another huge pitfall is what happens to the path when it’s forwarded. Does /shop get passed to the backend service as /shop or as /? Most controllers (like ingress-nginx) by default will forward the full original path. Your shop service, expecting to be rooted at /, now gets a request for /shop/cart and freaks out because it has no such route. The fix is to use a rewrite-target annotation (which is controller-specific, hence the ugly “implementation-specific” leak). For ingress-nginx, you’d add this to your metadata:
metadata:
name: path-based-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
rules:
- host: myapp.com
http:
paths:
- pathType: Prefix
path: "/shop(/|$)(.*)" # Now using a regex capture group
backend:
service:
name: shop-service
port:
number: 80
This regex matches /shop and any path after it, and the rewrite-target: /$2 sends just the captured (.*) part to the backend. So /shop/cart gets rewritten to /cart before being sent to the shop-service. It’s powerful, but it’s a messy, vendor-specific solution to a common problem. You just have to know that this is a thing. Welcome to the trenches.