Ingress: one front door
HTTP routing for the web.
Say you've got three small web apps, and you want all of them reachable from the internet. The clumsy way is to give each app its own public address, its own load balancer, and its own security certificate to renew. A load balancer is the cloud service that hands an app a public address. A certificate is the file that switches on the little padlock in your browser. Three apps done this way means three separate bills every month, plus three renewal dates to babysit. Ingress is the tidier alternative: one public front door that greets every visitor and sends each one to the right app.
Think about a big office building with one reception desk in the lobby. Guests don't wander the halls hunting for the person they came to see. They give the receptionist a name, and she points them to the right office. An Ingress is that receptionist, but for web traffic. It checks a couple of details on each incoming request and passes it to the correct app inside your cluster, the group of machines your apps run on.
What the front door reads
Two things about each web request decide where it should go. The first is the hostname, which is just the site's name, like shop.example.com. The second is the path, the part of the address after the site name, like /api. So one rule can say: send anything for shop.example.com to the shop app, but when the path starts with /api, send it to the api app instead. That sorting by name and path is called HTTP routing. HTTP, short for hypertext transfer protocol, is simply the language browsers and web servers use to talk to each other. HTTPS is that same language with encryption added, and encryption just means the messages get scrambled so nobody in between can read them. HTTPS is what puts the little padlock next to the address in your browser.
Each of those apps already has a Service inside the cluster. A Service is a stable internal name for an app, so you never have to chase individual Pods around. (A Pod is the smallest unit Kubernetes runs: one or more containers bundled together, and a container is just your packaged-up app.) The default kind of Service, called ClusterIP, can only be reached from inside the cluster. It has no public address at all. Your Ingress sits in front of these private Services, and it's the piece that describes how the outside world gets in through that one door.
Here's a complete Ingress you can save to a file and apply. It's written in YAML, the plain-text format Kubernetes uses to describe what you want to exist. This one assumes you already have two Services running, one named shop and one named api, since the Ingress only points traffic at them.
apiVersion: networking.k8s.io/v1kind: Ingressmetadata:name: sitespec:ingressClassName: nginxtls:- hosts: [shop.example.com]secretName: shop-tlsrules:- host: shop.example.comhttp:paths:- path: /apipathType: Prefixbackend:service:name: apiport:number: 80- path: /pathType: Prefixbackend:service:name: shopport:number: 80
Read it from top to bottom. It's an Ingress named site. The ingressClassName line asks for the nginx controller (hold that thought). The tls section switches on HTTPS for shop.example.com (TLS is just the encryption that HTTPS runs on), using a certificate stored in a Secret named shop-tls. A Secret is just Kubernetes' safe spot for sensitive values like passwords and certificates. The rules underneath do the sorting: anything beginning with /api goes to the api Service, and everything else goes to the shop Service. Save the file, then apply it.
kubectl apply -f ingress.yaml
ingress.networking.k8s.io/site created
Now ask the cluster what you just made.
kubectl get ingress
NAME CLASS HOSTS ADDRESS PORTS AGEsite nginx shop.example.com 203.0.113.42 80, 443 45s
That ADDRESS column is the public IP address your front door answers on. IP is short for internet protocol, and an IP address is just the numbered address a machine uses on the internet, a bit like a house number for computers. Point your domain's DNS at that number and shop.example.com goes live. DNS, the domain name system, is the internet's address book: it turns a friendly name into the IP address behind it, the way your phone's contacts turn a name into a number. One thing trips up almost everyone the first time, though.
The rules need someone to enforce them
An Ingress is only a list of rules. On its own, it won't accept a single connection. Back to the office building: writing "send accounting visitors to floor 3" on a sticky note does nothing if nobody is sitting at the desk to read it. That someone is the Ingress controller, a program that runs inside your cluster, watches for Ingress rules, and does the real work of accepting connections and forwarding them. Nginx and Traefik are two popular ones. On a managed cloud, you usually install a controller once, or the platform gives you one already.
Here's the failure almost everyone hits at least once. Apply that same Ingress on a cluster with no controller, run kubectl get ingress, and the ADDRESS column just stays blank:
kubectl get ingress
NAME CLASS HOSTS ADDRESS PORTS AGEsite nginx shop.example.com 80, 443 9m
kubectl apply reported success and the rules were saved, so nothing looks broken. But a blank address means no traffic, and from the outside the app looks dead. Blank for a minute is normal while the cloud provisions the load balancer; blank forever means nobody is at the desk. Check who's running:
kubectl get pods -n ingress-nginx
No resources found in ingress-nginx namespace.
An empty namespace means exactly what it looks like: no controller installed. Install one (a one-time Helm install, or your cloud's add-on) and run the same check again. This time a pod answers:
kubectl get pods -n ingress-nginx
NAME READY STATUS RESTARTS AGEingress-nginx-controller-7d9c8f5b6c-x2kq4 1/1 Running 0 6d
A Running controller pod means the desk is staffed, and within a minute the ADDRESS column on your Ingress fills in. That ingressClassName: nginx line from the manifest is how your Ingress tells the controller, "you're the one who handles me." If several controllers are installed, that line keeps each Ingress pointed at the right one.
When an Ingress is the wrong tool
One honest limit is worth knowing. An Ingress only understands HTTP and HTTPS, because a hostname and a path are the only things an Ingress rule gives you to sort on. To expose something that isn't a website, like a database on its own port or a mail server speaking raw TCP (the low-level network traffic that HTTP itself is built on top of), there is nowhere in an Ingress to write that down. That's a job for a LoadBalancer Service instead: web traffic goes through an Ingress, everything else gets its own LoadBalancer.
Two names that sound alike do different jobs, so it is worth saying once more. The Ingress is the list of rules. The Ingress controller is the program that reads the list and moves the traffic. Rules with nobody to read them change nothing. And for anything on the web, one front door sorting by hostname and path beats giving every app its own public load balancer and its own bill.
The HTTPS certificate is handled at the front door too. The controller unwraps the encryption using the certificate in that Secret you named, so the apps behind it carry on speaking plain HTTP and never touch a certificate. Certificates are also the part most likely to go wrong later. When the padlock misbehaves, read the controller's logs and run kubectl describe ingress site, which lists recent events at the bottom. The app's own Pods usually have nothing to say about it, because the request never got that far.
One habit is worth starting early. Back at the reception desk, two people can't both be listed under the same name. If two Ingresses claim the same hostname, the controller has to pick one of them, and a deploy is a bad moment to find out which. Give each app its own hostname, like shop.example.com and admin.example.com, and keep each app's Ingress next to that app instead of stuffing every rule into one shared file nobody remembers owning.
Try this
Run these in a Linux or macOS shell, or in Git Bash or WSL on Windows, since the manifest is piped straight into kubectl. If a controller is installed, the address and the rules fill in. If none is installed the commands still work, and the blank address is exactly the thing you're learning to spot.
$ kubectl create deployment inweb --image=nginx:1.27 --replicas=1deployment.apps/inweb created$ kubectl expose deployment inweb --port=80service/inweb exposed$ kubectl apply -f - <<'EOF'apiVersion: networking.k8s.io/v1kind: Ingressmetadata:name: inwebspec:ingressClassName: nginxrules:- host: hello.localhttp:paths:- path: /pathType: Prefixbackend:service:name: inwebport:number: 80EOFingress.networking.k8s.io/inweb created$ kubectl get ingress inwebNAME CLASS HOSTS ADDRESS PORTS AGEinweb nginx hello.local 203.0.113.42 80 8s$ kubectl describe ingress inweb | grep -A6 -E 'Rules:|Backend|Address|Events:'Address: 203.0.113.42Ingress Class: nginxDefault backend: <default>Rules:Host Path Backends---- ---- --------hello.local/ inweb:80 (10.244.0.7:80)Annotations: <none>Events:Type Reason Age From Message---- ------ ---- ---- -------Normal Sync 8s nginx-ingress-controller Scheduled for sync$ kubectl delete ingress inweb; kubectl delete svc inweb; kubectl delete deployment inwebingress.networking.k8s.io "inweb" deletedservice "inweb" deleteddeployment.apps "inweb" deleted
Takeaway
An Ingress is a list of rules, and a controller is what makes those rules real. So when web traffic doesn't arrive, check in that order: is a controller pod running, does kubectl get ingress show an address, and does kubectl describe ingress say anything under Events. Those three checks catch most of it.