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. /shop matches /shop, /shop/, and /shop/cart. It does not match /shopping. This is usually what you want.
  • Exact: Exactly matches the specified path. Case-sensitive. /shop is /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.