Ingress

HTTP routing and TLS in front of Services.

Intermediate12 min · lesson 36 of 65
In plain terms
An Ingress is the building’s reception desk for web visitors: one street address, and the receptionist sends each visitor to the right office based on who they asked for.

Your app runs fine when you port-forward straight to it, a direct tunnel from your laptop to the running container. Point a browser at the real hostname and you get a timeout, a blank page, or somebody else's app. Nine times out of ten the missing piece is Ingress. The tenth time it's an Ingress that exists but has nothing running to carry out its rules, and that's the nastier case, because nothing prints an error to tell you.

Here's the problem Ingress solves. Every Service you expose with type LoadBalancer asks the cloud for its own public load balancer, its own IP address, and its own line on the bill. Ten web apps means ten load balancers and no shared place to handle certificates or routing. Now think of a large office building. You don't give every team its own street entrance and its own receptionist. There's one front door and one reception desk, and the receptionist reads who each visitor asked for and sends them to the right floor. Ingress is that reception desk for web traffic: one public entry point that routes each HTTP request to the right internal Service (Kubernetes' stable handle for an app) by hostname and URL path.

The object is inert, the controller does the work

An Ingress object is just a list of routing rules, written down and stored. By itself it moves no traffic at all. You also need an Ingress controller, a real program (ingress-nginx, Traefik, HAProxy, or a cloud provider's own) running as Pods inside the cluster. A Pod is the smallest thing Kubernetes runs, one or more containers sharing an address. The controller watches the API server (the cluster's single front door and the only component that writes cluster state) for Ingress objects, reads their rules, and rewrites its own proxy configuration to match. It watches rather than polls, so a change lands almost instantly. The real traffic path is one cloud load balancer, into the controller's Pods, which forward to the right ClusterIP Service (an address that only exists inside the cluster), which lands on an application Pod.

This split causes the single most common Ingress mistake. Someone applies an Ingress on a fresh cluster that has no controller installed, then waits for the site to come up. It never does, and nothing complains. The rules just sit in etcd, the cluster's key-value database, being ignored. Before you debug anything downstream, confirm a controller is actually running and that an IngressClass exists for your rules to attach to.

terminal
$ kubectl get pods -n ingress-nginx
NAME READY STATUS RESTARTS AGE
ingress-nginx-controller-7c9f8d5b4f-2xk9p 1/1 Running 0 6d
$ kubectl get ingressclass
NAME CONTROLLER PARAMETERS AGE
nginx k8s.io/ingress-nginx <none> 6d

Rules: hosts, paths, and the pathType trap

Two things route a request: the host it asked for and the path it wants. ingressClassName ties this object to one controller, since a cluster can run several. The field people quietly get wrong is pathType. Prefix matches a whole subtree, so /api matches /api and /api/orders alike. Exact matches one string and nothing beneath it. A third value, ImplementationSpecific, hands the decision to the controller. Mix these up and you get the maddening case of a route that almost works. Anything past plain routing, like a rate limit or a URL rewrite, you reach through annotations, which are key-value hints the controller reads off the object.

The same object routes two ways at once. By host, shop.acme.io and blog.acme.io can send visitors to different Services from a single Ingress. By path, everything under one hostname splits by URL, which is what the file below does: send /api to the api Service and everything else to web. Get the hostname wrong by one character and the request falls through to whatever default the controller keeps, often a bare 404. Big sites use both styles together. Pick the app by its hostname first, then the right piece of that app by its path.

ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shop
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts: [shop.acme.io]
secretName: shop-tls
rules:
- host: shop.acme.io
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api
port:
number: 80
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
terminal
$ kubectl apply -f ingress.yaml
ingress.networking.k8s.io/shop created
$ kubectl get ingress shop
NAME CLASS HOSTS ADDRESS PORTS AGE
shop nginx shop.acme.io 203.0.113.10 80, 443 25s

The ADDRESS column is your first signal. Once it shows an external IP or hostname, the controller has accepted the object and pointed the load balancer at it. A blank ADDRESS a minute later means no controller ever claimed the Ingress, and that is nearly always a wrong or missing ingressClassName, so nothing is listening for that class.

An ADDRESS on the object is not the finish line. That IP or hostname is only where the load balancer sits. A browser still has no idea that shop.acme.io belongs there until you add a DNS record, the internet's address book, pointing the hostname at that address. Skip it and you get the classic afternoon where the site answers by raw IP but never by name, with an Ingress that looks flawless the whole time.

TLS termination in one place

TLS, short for Transport Layer Security, is the lock behind the HTTPS padlock. The receptionist analogy still holds: instead of every office printing its own visitor badges, reception issues and checks them in one spot. The controller terminates TLS, which means it holds the certificate and private key, decrypts incoming HTTPS at the edge, and then speaks plain HTTP to your Pods inside the cluster. The certificate lives in a Secret of type kubernetes.io/tls. In practice cert-manager issues and renews these automatically from a certificate authority like Let's Encrypt, so nobody is copying key files around by hand.

terminal
$ kubectl create secret tls shop-tls --cert=tls.crt --key=tls.key
secret/shop-tls created
$ kubectl get secret shop-tls
NAME TYPE DATA AGE
shop-tls kubernetes.io/tls 2 10s
$ curl -sI https://shop.acme.io/ | head -1
HTTP/2 200

When it breaks, verify in the order traffic flows

