A type: LoadBalancer Service gives you one external IP per Service — expensive and unmanageable at scale. Ingress solves this: a single entry point (one IP/LB) that routes HTTP/HTTPS traffic to multiple backend Services based on hostname and path — like a virtual host configuration.
1. How Ingress Works
Ingress has two components:
- Ingress resource — a declarative YAML describing routing rules (host, path → backend Service)
- Ingress Controller — the actual software (nginx, envoy, traefik) that reads the rules and routes traffic
The Ingress resource is useless without a controller. Creating an Ingress object does nothing unless a controller is watching for it.
2. Ingress Resource
Simple Fanout (Path-Based Routing)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: / # controller-specific
spec:
ingressClassName: nginx # which controller handles this
rules:
- host: app.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api-svc
port:
number: 80
- path: /
pathType: Prefix
backend:
service:
name: web-svc
port:
number: 80
Name-Based Virtual Hosting
spec:
rules:
- host: api.example.com # Route by hostname
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-svc
port:
number: 80
- host: blog.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: blog-svc
port:
number: 80
Path Types
| pathType | Matching | Example |
|---|---|---|
Prefix | Matches URL path prefix (by path segment) | /api matches /api, /api/, /api/v1 |
Exact | Matches exactly | /api matches only /api, not /api/ |
ImplementationSpecific | Up to the controller | Depends on nginx/envoy config |
/api matches /api and /api/v1 but NOT /apikeys (different segment boundary). This is stricter than simple string prefix matching. The / separates segments.
Default Backend
spec:
defaultBackend: # Handles requests that match no rule
service:
name: default-svc
port:
number: 80
rules:
- ... # Specific rules
kubectl create ingress app --rule="app.example.com/api=api-svc:80" --rule="app.example.com/=web-svc:80" --dry-run=client -o yaml
3. TLS Termination
Ingress handles HTTPS by terminating TLS at the controller. Traffic between controller → backend Pods is typically unencrypted (within the cluster).
Setup
# 1. Create a TLS Secret:
kubectl create secret tls app-tls \
--cert=./tls.crt \
--key=./tls.key
# 2. Reference it in the Ingress:
spec:
tls:
- hosts:
- app.example.com # Must match a rule host
- api.example.com
secretName: app-tls # TLS Secret in same namespace
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-svc
port:
number: 80
TLS Modes
| Mode | Description | Config |
|---|---|---|
| Terminate at Ingress | Client → HTTPS → Controller → HTTP → Pod | Default (spec.tls with Secret) |
| Passthrough | Client → HTTPS → Controller passes encrypted to Pod | Annotation: ssl-passthrough: "true" |
| Re-encrypt | Client → HTTPS → Controller → HTTPS → Pod | Controller-specific backend TLS annotations |
cert-manager.io/cluster-issuer: letsencrypt-prod and it handles everything.
HTTP → HTTPS Redirect
# Most controllers support forced redirect:
metadata:
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true" # NGINX
# or for other controllers:
# traefik.ingress.kubernetes.io/redirect-scheme: https
4. Ingress Controllers
The Ingress resource is just a spec. The controller is the implementation. You must install one — nothing happens without it.
| Controller | Proxy | Strengths | Managed By |
|---|---|---|---|
| ingress-nginx | NGINX | Most popular, battle-tested, extensive annotations | K8s community |
| Traefik | Traefik | Auto-discovery, middleware chain, Let's Encrypt built-in | Traefik Labs |
| Contour | Envoy | HTTPProxy CRD (richer than Ingress), multi-team delegation | VMware/Heptio |
| Emissary | Envoy | API Gateway features, rate limiting, auth | Ambassador Labs |
| AWS ALB Controller | AWS ALB | Native AWS integration, no in-cluster proxy Pods | AWS |
| GKE Ingress | Google Cloud LB | Native GCP, global HTTPS LB |
IngressClass
When multiple controllers exist in a cluster, ingressClassName determines which one handles a given Ingress:
# IngressClass resource (installed by the controller):
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: nginx
annotations:
ingressclass.kubernetes.io/is-default-class: "true" # default if none specified
spec:
controller: k8s.io/ingress-nginx
# Reference in Ingress:
spec:
ingressClassName: nginx
NGINX vs Envoy-Based Controllers
| Aspect | NGINX (ingress-nginx) | Envoy (Contour, Emissary) |
|---|---|---|
| Config reload | Reload nginx process (brief connection drain) | Hot config via xDS API (zero-drop) |
| L7 features | Via annotations (100+ supported) | Via CRDs (HTTPProxy, Mapping) |
| Observability | Access logs, basic metrics | Rich L7 metrics, distributed tracing headers |
| Performance | Excellent for most workloads | Better at high connection counts, HTTP/2 |
| Community | Largest user base, most StackOverflow answers | Growing, favored by platform teams |
5. Common NGINX Ingress Annotations
| Annotation | Purpose | Example Value |
|---|---|---|
nginx.ingress.kubernetes.io/rewrite-target | Rewrite URL path before forwarding | / or /$2 |
nginx.ingress.kubernetes.io/ssl-redirect | Force HTTPS | "true" |
nginx.ingress.kubernetes.io/proxy-body-size | Max request body | "50m" |
nginx.ingress.kubernetes.io/proxy-read-timeout | Backend read timeout | "60" |
nginx.ingress.kubernetes.io/use-regex | Enable regex in path | "true" |
nginx.ingress.kubernetes.io/affinity | Session stickiness | "cookie" |
nginx.ingress.kubernetes.io/limit-rps | Rate limiting (requests/sec) | "10" |
nginx.ingress.kubernetes.io/auth-url | External auth (forward auth) | URL of auth service |
Rewrite Example
# Route /api/v1/users → backend receives /users
metadata:
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/use-regex: "true"
spec:
rules:
- host: app.example.com
http:
paths:
- path: /api(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: api-svc
port:
number: 80
Summary
| Concept | Key Point |
|---|---|
| Ingress resource | Declarative routing rules (host + path → Service) |
| Ingress Controller | Actual proxy that implements the rules (must be installed) |
| IngressClass | Selects which controller handles an Ingress |
| Path types | Prefix (per-segment), Exact, ImplementationSpecific |
| TLS | Reference a kubernetes.io/tls Secret in spec.tls |
| Annotations | Controller-specific features (rewrite, rate limit, auth) — not portable |
| NGINX vs Envoy | NGINX: simple, proven. Envoy: hot-reload, richer L7, better at scale. |
| cert-manager | Automates TLS cert provisioning from Let's Encrypt |
📝 Quiz: Ingress & Ingress Controllers
Q1: You create an Ingress resource but it doesn't work — no traffic is routed. What's likely missing?
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.9.0/deploy/static/provider/cloud/deploy.yaml).Q2: What's the difference between pathType: Prefix with path /api matching /api vs /apikeys?
/api Prefix matches /api, /api/, and /api/v1 but NOT /apikeys. Prefix matching works per path segment (separated by /). /apikeys is a different segment — it doesn't start with /api/.Q3: Your Ingress has TLS configured for app.example.com. A client connects via HTTP. What happens?
nginx.ingress.kubernetes.io/ssl-redirect: "true". Without this annotation, the HTTP request may be served unencrypted (controller-dependent behavior).Q4: You have two Ingress controllers: nginx and traefik. How does a new Ingress know which controller should handle it?
spec.ingressClassName to the name of the IngressClass that corresponds to the desired controller (e.g., ingressClassName: nginx or ingressClassName: traefik). If not set, the controller marked with ingressclass.kubernetes.io/is-default-class: "true" handles it.Q5: Why might you choose an Envoy-based Ingress controller over NGINX?
Q6: What's the main limitation of the Ingress API that led to the creation of the Gateway API?