CoursesKubernetes fundamentalsIngress: one front door

Ingress: one front door

HTTP routing for the web.

Beginner10 min · lesson 19 of 24
In plain terms
An Ingress is the building’s receptionist for web visitors: one street address, and they send each guest to the right office based on who they asked for.

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.

ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: site
spec:
ingressClassName: nginx
tls:
- hosts: [shop.example.com]
secretName: shop-tls
rules:
- host: shop.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api
port:
number: 80
- path: /
pathType: Prefix
backend:
service:
name: shop
port:
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.

terminal
kubectl apply -f ingress.yaml
output
ingress.networking.k8s.io/site created

Now ask the cluster what you just made.

terminal
kubectl get ingress
output
NAME CLASS HOSTS ADDRESS PORTS AGE
site 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:

terminal
kubectl get ingress
output
NAME CLASS HOSTS ADDRESS PORTS AGE
site 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:

terminal
kubectl get pods -n ingress-nginx
output
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:

terminal
kubectl get pods -n ingress-nginx
output
NAME READY STATUS RESTARTS AGE
ingress-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.

One door, the right app
1visitorasks for shop.example.com/api2Ingress controllerthe program at the desk (nginx)3match host + pathreads your rules, handles HTTPS4ClusterIP Servicethe right internal app: api5Podanswers the request
One public door. The controller reads your Ingress rules, handles the HTTPS certificate, and forwards each request to the internal Service whose host and path match.

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.

terminal
$ kubectl create deployment inweb --image=nginx:1.27 --replicas=1
deployment.apps/inweb created
$ kubectl expose deployment inweb --port=80
service/inweb exposed
$ kubectl apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: inweb
spec:
ingressClassName: nginx
rules:
- host: hello.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: inweb
port:
number: 80
EOF
ingress.networking.k8s.io/inweb created
$ kubectl get ingress inweb
NAME CLASS HOSTS ADDRESS PORTS AGE
inweb nginx hello.local 203.0.113.42 80 8s
$ kubectl describe ingress inweb | grep -A6 -E 'Rules:|Backend|Address|Events:'
Address: 203.0.113.42
Ingress Class: nginx
Default 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 inweb
ingress.networking.k8s.io "inweb" deleted
service "inweb" deleted
deployment.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.

A class name nobody answers to
There is a second way to end up with a blank ADDRESS, and from the outside it looks identical to having no controller at all. Say the nginx controller is running perfectly well, but your Ingress asks for ingressClassName: traefik. Every controller in the cluster reads that line, decides this Ingress belongs to someone else, and leaves it alone. The rules are saved, the controller pod is healthy, and the address never arrives. So when a controller is clearly running and the address is still blank after a few minutes, hold the CLASS column from kubectl get ingress next to the controller you actually installed and check that they match.
Quick check
01kubectl get ingress has shown a blank ADDRESS for nine minutes, and kubectl get pods -n ingress-nginx prints "No resources found in ingress-nginx namespace." You install an nginx controller. What do you do to the Ingress next?
Correct — The rules were already saved the moment apply printed ingress.networking.k8s.io/site created. Installing a controller staffs the desk, and it picks up the Ingress that is already there, filling in the address within about a minute.
Incorrect — A controller watches for Ingress objects when it starts, not only for ones created after it. The rules sat there waiting the whole time, so a second apply changes nothing.
Incorrect — There is no registration step to redo. Deleting and recreating only gives you a window where the rules do not exist at all.
Incorrect — The address goes to the front door, not to each app behind it. Giving every backend its own load balancer is exactly the per-app bill an Ingress exists to avoid.
02A teammate looks at the sample manifest and says the shop and api Services should be switched from ClusterIP to LoadBalancer so the Ingress can reach them. Why is that the wrong move?
Incorrect — An Ingress can point at a Service whatever its type. The problem with the change is not that it breaks routing, it is that it buys public addresses nobody needs.
Incorrect — Both types carry HTTP fine. Raw TCP is the reason you would reach for a LoadBalancer for something like a database, which is a different situation from a web app behind an Ingress.
Correct — The Ingress controller runs inside the cluster, which is the one place a ClusterIP Service is reachable from. Every app given its own public address brings back a separate bill and a separate certificate to renew.
Incorrect — Changing a Service type does not move certificate handling onto the app. The controller still unwraps the encryption at the front door using shop-tls, and the Pods still speak plain HTTP.
03The padlock on shop.example.com starts throwing a certificate warning in browsers. Which check is the right first look?
Incorrect — The controller unwraps the encryption at the front door and hands plain HTTP to the app behind it. The Pods never touch a certificate, so they usually have nothing to say about one.
Correct — TLS is handled at the front door with the certificate in the Secret you named, so the controller and the Ingress events are where a bad or missing certificate shows itself.
Incorrect — A stopped controller takes the whole site down and empties the ADDRESS column rather than serving a page with a warning. Worth a glance later, but it is not what a padlock complaint points at.
Incorrect — Wrong DNS would send visitors to some other machine entirely. Here the request is arriving at your front door and getting a reply, so the certificate at that door is the suspect.

Related