Ingress resources, ingress controllers, TLS termination, Gateway API, and path-based and host-based routing.
Services expose Pods to the network, but they have limitations for HTTP traffic:
Ingress solves this by providing HTTP/HTTPS routing rules in front of multiple Services through a single entry point:
┌────────────────────┐
Internet ────────────────▶ Ingress Controller │
│ (nginx, traefik) │
└────────┬───────────┘
│
┌────────────────┼────────────────┐
│ │ │
┌─────────▼──┐ ┌────────▼───┐ ┌───────▼─────┐
│ app.com/ │ │ app.com/api │ │ blog.com/ │
│ → frontend │ │ → api svc │ │ → blog svc │
│ Service │ │ Service │ │ Service │
└────────────┘ └────────────┘ └─────────────┘
One load balancer, many Services. The Ingress Controller routes traffic based on hostname and path.
An Ingress resource by itself does nothing. You need an Ingress Controller — a Pod that reads Ingress resources and configures a reverse proxy (nginx, Traefik, HAProxy, Envoy).
minikube addons enable ingress
# Verify it's running
kubectl get pods -n ingress-nginx
# NAME READY STATUS RESTARTS
# ingress-nginx-controller-7d4b8c6f5-abc12 1/1 Running 0
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.9.4/deploy/static/provider/cloud/deploy.yaml
# Wait for the controller to be ready
kubectl wait --namespace ingress-nginx \
--for=condition=ready pod \
--selector=app.kubernetes.io/component=controller \
--timeout=120s
# Verify
kubectl get pods -n ingress-nginx
# NAME READY STATUS RESTARTS
# ingress-nginx-controller-xxxxx 1/1 Running 0
kubectl get svc -n ingress-nginx
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
# ingress-nginx-controller LoadBalancer 10.96.50.200 localhost 80:31080/TCP,443:31443/TCP
Tip: The Ingress Controller itself runs as a Deployment with a LoadBalancer Service. This is the one cloud LB you pay for — all your Ingress rules route through it.
An Ingress resource defines routing rules. The controller reads these rules and configures its reverse proxy.
Route different URL paths to different Services:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: myapp.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: frontend
port:
number: 80
- path: /api
pathType: Prefix
backend:
service:
name: api
port:
number: 80
kubectl apply -f ingress-path.yaml
kubectl get ingress
# NAME CLASS HOSTS ADDRESS PORTS AGE
# app-ingress nginx myapp.local localhost 80 5s
# Test (add myapp.local to /etc/hosts pointing to your cluster IP)
# On Docker Desktop: echo "127.0.0.1 myapp.local" | sudo tee -a /etc/hosts
curl http://myapp.local/ # → frontend Service
curl http://myapp.local/api # → api Service
paths:
- path: /api
pathType: Prefix # matches /api, /api/, /api/users, /api/users/123
- path: /api
pathType: Exact # matches only /api, NOT /api/ or /api/users
- path: /
pathType: ImplementationSpecific # up to the controller
Gotcha:
Prefixmatching is by path segment, not string prefix./apimatches/apiand/api/usersbut NOT/api2. This is a common source of confusion.
Route different hostnames to different Services:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: multi-host
spec:
ingressClassName: nginx
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app
port:
number: 80
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api
port:
number: 80
- host: blog.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: blog
port:
number: 80
Three domains, three Services, one Ingress Controller, one load balancer.
Ingress handles HTTPS for you. Store the certificate in a Secret and reference it:
# Self-signed cert for testing
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout tls.key -out tls.crt \
-subj "/CN=myapp.local"
kubectl create secret tls myapp-tls --cert=tls.crt --key=tls.key
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: secure-app
spec:
ingressClassName: nginx
tls:
- hosts:
- myapp.local
secretName: myapp-tls # the TLS Secret
rules:
- host: myapp.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: frontend
port:
number: 80
# HTTPS works now
curl -k https://myapp.local/
# (-k skips certificate verification for self-signed certs)
Tip: In production, use cert-manager to automatically provision and renew TLS certificates from Let's Encrypt. Install cert-manager, create a ClusterIssuer, and add an annotation to your Ingress — certificates are handled automatically.
The nginx Ingress Controller supports many annotations for fine-tuning:
metadata:
annotations:
# Rewrite the URL path before forwarding
nginx.ingress.kubernetes.io/rewrite-target: /
# Force HTTPS redirect
nginx.ingress.kubernetes.io/ssl-redirect: "true"
# Rate limiting
nginx.ingress.kubernetes.io/limit-rps: "10"
# Request body size limit
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
# Timeouts
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
# WebSocket support
nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
# CORS
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-origin: "https://myapp.com"
Gotcha: Annotations are vendor-specific.
nginx.ingress.kubernetes.io/*only works with the nginx controller. Traefik uses different annotations. This non-standard configuration is one reason Gateway API was created.
Gateway API is the successor to Ingress. It's more expressive, role-oriented, and standardized across controllers. As of Kubernetes 1.29+, it's GA for HTTP routing.
| Aspect | Ingress | Gateway API |
|---|---|---|
| Resource types | 1 (Ingress) | Multiple (Gateway, HTTPRoute, GRPCRoute, TCPRoute) |
| Configuration | Vendor annotations | Standard spec fields |
| Role model | One resource for everything | Infra team manages Gateway, app team manages Routes |
| Protocols | HTTP/HTTPS only | HTTP, HTTPS, gRPC, TCP, UDP |
| Header matching | Annotation-dependent | Built-in |
| Traffic splitting | Not supported | Built-in (canary %) |
┌─────────────┐ GatewayClass defines the controller (like IngressClass)
│ GatewayClass│ Managed by cluster admin
└──────┬──────┘
│
┌──────▼──────┐ Gateway defines the listener (ports, TLS, hostnames)
│ Gateway │ Managed by infra/platform team
└──────┬──────┘
│
┌──────▼──────┐ HTTPRoute defines routing rules (paths, headers, backends)
│ HTTPRoute │ Managed by application developers
└─────────────┘
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/standard-install.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: main-gateway
spec:
gatewayClassName: nginx
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: All
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: app-routes
spec:
parentRefs:
- name: main-gateway
hostnames:
- "myapp.local"
rules:
- matches:
- path:
type: PathPrefix
value: /api
backendRefs:
- name: api
port: 80
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: frontend
port: 80
Gateway API has built-in support for canary deployments — something Ingress can't do:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: canary-route
spec:
parentRefs:
- name: main-gateway
rules:
- backendRefs:
- name: app-v1
port: 80
weight: 90 # 90% of traffic
- name: app-v2
port: 80
weight: 10 # 10% of traffic (canary)
No annotations, no vendor-specific hacks — traffic splitting is part of the standard spec.
Should I use Ingress or Gateway API? If you're starting fresh, learn Gateway API — it's the future. If you have existing Ingress resources, they're not going away soon. Both will coexist for years. The concepts (routing, TLS, host/path matching) are the same — only the resource format differs.
Let's deploy two services and route to them with Ingress:
# Backend API
kubectl create deployment api --image=hashicorp/http-echo -- -text="API response"
kubectl expose deployment api --port=80 --target-port=5678
# Frontend
kubectl create deployment web --image=hashicorp/http-echo -- -text="Frontend response"
kubectl expose deployment web --port=80 --target-port=5678
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: demo.local
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api
port:
number: 80
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
kubectl apply -f ingress.yaml
# Add to /etc/hosts (use your cluster IP)
# echo "127.0.0.1 demo.local" | sudo tee -a /etc/hosts
kubectl get ingress
# NAME CLASS HOSTS ADDRESS PORTS AGE
# demo-ingress nginx demo.local localhost 80 10s
# Test the routes
curl http://demo.local/
# Frontend response
curl http://demo.local/api
# API response
# Clean up
kubectl delete ingress demo-ingress
kubectl delete deployment api web
kubectl delete svc api web
Progress through each section in order, or jump to where you need practice.
Practice individual concepts you just learned.
Combine concepts and learn patterns. Each challenge has multiple variants at different difficulties.
/api to one Service, / to anotherapp.com and api.com to different Servicestls sectionPrefix matches path segments, Exact matches exactly