13.1 What Ingress Is and Why Services Are Not Enough
Look, you’ve got your Pods running. You’ve defined a Service to give them a stable IP and load balance between them. You’re feeling pretty good. But then you realize: “Wait, how do I get traffic from the internet to this Service?” You can’t just set type: LoadBalancer on every single Service; your cloud provider will send you a strongly worded bill, and you’ll have a dozen IPs to manage. And what about SSL termination? And path-based routing? My god, the Service object has no idea what a “URL path” even is. It’s just… load balancing TCP.
This is why we need to talk about Ingress. Don’t let the name fool you; it’s not some complex, mystical concept. An Ingress is simply a set of rules you define for how external HTTP/HTTPS traffic should be routed to your Services. Think of it as the traffic cop for your cluster’s north-south traffic. The Service is the internal network, handing off requests between Pods. The Ingress is the front door, the public-facing gateway.
But here’s the critical, often-missed detail: the Ingress resource itself is not the thing doing the work. It’s just a set of instructions, a wishlist you write in YAML. The actual heavy lifting—the listening on ports 80 and 443, the routing logic, the TLS termination—is done by a separate Pod called an Ingress Controller. This is the single most important thing to internalize. You must install an Ingress Controller (like NGINX, Traefik, HAProxy, etc.) for your Ingress rules to mean anything at all. Without one, your beautifully crafted Ingress manifest is a book with no one to read it.
The Anatomy of a Basic Ingress
Let’s make this concrete. You have two Services: a frontend web app (web-app-service) and a backend API (api-service). You want yourdomain.com/ to go to the web app and yourdomain.com/api/ to go to the API. Here’s how you tell the world (and your Ingress Controller) to do that.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-basic-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: / # This annotation is controller-specific!
spec:
rules:
- host: yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-app-service
port:
number: 80
- path: /api
pathType: Prefix
backend:
service:
name: api-service
port:
number: 8080
See that nginx.ingress.kubernetes.io/rewrite-target annotation? This is where we get into the “rough edges.” The Ingress spec is deliberately minimal, so each controller implements advanced features like URL rewriting through their own annotations. This is both a blessing (flexibility) and a curse (it’s not portable). Want to switch from the NGINX controller to Traefik? You’re rewriting those annotations, my friend. It’s the Kubernetes way—standardization, until you actually need to do something useful.
Taming the TLS Beast
You’re not serving plain HTTP like some animal from 1995, are you? Of course not. Ingress handles TLS termination beautifully, keeping the crypto overhead at the edge and letting your internal Pods communicate in plaintext. You do this by first creating a Secret that holds your TLS private key and certificate. Pro tip: Use kubectl create secret tls for this, not a generic Secret. It formats the data correctly.
kubectl create secret tls my-tls-secret \
--cert=path/to/cert.crt \
--key=path/to/private.key
Then, you reference that secret in your Ingress manifest. The Controller will use it to handle the HTTPS connection.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-tls-ingress
spec:
tls:
- hosts:
- yourdomain.com
secretName: my-tls-secret
rules:
- host: yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-app-service
port:
number: 80
Now, for the love of all that is holy, automate this with cert-manager and Let’s Encrypt. Manually creating and rotating TLS secrets is a recipe for a 2 AM wake-up call when your certificate expires. Let the robots handle the robots’ work.
Path Matching: Prefix vs. Exact (and Why It Bites People)
Look back at the pathType field. This seems trivial until it ruins your day. Prefix means exactly what it says: the path /api will match /api, /api/, /api/v1, and /apocalypse (which, admittedly, is probably not what you wanted). Exact means, well, exact. /api only matches /api, not /api/.
The gotcha? The matching logic is often longest-path-first. So if you have two rules, /api and /api/special, a request to /api/special could be matched by the /api rule first if you’re not careful, because it’s a prefix match. Always define your more specific paths first. The implementation details vary by controller, because of course they do, but thinking in terms of specificity will save you. This is the kind of trench knowledge you only get after burning an afternoon debugging why your “special” endpoint is getting a 404. You’re welcome.