Ingress Controllers
Expose HTTP and HTTPS routes with Ingress controllers, path-based routing, and TLS certificates.
Ingress Controllers
In the previous tutorial, we mastered Jobs and CronJobs — running one-time and scheduled tasks. Now let's solve a big problem: getting outside traffic into your cluster.
Remember Services with type LoadBalancer? They work, but each one gets its own external IP. And in the cloud, each one costs money. Running 20 services? That's 20 load balancers. Your finance team will not be happy.
Ingress is the solution — one entry point that routes to multiple services based on hostname or URL path. Think of it as the reception desk of your office building: one front door, but it knows exactly which floor and room to send each visitor to.
How Ingress Works
Internet → Ingress Controller → Ingress Rules → Services → Pods
↓
(nginx, traefik, etc.)
Two components:
- Ingress Controller: A Pod running a reverse proxy (nginx, traefik, HAProxy) — the actual bouncer
- Ingress Resource: YAML defining routing rules — the guest list
The controller watches for Ingress resources and configures itself automatically. You write the rules, it does the routing. How cool is that?
Install NGINX Ingress Controller
First things first — unlike most Kubernetes features, Ingress doesn't work out of the box. You need to install a controller.
On Minikube, it's a one-liner:
minikube addons enable ingress
Verify it's running:
kubectl get pods -n ingress-nginx
NAME READY STATUS RESTARTS AGE
ingress-nginx-controller-5c8d66c76d-abcde 1/1 Running 0 2m
For other environments, use Helm:
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm install ingress-nginx ingress-nginx/ingress-nginx
Create Test Applications
Let's deploy two simple apps so we have something to route to:
# app1.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: app1
spec:
replicas: 1
selector:
matchLabels:
app: app1
template:
metadata:
labels:
app: app1
spec:
containers:
- name: app1
image: hashicorp/http-echo
args: ["-text=Hello from App 1"]
ports:
- containerPort: 5678
---
apiVersion: v1
kind: Service
metadata:
name: app1-service
spec:
selector:
app: app1
ports:
- port: 80
targetPort: 5678
# app2.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: app2
spec:
replicas: 1
selector:
matchLabels:
app: app2
template:
metadata:
labels:
app: app2
spec:
containers:
- name: app2
image: hashicorp/http-echo
args: ["-text=Hello from App 2"]
ports:
- containerPort: 5678
---
apiVersion: v1
kind: Service
metadata:
name: app2-service
spec:
selector:
app: app2
ports:
- port: 80
targetPort: 5678
Apply:
kubectl apply -f app1.yaml -f app2.yaml
Basic Ingress: Path-Based Routing
Now the fun part! Let's route different URL paths to different services. Like /app1 goes to App 1 and /app2 goes to App 2:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: path-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: /app1
pathType: Prefix
backend:
service:
name: app1-service
port:
number: 80
- path: /app2
pathType: Prefix
backend:
service:
name: app2-service
port:
number: 80
Apply:
kubectl apply -f path-ingress.yaml
Check status:
kubectl get ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
path-ingress nginx * 192.168.49.2 80 30s
Test (on Minikube):
minikube ip
# 192.168.49.2
curl http://192.168.49.2/app1
# Hello from App 1
curl http://192.168.49.2/app2
# Hello from App 2
One IP, two apps. Your finance team just bought you lunch.
Path Types
| Type | Behavior |
|---|---|
Prefix | Matches URL paths starting with the value |
Exact | Only matches the exact path |
ImplementationSpecific | Depends on the Ingress controller |
Host-Based Routing
"What if I want different domains to go to different services?"
That's host-based routing — like virtual hosts in Apache or nginx. Different hostnames, same IP:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: host-ingress
spec:
ingressClassName: nginx
rules:
- host: app1.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app1-service
port:
number: 80
- host: app2.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app2-service
port:
number: 80
Apply:
kubectl apply -f host-ingress.yaml
Add to /etc/hosts (so your machine knows where to find these fake domains):
192.168.49.2 app1.local app2.local
Test:
curl http://app1.local
# Hello from App 1
curl http://app2.local
# Hello from App 2
TLS/HTTPS
"What about HTTPS? I can't serve everything over plain HTTP like it's 2005."
Fair point. Let's add TLS.
Create a TLS Secret
Generate a self-signed certificate (for testing — don't use this in production unless you enjoy browser warnings):
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout tls.key -out tls.crt \
-subj "/CN=myapp.local"
Create secret:
kubectl create secret tls myapp-tls --key tls.key --cert tls.crt
Or via YAML:
apiVersion: v1
kind: Secret
metadata:
name: myapp-tls
type: kubernetes.io/tls
data:
tls.crt: <base64-encoded-cert>
tls.key: <base64-encoded-key>
Ingress with TLS
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: tls-ingress
spec:
ingressClassName: nginx
tls:
- hosts:
- myapp.local
secretName: myapp-tls
rules:
- host: myapp.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app1-service
port:
number: 80
Apply:
kubectl apply -f tls-ingress.yaml
Test:
curl -k https://myapp.local
# Hello from App 1
The -k flag ignores certificate validation (because our cert is self-signed and browsers/curl will complain about it).
Useful Annotations
NGINX Ingress supports a ton of annotations. Here are the ones you'll actually use:
Redirect HTTP to HTTPS
Because unencrypted traffic in 2024 is a no-go:
metadata:
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
URL Rewriting
Strip prefixes so your backend doesn't need to know about the routing structure:
metadata:
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
rules:
- http:
paths:
- path: /api(/|$)(.*)
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
/api/users becomes /users when forwarded to the backend. Your API doesn't need to know it's behind an /api prefix. Sneaky and clean.
Rate Limiting
Protect your backend from overeager clients:
metadata:
annotations:
nginx.ingress.kubernetes.io/limit-rps: "10"
nginx.ingress.kubernetes.io/limit-connections: "5"
Timeouts
metadata:
annotations:
nginx.ingress.kubernetes.io/proxy-connect-timeout: "10"
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
CORS
metadata:
annotations:
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-origin: "https://example.com"
Default Backend
What happens when someone hits a URL that doesn't match any rule? Without a default backend, they get a generic 404. Let's do better:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-with-default
spec:
ingressClassName: nginx
defaultBackend:
service:
name: default-service
port:
number: 80
rules:
- host: myapp.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app1-service
port:
number: 80
Combining Rules
Here's what a real-world Ingress looks like — multiple hosts, multiple paths, TLS, the works:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: production-ingress
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- api.example.com
- www.example.com
secretName: example-tls
rules:
- host: www.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: frontend-service
port:
number: 80
- host: api.example.com
http:
paths:
- path: /v1
pathType: Prefix
backend:
service:
name: api-v1-service
port:
number: 80
- path: /v2
pathType: Prefix
backend:
service:
name: api-v2-service
port:
number: 80
That's a production-grade Ingress. One resource, two domains, API versioning, HTTPS everywhere. Pretty slick, right?
Troubleshooting
Ingress can be finicky. Here's your debugging playbook.
Check Ingress Status
kubectl describe ingress <ingress-name>
Look for:
- Events showing configuration updates
- Address field (should have IP)
- Backend status
View Controller Logs
kubectl logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx
Common Issues
No Address assigned:
- Ingress controller not running (did you enable the addon?)
- Wrong
ingressClassName(typo, perhaps?)
404 errors:
- Service name or port wrong (double-check your YAML)
- Path doesn't match (Prefix vs Exact matters!)
502 Bad Gateway:
- Backend pods not ready (check readiness probes from the previous tutorial!)
- Service selector doesn't match pods (labels, labels, labels)
Debug with curl
curl -v http://myapp.local
The -v flag shows headers and connection details. Look for clues about what the Ingress controller is doing with your request.
Clean Up
kubectl delete ingress path-ingress host-ingress tls-ingress 2>/dev/null
kubectl delete -f app1.yaml -f app2.yaml 2>/dev/null
kubectl delete secret myapp-tls 2>/dev/null
What's Next?
Nice work! You now know how to expose your apps to the outside world with proper HTTP routing, TLS, path-based and host-based rules. One entry point, many services. Your cluster is starting to look production-ready.
But here's the thing — all this traffic flowing around inside your cluster... who decides what can talk to what? Right now, anything can reach anything else. That's where Network Policies come in — the firewall rules of Kubernetes. Let's go!