Debug an Ingress the same way a request travels through it. Is there an ADDRESS? Then does the controller's own events log show it synced the rule? Then does the backend Service actually have endpoints, meaning ready Pods behind it? A 503 coming from the controller almost always means the Service has no ready endpoints to send to. A 404 from the controller means no rule matched the host or path you sent. describe surfaces the controller's events, and EndpointSlices tell you whether the Service has anywhere to route traffic at all. The controller's own logs are the other half of the story, since ingress-nginx prints each config reload and any host it refused to load.

terminal
$ kubectl describe ingress shop | sed -n '/Events/,$p'
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Sync 30s nginx-ingress-controller Scheduled for sync
$ kubectl get endpointslices -l kubernetes.io/service-name=api
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
api-abc12 IPv4 80 10.244.1.7 6d
Ingress applied but the site is down
Site not reachable through the Ingress
read the symptom, then jump to the layer that owns it
no ADDRESS
Controller never claimed it
Wrong or missing ingressClassName, or no controller installed. Run kubectl get ingressclass and fix the class name.
HTTP 404
No rule matched
The request's Host header or path matched nothing. Check the host spelling and pathType (Prefix vs Exact).
HTTP 503
Backend has no endpoints
The Service selector matches no ready Pods. Check kubectl get endpointslices for that Service.
cert warning
TLS Secret missing or wrong host
secretName points at a missing Secret, or the cert doesn't cover the host. Inspect the kubernetes.io/tls Secret.
An Ingress only sees Services in its own namespace
The backend Service named in an Ingress must live in the same namespace as the Ingress. Put the Ingress in default and the Service in shop, and the controller finds no such Service and answers 503, while the Ingress object itself looks perfectly fine. There is no cross-namespace backend field. To route to an app in another namespace, put a matching Ingress in that namespace, or add an ExternalName Service in the Ingress's namespace to bridge to it. Confirm both sit together with kubectl get ingress,svc -n shop.

No controller means Ingress objects do nothing. Install and watch the controller pods first.

Path types Prefix versus Exact change matching. Trailing slashes have ruined more cutovers than TLS.

TLS secrets must live in the right namespace for most controllers. Cross-namespace tricks are controller-specific.

Try this

Confirm an Ingress controller is running, apply a basic Ingress host rule to a Service, and curl with a Host header. Add TLS only if you have a test secret ready.

terminal
$ kubectl get pods -n ingress-nginx
NAME READY STATUS RESTARTS AGE
ingress-nginx-controller-7c9f8d5b4f-2xk9p 1/1 Running 0 6d
$ kubectl get ingressclass
NAME CONTROLLER PARAMETERS AGE
nginx k8s.io/ingress-nginx <none> 6d
$ kubectl apply -f ingress.yaml
ingress.networking.k8s.io/shop created
$ kubectl get ingress shop
NAME CLASS HOSTS ADDRESS PORTS AGE
shop nginx shop.acme.io 203.0.113.10 80, 443 25s
$ kubectl create secret tls shop-tls --cert=tls.crt --key=tls.key
secret/shop-tls created
$ kubectl get secret shop-tls
NAME TYPE DATA AGE
shop-tls kubernetes.io/tls 2 10s
$ curl -sI https://shop.acme.io/ | head -1
HTTP/2 200
$ kubectl describe ingress shop | sed -n '/Events/,$p'
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Sync 30s nginx-ingress-controller Scheduled for sync
$ kubectl get endpointslices -l kubernetes.io/service-name=api
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
api-abc12 IPv4 80 10.244.1.7 6d

Takeaway

Ingress is HTTP routing in front of Services. The controller is the real data plane; the Ingress object is the config.

Quick check
01You apply an Ingress and kubectl get ingress shows the ADDRESS column still empty two minutes later. What is the most likely cause?
Incorrect — Crashing Pods produce a 503 once traffic actually flows; they don't stop the controller from assigning an ADDRESS.
Correct — A blank ADDRESS means no controller accepted the object, almost always a missing controller or a wrong ingressClassName.
Incorrect — A missing certificate breaks HTTPS, but the controller still assigns an ADDRESS and serves the rule over HTTP.
Incorrect — DNS maps a hostname to the ADDRESS; it cannot be the reason the ADDRESS field itself is blank.
02An Ingress has TLS configured with a kubernetes.io/tls Secret. In the normal ingress-nginx setup, where is the incoming HTTPS decrypted, and what reaches the backend Pods?
Incorrect — The Pods don't hold the certificate; the controller owns it and handles decryption centrally, like reception issuing every badge in one spot.
Incorrect — Standard Ingress terminates TLS at the edge rather than passing it through, and the Pods speak plain HTTP.
Incorrect — The load balancer just forwards to the controller's Pods; the controller is what holds the cert and decrypts.
Correct — the controller holds the certificate and private key, decrypts HTTPS at the edge, and speaks plain HTTP to your Services inside the cluster.
03Requests to https://shop.acme.io/ return 200, but https://shop.acme.io/api returns HTTP 503 from the controller. The Ingress shows an ADDRESS. What's the most likely cause?
Correct — a 503 from the controller means the matched backend has no ready Pods to route to, so the api Service selects nothing ready while web is fine.
Incorrect — That leaves the ADDRESS blank and breaks the whole object, but here / already works and an ADDRESS is assigned.
Incorrect — A missing certificate produces a cert warning, not a 503 on one path while another returns 200 over the same HTTPS host.
Incorrect — A path that matched nothing would return 404, not 503; a 503 means the rule matched but the backend had nowhere to send the request.

Related