Skip to content

Ingress & Controllers

The previous page ended on a sharp limitation. A Service of type LoadBalancer gets external traffic to a set of Pods — but it does so one cloud load balancer per Service. Twenty microservices means twenty load balancers, twenty public IPs, twenty bills, and no shared place to terminate TLS or route by URL. That’s the same crude, per-service plumbing you’d wire up by hand on bare metal, just moved into the cloud. Kubernetes has a better answer for HTTP, and it’s called Ingress.

Almost everything users hit over the web shares one shape: requests arrive at https://example.com, and which backend handles them depends on the host (api.example.com vs app.example.com) and the path (/checkout vs /search). That’s L7 routing — the job of a reverse proxy, the same one from Load Balancing & Proxies. Doing it manually means hand-editing an nginx config every time a service is added, then reloading it without dropping connections, then redoing the TLS certificates by hand. Each step is a chance to typo a server block and take the site down.

Ingress turns all of that into one declarative object:

┌─────────────────────────────────────┐
Internet │ Kubernetes cluster │
│ │
example.com ──HTTPS──► │ [ Ingress Controller ] (one LB) │
│ │ routes by host + path │
│ ├─ api.example.com → Service api │
│ ├─ /search → Service web │
│ └─ /checkout → Service pay │
│ │
└─────────────────────────────────────┘

One public entry point, TLS terminated once, all routing rules version-controlled as data.

Two things, often confused: the resource vs the controller

Section titled “Two things, often confused: the resource vs the controller”

This is the single most important distinction on the page, and it’s the reconciliation loop wearing a new hat.

  • The Ingress resource is a piece of YAML — desired state. It says “/search should go to the web Service.” It does nothing on its own. Apply an Ingress to a cluster with no controller and traffic goes nowhere; the object just sits there.
  • The Ingress controller is a running program (NGINX, Traefik, HAProxy, or a cloud one) that watches Ingress resources and configures a real proxy to match. It’s the actuator that turns the data into behavior.

Because the resource is generic but controllers differ, you tell Kubernetes which controller should own an Ingress with an IngressClass. No ingressClassName and no default class means no controller picks it up — a classic “my Ingress does nothing” gotcha.

First, install a controller. You don’t write one; you deploy a maintained one:

Terminal window
# Install the community NGINX ingress controller (creates one cloud LoadBalancer)
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx --create-namespace
# It comes with an IngressClass named "nginx"
kubectl get ingressclass

Now the routing rules. Note these reference Services by name — Ingress sends traffic to Services, which in turn select Pods:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shop
spec:
ingressClassName: nginx # which controller owns this
rules:
- host: api.example.com # host-based routing
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api
port:
number: 80
- host: shop.example.com
http:
paths:
- path: /search # path-based routing
pathType: Prefix
backend:
service: { name: web, port: { number: 80 } }
- path: /checkout
pathType: Prefix
backend:
service: { name: pay, port: { number: 80 } }
Terminal window
kubectl apply -f ingress.yaml
kubectl get ingress shop # ADDRESS is the controller's public IP
kubectl describe ingress shop # events show whether the controller accepted it

pathType: Prefix matches /search and everything under it; Exact matches only the literal path. The controller compiles these rules into its proxy config and reloads — gracefully, without dropping in-flight requests, which is exactly the manual dance you no longer perform.

TLS is where the manual process is most dangerous: a mistyped cert path or an expired certificate is a visible outage. Ingress moves the certificate into a Secret and lets the controller terminate TLS for you:

spec:
tls:
- hosts: [shop.example.com]
secretName: shop-tls # a kubernetes.io/tls Secret holding cert + key
rules:
- host: shop.example.com
# ...paths as above

The Secret holds the certificate and private key (see HTTP & TLS for the handshake itself). The controller presents the cert, terminates TLS at the edge, and forwards plain HTTP to Pods inside the cluster. Pair it with cert-manager and the entire certificate lifecycle — issue from Let’s Encrypt, renew before expiry, swap the Secret — becomes an automated control loop. The most common “we forgot to renew” outage simply stops happening.

