14.3 The Ingress Model
Kubernetes didn’t invent new routing concepts — it renamed the ones you already know. The translation table: every Ingress object you’ll ever read, as the NGINX config it becomes.
Advanced
What you will learn
The cast: Service, Endpoints, pods, Ingress, controller.
The translation table: k8s objects → NGINX directives.
Readiness as membership: why "pod NotReady" is Lesson 13's endpoint removal, automated.
Annotations as policy, and their honest limits.
1. The cast
A request entering a Kubernetes cluster meets this chain:
internet -> ingress controller's NGINX
^
| watches & renders
[ Ingress resource ] <- your routing intent (host/path rules)
|
points at a Service (a stable name)
|
Endpoints/EndpointSlices (the CURRENTLY READY pod IPs)
|
pods (your actual processes)The magic — and it is the Module 13 story with new names — is the middle: the controller watches the Service's EndpointSlices and renders NGINX config whose upstream contains exactly the ready pod addresses. Nothing you learned changes; the pipeline is industrialized:
Kubernetes says | You learned it as |
|---|---|
Service (ClusterIP/headless) | the stable name of Module 13's discovery contract |
Endpoints / EndpointSlices | the resolved-address list behind the name |
Ingress resource | a
|
ingress controller | the manifest→config→reload loop of Lesson 14.4 |
readiness probe | the health gate that edits membership (Module 13's "DNS says exists" vs "actually serving", answered) |
2. The translation table
An Ingress resource, annotated with its NGINX meaning:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shop-routes
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true" # http->https server (Module 6)
nginx.ingress.kubernetes.io/limit-rps: "100" # limit_req zone (Module 7)
nginx.ingress.kubernetes.io/proxy-body-size: "10m" # client_max_body_size
spec:
ingressClassName: nginx # which controller owns this
tls:
- hosts: [shop.example.com] # ssl_certificate pair (Module 6)
secretName: shop-tls # (cert = a k8s Secret, mounted)
rules:
- host: shop.example.com # server_name (Module 2)
http:
paths:
- path: /api/ # location (Module 2)
pathType: Prefix
backend:
service:
name: svc-api # upstream svc-api (Module 5)
port: { number: 8080 }
- path: / # location /
pathType: Prefix
backend:
service:
name: svc-web
port: { number: 80 }Read that top to bottom and you have written it before — TLS termination, host routing, path locations, upstream pools — every line a Module number. The controller's rendered config would contain, in order: a redirect server, the TLS server with
ssl_certificate from the Secret, two locations proxying to two upstreams whose
server lines are the current ready pods. Even the details translate:
pathType: Prefix vs
ImplementationSpecific is Module 2's prefix-vs-regex location choice in YAML clothing.
3. Readiness is membership
The single most important operational fact of the ingress model — and it is Module 13's failure-honesty lesson with a health layer on top:
A pod that flips NotReady disappears from EndpointSlices, the controller re-renders, NGINX reloads, and traffic stops reaching it — all within the controller's sync loop (seconds), with no human, no DNS TTL, and no stale cache.
Compare the shapes:
Module 13 (DNS) | k8s ingress | |
|---|---|---|
Who notices a dead pod | DNS record removal (external) | kubelet readiness → API → controller |
Staleness bound |
| sync loop latency (~seconds) |
Health semantics | none ("exists") | readiness ("serving") |
Drain story | record disappears (hard cut) | terminationGracePeriod + preStop (real drain) |
This is why Lesson 13.1 deferred the controller story to this module: they bolt health-gated membership onto the runtime-upstream pattern. (And why the mini-controller in Lesson 14.4 models readiness as editing the manifest — that edit IS the EndpointSlice update, by hand.)
The same table exposes k8s' version of the classic trap: readiness flapping. A pod bouncing Ready/NotReady re-renders config on every flip — reload storms are the controller-era echo of Module 13's churn warning. Mitigations live at the probe level (initial delays, failure thresholds) and at the controller level (change-detection that ignores no-op diffs) — but the concept is one you've held since Module 1: reloads are cheap, not free.
4. Annotations as policy — the honest limits
Annotations (
nginx.ingress.kubernetes.io/*) are the escape hatch from YAML-standard into NGINX-specific policy — rate limits, timeouts, body sizes, snippets. Their power is real (self-service routing for teams that shouldn't touch the edge config); their limits are architectural:
Annotation drift is config drift. A typo'd annotation silently does nothing on some controllers, crashes renders on others. (Lesson 14.4's
gen-config.shrefusing an invalid render is the discipline that catches this class of bug — in CI, not in prod.)Snippets (
configuration-snippet) are arbitrary config injection — powerful, and a governance hole you can drive arewrite-shaped truck through. Many platforms disable them; the ones that don't should.Portability is aspirational. Annotations bind you to a controller family — which is fine if chosen deliberately (Lesson 14.5's decision) and expensive if discovered later.
The trajectory of the ecosystem — Gateway API — is exactly this lesson's conclusion: policy that outgrew annotations deserves a first-class object (HTTPRoute, with proper fields, proper validation, proper portability).
Try it yourself
Translation practice, no k8s required: open the
services.json(Lesson 14.4's manifest) next to the YAML above and map each field to its NGINX rendering — then rungen-config.shand check yourself against the output.The readiness story, by hand: remove an endpoint from
services.json, re-render, reload — you have just performed an EndpointSlice update with a text editor.
Q&A
Q: Do I need a Service if I have an Ingress? A: Yes — the Ingress points at Services; the controller reads their Endpoints. (Headless Services + your own NGINX via
resolver — Lesson 13.1's backdoor — is the no-controller alternative.)
Q: Who terminates TLS in the cluster? A: Typically the controller's NGINX (certs from Secrets, auto renewed by cert-manager) — then HTTP or mTLS inward, per Lesson 14.1's Shape B. Passthrough (Lesson 10.5's SNI routing) is also supported by ingress-nginx for the can't-decrypt-here compliance shapes.
Q:
pathType: Prefix
vs
Exact
vs
ImplementationSpecific
? A: Prefix = location
/api/ (longest-prefix), Exact =
location = /api, ImplementationSpecific = "ask your controller" — often regex locations under ingress-nginx. Module 2's matching algorithm, resurfaced as an enum.
Q: What happens to my Module 7 WAF/limits at the edge? A: They ride annotations or the controller's global config (
limit-rps, snippets, custom templates) — the
directives are identical; only their packaging changes.
Takeaways
Kubernetes renamed your toolkit: Service = stable name, Endpoints = address list, Ingress = server/location policy, controller = the render-and-reload loop, readiness = health- gated membership.
The translation table is mechanical — every Ingress field maps to a directive you've written; read YAML as config and it stops being alien.
Readiness-as-membership is Module 13 with a health layer: seconds-scale, health-gated, drained — but flap-happy probes can reload-storm you just like churny DNS could TTL-tax you.
Annotations are policy escape hatches with real governance costs; the ecosystem's answer (Gateway API) is the lesson's own conclusion wearing a nicer type system.
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.