Ingress & Ingress Controllers
A Service of type LoadBalancer gives you one external IP for one service. For a cluster running a dozen services, that's a dozen cloud load balancers and a dozen bills. Ingress solves this: one load balancer at the cluster edge, one Ingress Controller that reads routing rules, and as many host/path rules as you need — all for free after the initial controller setup.
The Problem with LoadBalancers
Without Ingress, exposing multiple HTTP services to the internet looks like this: one LoadBalancer Service per app, each with its own cloud IP, each costing money. Routing between them requires DNS entries pointing to separate IPs. There's no shared TLS, no centralized auth, no path rewriting.
How Ingress Works
Ingress splits the problem into two pieces:
- Ingress resource — a Kubernetes API object (
kind: Ingress) that declares routing rules: which hostname maps to which Service, which paths get which backends, which Secret holds TLS certs. - Ingress Controller — a pod running inside the cluster that watches Ingress resources and configures an actual reverse proxy (nginx, Envoy, Traefik, HAProxy…). Nothing happens until a controller is installed.
A single LoadBalancer Service exposes the Ingress Controller to the internet. All HTTP/HTTPS traffic flows in through that one IP, and the controller does the routing internally.
Creating an Ingress resource in a cluster with no Ingress Controller installed has no effect. The resource sits there with no ADDRESS assigned. Check kubectl get ingress — if the ADDRESS column is empty, a controller is missing or misconfigured.
Ingress Controllers
Kubernetes ships no built-in Ingress Controller — you install one. The most widely used options:
| Controller | Proxy | Best for |
|---|---|---|
| ingress-nginx | nginx | General-purpose, most community resources, good annotation coverage |
| Traefik | Traefik | Automatic cert renewal (Let's Encrypt), Docker/K8s-native config, middleware plugins |
| AWS ALB Controller | AWS ALB | EKS clusters — uses native ALB, supports WAF and shield |
| GKE Ingress | Google LB | GKE clusters — uses Google's global HTTP(S) LB natively |
| HAProxy Ingress | HAProxy | High-throughput, bare-metal, fine-grained ACL control |
Install ingress-nginx with Helm (works on any cluster including local kind/minikube):
helm upgrade --install ingress-nginx ingress-nginx \
--repo https://kubernetes.github.io/ingress-nginx \
--namespace ingress-nginx --create-namespace \
--set controller.service.type=LoadBalancer
On a local cluster (kind), use NodePort instead of LoadBalancer and add a host-network config — or use kubectl port-forward during development.
Host-based Routing
Route traffic to different Services based on the Host HTTP header. The most common pattern for multi-tenant clusters where each service owns a subdomain.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: multi-host
namespace: default
spec:
ingressClassName: nginx
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api
port:
number: 8080
Path-based Routing
Route different URL paths on the same hostname to different backend Services. Useful for splitting a monolith into micro-services without changing the public API surface.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: path-split
namespace: default
spec:
ingressClassName: nginx
rules:
- host: example.com
http:
paths:
- path: /api
pathType: Prefix # matches /api, /api/v1, /api/users/…
backend:
service:
name: api
port:
number: 8080
- path: /static
pathType: Prefix
backend:
service:
name: cdn
port:
number: 80
- path: /
pathType: Prefix # catch-all — must be last
backend:
service:
name: web
port:
number: 80
pathType values:
| pathType | Behaviour |
|---|---|
Prefix | Matches the path and any sub-path (/api matches /api, /api/v1, /api/users) |
Exact | Matches only the exact path, case-sensitive |
ImplementationSpecific | Interpretation is up to the Ingress Controller — avoid unless you know the controller |
TLS Termination
The Ingress Controller handles TLS termination — clients connect over HTTPS, the controller decrypts the traffic, and forwards plain HTTP to backend pods. You store the certificate and key in a TLS Secret.
apiVersion: v1
kind: Secret
metadata:
name: example-tls
namespace: default
type: kubernetes.io/tls
data:
tls.crt: <base64-encoded-cert>
tls.key: <base64-encoded-key>
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: secure-ingress
namespace: default
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- example.com
- api.example.com
secretName: example-tls # must contain tls.crt and tls.key
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
For end-to-end encryption use a service mesh (Istio/Linkerd) or
ssl-passthrough mode.
For automatic certificate provisioning (Let's Encrypt), install cert-manager and add an annotation:
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
cert-manager watches Ingress objects with that annotation, requests a certificate automatically, and stores it in the named Secret. Renewal is also automatic.
Annotations
Ingress annotations configure controller-specific behaviour that doesn't fit in the standard spec. They're controller-specific — annotations that work on nginx-ingress do nothing on Traefik or the AWS ALB controller.
metadata:
annotations:
# Force HTTPS — redirect HTTP to HTTPS
nginx.ingress.kubernetes.io/ssl-redirect: "true"
# Rewrite the URL path before forwarding to backend
# /app/users → /users
nginx.ingress.kubernetes.io/rewrite-target: /$2
# with path: /app(/|$)(.*)
# Increase proxy timeouts (seconds)
nginx.ingress.kubernetes.io/proxy-connect-timeout: "60"
nginx.ingress.kubernetes.io/proxy-read-timeout: "120"
# Enable CORS
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-origin: "https://app.example.com"
# Rate limiting
nginx.ingress.kubernetes.io/limit-rps: "10"
# Basic auth
nginx.ingress.kubernetes.io/auth-type: basic
nginx.ingress.kubernetes.io/auth-secret: basic-auth-secret
nginx.ingress.kubernetes.io/auth-realm: "Protected"
Complex setups end up with 15–20 annotations per Ingress, each controller has different annotation keys, and there's no schema validation. This is one of the main reasons Gateway API exists — it moves configuration into typed resources instead of free-form strings.
IngressClass
When multiple Ingress Controllers are installed (e.g. nginx for public traffic, Traefik for internal), you use IngressClass to tell each Ingress resource which controller should handle it.
# Each controller registers an IngressClass
kubectl get ingressclass
# NAME CONTROLLER PARAMETERS AGE
# nginx k8s.io/ingress-nginx <none> 2d
# traefik traefik.io/ingress <none> 1d
# Reference it in your Ingress:
spec:
ingressClassName: nginx # or traefik
One IngressClass can be set as the cluster default (annotation ingressclass.kubernetes.io/is-default-class: "true") — Ingress objects without an explicit ingressClassName are handled by the default controller.
Ingress vs Gateway API
The Ingress spec was designed in 2015 for basic HTTP routing. It shows its age: no TCP/UDP support, no traffic splitting, no header manipulation without annotations, no multi-tenant separation between routes and gateways. Gateway API (GA in Kubernetes 1.31) is the official successor.
| Ingress | Gateway API | |
|---|---|---|
| Protocol support | HTTP/HTTPS only | HTTP, HTTPS, TCP, UDP, gRPC, TLS |
| Traffic splitting | Via annotations (controller-specific) | Native weight on routes |
| Header manipulation | Annotations | Typed filter fields |
| Multi-tenancy | Single Ingress resource owns routes | Gateway owned by infra team; HTTPRoute owned by app teams |
| Status | Stable, widely supported | GA in 1.31, growing support |
| Migration | — | Most major controllers have Gateway API support |
For new clusters, prefer Gateway API if your controller supports it. For existing Ingress setups, there's no urgency — Ingress is not deprecated and will continue to work.
kubectl Commands
# Apply an Ingress
kubectl apply -f ingress.yaml
# List Ingress resources (ADDRESS column shows the assigned IP)
kubectl get ingress
kubectl get ingress -A # all namespaces
# Describe an Ingress (shows rules, backend services, TLS, events)
kubectl describe ingress my-ingress
# Check which pods the Ingress Controller is routing to
kubectl get endpoints -n default web api auth
# Watch Ingress Controller logs (nginx-ingress)
kubectl logs -n ingress-nginx \
-l app.kubernetes.io/component=controller \
--tail=50 -f
# Test routing with curl (override Host header)
curl -H "Host: api.example.com" http://<INGRESS_IP>/v1/health
# List IngressClasses
kubectl get ingressclass
# Delete an Ingress
kubectl delete ingress my-ingress