
You've built an app, shipped it into the cluster, and now you want people to actually reach it. Not at http://34.12.9.5:38080 like a hostage note, but at myshop.com, over HTTPS, like a real website. And when you add a second app next month, you really don't want to pay for a second cloud load balancer just to route a URL. That whole problem, one clean front door for many services, is what Ingress solves. This is the topic that finally made Kubernetes networking feel usable to me.
I'll build up to it the way it actually clicked: start with how you expose an app, hit the wall, then let Ingress solve it. What, how, why, takeaway, as always.
Getting to the wall: how you expose an app
Quick recap, because Ingress only makes sense once you feel the pain it removes. There are three ways to expose an app, and each is a step up:
Internal only. Great for a database or backend that other pods talk to, but nobody outside can reach it.
Opens a high port (like 38080) on every node, so users hit node-IP:38080. It works, but the port is ugly and you're handing out raw node IPs.
In the cloud, Kubernetes asks the provider for a real external load balancer with its own public IP. Clean, but you get (and pay for) one per service.
Now the wall. Say your shop lives at myshop.com/wear, and you add a video section at myshop.com/watch. They're separate deployments with separate services. With the tools above, each needs its own load balancer, its own public IP, and its own SSL certificate to manage. Two apps, two load balancers. Ten apps, ten. That's expensive, fiddly, and there's no single place to say "route /wear here and /watch there". You need something that sits out front and makes routing decisions based on the URL.
What Ingress actually is
Ingress is a single entry point that routes incoming HTTP requests to different services based on the URL path or the hostname, terminating SSL along the way. That's what separates it from a plain LoadBalancer, which works at the network level: it forwards a port and understands nothing about URLs. Ingress works at layer 7, meaning it reads the actual HTTP request, the host and the path, so it can make routing decisions on what it sees. Think of it as a reverse proxy (NGINX, HAProxy, Traefik) that you configure with normal Kubernetes objects instead of hand-editing config files.
What you get out of it is one public IP, one place for SSL certificates, and all your URL routing in version-controlled YAML. Before Ingress you'd deploy and babysit those reverse proxies yourself. Now Kubernetes runs the proxy and you just declare the rules.
The two parts everyone conflates
This is the bit that confused me at first, so let me split it cleanly. "Ingress" is really two things:
The actual proxy doing the work, a real program (ingress-nginx, Traefik, HAProxy, Istio) running as pods. It watches the cluster for rules and configures itself.
Just the routing rules, written as a Kubernetes object (kind: Ingress). It's data, not a program. It does nothing on its own.
Deploying the controller (the shape of it)
You normally install a controller from its official manifest or a Helm chart rather than hand-writing it, but it helps to know what's inside, because it's just Kubernetes objects you already know:
- A Deployment running the controller image (the NGINX proxy that watches for Ingress rules).
- A Service (NodePort or LoadBalancer) to actually expose the controller to the outside world on ports 80 and 443.
- A ConfigMap to hold proxy settings (timeouts, SSL options, log format) separately from the image.
- A ServiceAccount (plus RBAC) so the controller has permission to read Ingress resources across the cluster.
apiVersion: extensions/v1beta1 and backend.serviceName. That syntax is removed in modern Kubernetes. The current API is networking.k8s.io/v1, which is what I'm using below. If your YAML errors on a recent cluster, this mismatch is usually why.Routing by path: one host, many services
Here's the first real Ingress resource. One hostname, and the path decides which service gets the request. Note the modern networking.k8s.io/v1 shape: each path has a pathType and points at a service by name and port number.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: store-ingress
spec:
ingressClassName: nginx # which controller handles this
rules:
- http:
paths:
- path: /wear
pathType: Prefix
backend:
service:
name: wear-service
port:
number: 80
- path: /watch
pathType: Prefix
backend:
service:
name: watch-service
port:
number: 80
Reading it plainly: any request to this ingress starting with /wear goes to wear-service, and /watch goes to watch-service, both on port 80. One rule, two paths, one public IP out front. Apply it with kubectl apply -f and check it landed:
kubectl describe ingress shows exactly how traffic will be split. Read it before you trust it.Routing by host: many domains, one ingress
The other way to split traffic is by hostname instead of path. Same idea, but now wear.myshop.com and watch.myshop.com are separate rules, each sending everything to its own service:
spec:
ingressClassName: nginx
rules:
- host: wear.myshop.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: wear-service
port:
number: 80
- host: watch.myshop.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: watch-service
port:
number: 80
So it comes down to how you want to carve things up:
| Path-based | Host-based | |
|---|---|---|
| Splits on | the URL path (/wear, /watch) | the hostname (wear.x.com) |
| Looks like | myshop.com/wear | wear.myshop.com |
| Rules | one rule, many paths | one rule per host |
| Needs | one DNS record | a DNS record per subdomain |
One more piece: the default backend. If a request matches no rule (someone hits myshop.com/listen and you never defined it), you can point spec.defaultBackend at a service that serves a friendly 404 instead of a blank error. Small thing, nice touch.
Try it: a 5-minute routing lab
Enough theory, let's route real traffic. You need Docker plus minikube (which bundles an ingress addon), or kind. Everything runs locally.
Step 1. Cluster + controller. minikube can switch the controller on with one command, no manifests to hunt down:
minikube start
minikube addons enable ingress # installs ingress-nginx for you
Step 2. Two tiny apps. Deploy two demo web servers and expose each as a service:
kubectl create deployment wear --image=hashicorp/http-echo -- -text="WEAR"
kubectl create deployment watch --image=hashicorp/http-echo -- -text="WATCH"
kubectl expose deployment wear --port=5678
kubectl expose deployment watch --port=5678
Step 3. The Ingress. Apply a path-based rule sending /wear and /watch to each service (using port 5678, what http-echo listens on). Step 4. Verify. Curl the ingress with each path and watch it route:
kubectl get pods -n ingress-nginx. And for host-based rules on a lab with no real DNS, you fake the hostname with curl -H "Host: wear.myshop.com" http://$IP/ so the controller sees the host header.The security angle
As a pentester, the ingress is the bit I'd look at hard, because it's literally the front door to everything. A few things worth keeping in mind. It terminates TLS, so your certificates and private keys live here, guard them. Path rules can be got wrong: a sloppy / catch-all or a path that accidentally exposes an internal admin service is a real finding. And the controller is software like anything else, ingress-nginx has had its share of CVEs, so it's on the patch list too. Ingress is powerful, and power at the front door is exactly where you want to be careful.
When the networking chain joined up
This is the one where Kubernetes networking stopped feeling like a pile of disconnected objects. The chain finally joins up: a pod runs the app, a service gives it a stable internal address, and ingress puts a single clean, routed, HTTPS front door on top of it all. The split that took me a beat to get was controller versus resource, the rules are just data, and nothing happens until a controller is there to act on them. I've done the minikube version but not wired up real DNS and Let's Encrypt certs on a live domain yet, so that's my next weekend job, and I'm sure there's TLS-annotation pain waiting for me. Next in the series I want to look at securing the ingress properly, TLS, and where it fits with everything else. If you've run ingress-nginx in anger, tell me what bit you first.
If this made ingress click, come say hi on LinkedIn or the contact page, and tell me what to break down next. More in the Kubernetes Journey.
Further reading
FAQ
What is Kubernetes Ingress?
Ingress is a Kubernetes object that gives you one external entry point which routes HTTP traffic to different services by URL path or hostname, and handles SSL. It is a layer-7 (HTTP-aware) load balancer configured with native Kubernetes objects instead of many separate cloud load balancers.
What is the difference between an Ingress controller and an Ingress resource?
The Ingress controller is the actual proxy that does the routing, like NGINX or Traefik, running as pods in the cluster. The Ingress resource is just the routing rules written as a Kubernetes object. Rules do nothing without a controller to read and enforce them.
Why is my Kubernetes Ingress not working?
The most common reason is that no Ingress controller is installed. Kubernetes does not ship one by default, so an Ingress resource on its own is ignored. Install a controller such as ingress-nginx, and make sure your Ingress references the right ingressClassName.
Related reading
- Topic 14: Network policies (pair these with ingress)
- Topic 12: kubectl proxy & port-forward (reaching services, ClusterIP)
- Topic 7: TLS, certificates & PKI (the certs ingress terminates)
- Browse the whole Kubernetes Journey