Ingress Controllers and Routing Patterns#
An Ingress resource defines HTTP routing rules – which hostnames and paths map to which backend Services. But an Ingress resource does nothing on its own. You need an Ingress controller running in the cluster to watch for Ingress resources and configure the actual reverse proxy.
Ingress Controllers#
The two most common controllers are nginx-ingress and Traefik.
nginx-ingress (ingress-nginx):
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm install ingress-nginx ingress-nginx/ingress-nginx --namespace ingress-nginx --create-namespaceNote: there are two different nginx ingress projects. kubernetes/ingress-nginx (community) and nginxinc/kubernetes-ingress (NGINX Inc). The community version is far more common. Make sure you install from https://kubernetes.github.io/ingress-nginx, not the NGINX Inc chart.
Traefik:
Traefik is the default ingress controller in k3s. For other clusters:
helm repo add traefik https://traefik.github.io/charts
helm install traefik traefik/traefik --namespace traefik --create-namespaceTraefik uses its own CRDs (IngressRoute) in addition to standard Ingress resources.
Basic Ingress Resource#
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
namespace: production
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-frontend
port:
number: 80The ingressClassName field tells Kubernetes which controller should handle this Ingress. If you omit it, the cluster’s default IngressClass is used (if one is configured). If you have multiple controllers and no ingressClassName, the Ingress may be ignored entirely.
Path Types: Prefix vs Exact vs ImplementationSpecific#
This is where most routing bugs come from.
Prefixmatches based on URL path prefix split by/. The path/apimatches/api,/api/, and/api/users, but does NOT match/apiv2.Exactmatches the URL path exactly./apimatches only/api, not/api/or/api/users.ImplementationSpecificleaves matching up to the IngressClass. With nginx-ingress, this behaves like a regex-capable prefix match.
paths:
- path: /api
pathType: Prefix # matches /api, /api/, /api/anything
backend:
service:
name: api-backend
port:
number: 8080
- path: /
pathType: Prefix # catches everything else
backend:
service:
name: web-frontend
port:
number: 80Order matters less than specificity. Kubernetes sorts rules by path length – longer paths match first.
Host-Based Routing#
Route different domains to different backends:
spec:
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 8080
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-app
port:
number: 80If you omit host, the rule matches all incoming requests regardless of hostname.
TLS Termination#
Create a TLS secret and reference it in the Ingress:
kubectl create secret tls app-tls \
--cert=tls.crt --key=tls.key \
-n productionspec:
tls:
- hosts:
- app.example.com
secretName: app-tls
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-app
port:
number: 80The host in tls.hosts must match the host in rules. If they do not match, TLS terminates with the default fake certificate and browsers show a certificate error.
For automatic certificate management, use cert-manager with a ClusterIssuer:
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts:
- app.example.com
secretName: app-tls-auto # cert-manager creates thisAnnotations That Matter (nginx-ingress)#
annotations:
# Rewrite /api/foo to /foo before forwarding to backend
nginx.ingress.kubernetes.io/rewrite-target: /$1
# Increase request body size (default is 1m, too small for file uploads)
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
# WebSocket support
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
# Force HTTPS redirect
nginx.ingress.kubernetes.io/ssl-redirect: "true"
# Backend protocol (when backend uses HTTPS)
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
# Rate limiting
nginx.ingress.kubernetes.io/limit-rps: "10"
# CORS
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-origin: "https://app.example.com"Common Pitfalls#
Trailing slash problems. A request to /app hitting a backend that expects /app/ results in a redirect loop or 404. Fix with rewrite-target or configure your backend to handle both.
Rewrite-target with regex capture groups. When using path regex with rewrite-target, you must use capture groups:
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/use-regex: "true"
# ...
paths:
- path: /api(/|$)(.*)
pathType: ImplementationSpecificWithout use-regex: "true", the regex is treated as a literal string.
413 Request Entity Too Large. The default proxy-body-size is 1MB. Any file upload or large POST will get a 413. Set the annotation to the size you need.
502 Bad Gateway. Usually means the backend service has no ready endpoints. Check kubectl get endpoints <service-name>.
Debugging Ingress#
# See the ingress rules and their status
kubectl describe ingress <ingress-name> -n <namespace>
# Check if the ingress controller assigned an address
kubectl get ingress -n <namespace>
# ADDRESS column should show the controller's IP/hostname
# View nginx-ingress controller logs
kubectl logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx
# Test from inside the cluster
kubectl run curl-test --image=curlimages/curl --rm -it --restart=Never -- \
curl -H "Host: app.example.com" http://ingress-nginx-controller.ingress-nginx.svc/healthz
# Check the generated nginx configuration
kubectl exec -n ingress-nginx deploy/ingress-nginx-controller -- cat /etc/nginx/nginx.conf | grep -A 20 "server_name app.example.com"If kubectl describe ingress shows the correct rules but traffic does not reach your backend, check that the ingressClassName matches your controller’s IngressClass, that the backend service exists and has endpoints, and that no network policy is blocking traffic from the ingress controller namespace to your application namespace.