Tie it back to the book’s thread: what manual, error-prone step does this remove? Without Ingress, adding a service to your public surface is a hand-edited proxy config, a manual reload hoping not to drop connections, a manually installed TLS cert, and a calendar reminder to renew it — four chances to cause an outage, repeated for every change. Ingress collapses all of it into one reviewed, version- controlled YAML object reconciled by a controller. Routing rules become diffs in a pull request; certificates renew themselves. The front door of production stops being a place where one careless edit takes everyone offline. From the edge, the next question turns inward — how Pods get their configuration and secrets.

Step back from the YAML and judge Ingress as a front-door design decision:

  • Why does it exist? Because a LoadBalancer Service gives you one cloud LB per Service — twenty microservices means twenty LBs, twenty IPs, twenty bills, and no shared place to terminate TLS or route by URL. Ingress is the one HTTP front door that does host- and path-based L7 routing.
  • What problem does it solve? It collapses hand-edited nginx server blocks, risky reloads, and manually installed certs into one version-controlled object that a controller reconciles — and with cert-manager, TLS issuance and renewal become a control loop, deleting the “we forgot to renew” outage (the Microsoft Teams 2020 failure).
  • What are the trade-offs? The resource is inert without a matching controller, so you must run and operate one (NGINX, Traefik, a cloud one) and wire ingressClassName — forget it and your Ingress silently does nothing.
  • When should I avoid it? When the traffic isn’t HTTP(S): raw TCP/UDP (a database, a game server) still needs a LoadBalancer Service or controller-specific config, since Ingress speaks only L7 — the newer Gateway API is what generalizes beyond it.
  • What breaks if I remove it? Every public service falls back to its own cloud load balancer, TLS gets terminated (and forgotten) per service, and adding a route to the public surface becomes a hand-edited proxy config and a manual reload — four chances at an outage per change instead of a diff in a pull request.
  1. A LoadBalancer Service already exposes a Pod. What specific problems appear when you have twenty web services, and how does Ingress address them?
  2. Explain the difference between an Ingress resource and an Ingress controller. What happens if you apply an Ingress to a cluster that has no controller installed?
  3. How is Ingress “the controller pattern again”? Name two other controllers in Kubernetes that work the same way.
  4. What does pathType: Prefix mean, and how does the controller route shop.example.com/checkout to the right Pods? (Hint: Ingress points at Services, not Pods directly.)
  5. Using the book’s thread, describe the manual, error-prone steps around TLS that Ingress plus cert-manager eliminate.
Show answers
  1. A LoadBalancer Service exposes Pods one cloud load balancer per Service: twenty services means twenty LBs, twenty public IPs, twenty bills, and no shared place to terminate TLS or route by URL. Ingress provides one entry point that routes by host and path and terminates TLS once, with all rules in a single version-controlled object.
  2. The resource is YAML (desired state) that does nothing on its own; the controller is a running program (NGINX, Traefik, a cloud one) that watches Ingress resources and configures a real proxy to match. Apply an Ingress with no controller installed and traffic goes nowhere — the object just sits there inert.
  3. It’s declare desired state, a controller reconciles reality to match: the Ingress controller watches Ingress resources and reconciles a proxy’s config. Two others: the scheduler (watches unassigned Pods) and the Deployment controller (watches/manages ReplicaSets).
  4. pathType: Prefix matches the path and everything under it (/checkout and below), versus Exact for the literal path only. The controller compiles the rules into proxy config, matches shop.example.com/checkout to the pay Service, and the Service then selects the actual Pods — Ingress points at Services, not Pods directly.
  5. Without Ingress, TLS means a hand-installed certificate, a mistyped cert path or expired cert as a visible outage, and a calendar reminder to renew that someone forgets. Ingress moves the cert into a Secret and terminates TLS at the edge; pair it with cert-manager and issuance, renewal-before-expiry, and Secret swap become an automated control loop — the “we forgot to renew” outage stops happening.