Kubernetes Course
Ingress Rules
The Ingress controller is running. Now you write the rules that tell it where to send traffic. This lesson covers every routing pattern you'll use in production — host-based routing, path-based routing, TLS termination, rewrite rules, and the annotations that give you rate limiting, authentication, and custom headers without touching your application code.
The Anatomy of an Ingress Object
An Ingress object is a set of routing rules. It lives in a namespace, references ClusterIP Services in that same namespace, and tells the Ingress controller: "when a request comes in matching this host and path, forward it to this Service on this port." Everything else — TLS termination, header manipulation, rate limiting — is layered on top via annotations or additional spec fields.
| Field | Purpose | Required? |
|---|---|---|
| spec.ingressClassName | Which Ingress controller handles this object. Must match an existing IngressClass name. | Yes (or use the default class) |
| spec.tls | TLS certificates — which hosts get HTTPS and which Secret holds the cert/key. | Optional (HTTP-only without it) |
| spec.rules | List of routing rules — host + path → Service backend combinations. | Yes (at least one rule) |
| spec.defaultBackend | Catch-all backend for requests that don't match any rule — returns a 404 page or custom error. | Optional |
Path-Based Routing
Path-based routing is the most common Ingress pattern for API gateways and microservices. One hostname, multiple paths, each path routes to a different service. Users see a unified API at api.company.com while behind the scenes, requests are distributed to completely separate microservices.
The scenario: Your e-commerce platform exposes a single API hostname to mobile apps: api.shop.com. Behind that hostname live four independent microservices: order management, product catalogue, user accounts, and the payment processor. Each team owns their service and their Ingress path. You're writing the Ingress manifest that routes all four.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-gateway # Ingress object name
namespace: production # Must be in the same namespace as the target Services
annotations:
nginx.ingress.kubernetes.io/use-regex: "true" # Enable regex in path matching
spec:
ingressClassName: nginx # Which controller handles this — matches IngressClass name
rules:
- host: api.shop.com # Only match requests with this Host header
http:
paths:
- path: /orders(/|$)(.*) # Match /orders, /orders/, /orders/123, etc.
pathType: ImplementationSpecific # Allows regex — controller-specific behaviour
backend:
service:
name: order-svc # Route to this ClusterIP Service
port:
number: 80
- path: /products(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: product-svc
port:
number: 80
- path: /users(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: user-svc
port:
number: 80
- path: /payments(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: payment-svc
port:
number: 80
- path: / # Catch-all: anything not matched above
pathType: Prefix # Prefix: matches any path starting with /
backend:
service:
name: frontend-svc # Serve the frontend SPA for unmatched paths
port:
number: 80
$ kubectl apply -f api-gateway-ingress.yaml
ingress.networking.k8s.io/api-gateway created
$ kubectl get ingress -n production
NAME CLASS HOSTS ADDRESS PORTS AGE
api-gateway nginx api.shop.com a9f8e1234567.elb.us-east-1.amazonaws.com 80 8s
$ kubectl describe ingress api-gateway -n production
Name: api-gateway
Namespace: production
Address: a9f8e1234567.elb.us-east-1.amazonaws.com
Ingress Class: nginx
Rules:
Host Path Backends
---- ---- --------
api.shop.com /orders(/|$)(.*) order-svc:80 (10.244.1.8:8080,10.244.2.9:8080)
/products(/|$)(.*) product-svc:80 (10.244.1.11:8080)
/users(/|$)(.*) user-svc:80 (10.244.2.4:8080,10.244.3.1:8080)
/payments(/|$)(.*) payment-svc:80 (10.244.1.9:8080)
/ frontend-svc:80 (10.244.2.12:3000)What just happened?
The three pathType values — Exact: matches only that exact path string, nothing more. Prefix: matches the path and everything after it. A Prefix rule for /orders matches /orders, /orders/, and /orders/123/items. ImplementationSpecific: interpreted by the controller — ingress-nginx uses it for regex patterns.
Path ordering matters — ingress-nginx evaluates paths from most-specific to least-specific, not in YAML order. More specific paths (longer strings, regex) take priority over less specific ones. The catch-all / Prefix rule is always last. When debugging routing issues, check the Backends column in kubectl describe ingress — it shows the actual Pod IPs that each path resolves to.
ADDRESS column — This shows the load balancer DNS name or IP that this Ingress is served behind. It comes from the LoadBalancer Service IP of the ingress controller. All Ingress objects in the cluster share this same address — they're all routed through the same entry point.
Host-Based Routing
Host-based routing sends different requests to different services based on the Host HTTP header — effectively virtual hosting. Requests for api.shop.com go to the API, requests for admin.shop.com go to the admin dashboard, requests for docs.shop.com go to the documentation site — all through the same load balancer IP.
The scenario: Your company runs three web properties on the same Kubernetes cluster: the public API, an internal admin portal, and a developer documentation site. All three should be accessible externally but through different hostnames. You also want TLS on all three — with certificates managed by cert-manager.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: multi-host-ingress
namespace: production
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod" # cert-manager: auto-provision TLS certs
nginx.ingress.kubernetes.io/ssl-redirect: "true" # Force HTTPS on all three hosts
spec:
ingressClassName: nginx
tls:
- hosts:
- api.shop.com # Include this hostname in the TLS certificate
secretName: api-shop-tls # cert-manager will create/renew this Secret automatically
- hosts:
- admin.shop.com
secretName: admin-shop-tls
- hosts:
- docs.shop.com
secretName: docs-shop-tls
rules:
- host: api.shop.com # Rule 1: API traffic
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-svc
port:
number: 80
- host: admin.shop.com # Rule 2: Admin portal — separate host, separate service
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: admin-svc
port:
number: 80
- host: docs.shop.com # Rule 3: Documentation site
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: docs-svc
port:
number: 80
$ kubectl apply -f multi-host-ingress.yaml ingress.networking.k8s.io/multi-host-ingress created $ kubectl get ingress -n production NAME CLASS HOSTS ADDRESS PORTS AGE multi-host-ingress nginx api.shop.com,admin.shop.com,docs.shop.com a9f8e1234567.elb.us-east-1.amazonaws.com 80, 443 12s $ kubectl get certificate -n production NAME READY SECRET AGE api-shop-tls True api-shop-tls 45s ← cert-manager provisioned and ready admin-shop-tls True admin-shop-tls 44s docs-shop-tls True docs-shop-tls 44s
What just happened?
cert-manager integration — The cert-manager.io/cluster-issuer: "letsencrypt-prod" annotation tells cert-manager (a separate cluster component) to automatically request Let's Encrypt certificates for each TLS host listed. cert-manager handles the ACME challenge, stores the certificate in the named Secret, and automatically renews it before expiry. You never manually manage certificates.
TLS and rules cross-reference by hostname — The spec.tls section and spec.rules are linked by hostname. When a request for api.shop.com arrives on port 443, the ingress controller looks up the TLS entry for that host, retrieves the certificate from the Secret, and performs the TLS handshake before routing to the backend. If the hostnames don't match between tls and rules, the certificate won't be served for those requests.
PORTS: 80, 443 — Now that TLS is configured, the ingress exposes both HTTP (for the redirect) and HTTPS. The ssl-redirect: true annotation means HTTP connections get a 308 redirect to HTTPS automatically. Your users always end up on HTTPS.
URL Rewriting
Sometimes the path your users or other services use externally doesn't match the path the backend application expects. URL rewriting lets the Ingress controller transform the path before forwarding the request — without any changes to the application.
The scenario: Your mobile app calls api.shop.com/orders/v1/... but the order service's internal API is at /v1/... — it has no /orders prefix. You need the Ingress to strip the /orders prefix before forwarding the request to the order service.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: rewrite-ingress
namespace: production
annotations:
nginx.ingress.kubernetes.io/use-regex: "true"
nginx.ingress.kubernetes.io/rewrite-target: /$2
# rewrite-target: replaces the matched path with /$2 (capture group 2 from the path regex)
# Combined with the path below:
# Request: /orders/v1/items → capture group 2 = v1/items → forwarded as /v1/items
# Request: /orders/ → capture group 2 = (empty) → forwarded as /
# Request: /orders → capture group 2 = (empty) → forwarded as /
spec:
ingressClassName: nginx
rules:
- host: api.shop.com
http:
paths:
- path: /orders(/|$)(.*) # Capture groups: group 1 = / or end, group 2 = remainder
pathType: ImplementationSpecific
backend:
service:
name: order-svc
port:
number: 80
- path: /products(/|$)(.*) # Same pattern for products
pathType: ImplementationSpecific
backend:
service:
name: product-svc
port:
number: 80
$ kubectl apply -f rewrite-ingress.yaml ingress.networking.k8s.io/rewrite-ingress created External request: GET https://api.shop.com/orders/v1/items?status=pending Order service sees: GET /v1/items?status=pending External request: GET https://api.shop.com/products/categories Product service sees: GET /categories External request: GET https://api.shop.com/orders/ Order service sees: GET /
What just happened?
Capture group magic — The path regex /orders(/|$)(.*) has two capture groups. Group 1 ($1) captures the slash or end-of-string after /orders. Group 2 ($2) captures everything after that. The rewrite-target: /$2 annotation replaces the entire matched path with just group 2, effectively stripping the /orders prefix.
Query strings are preserved — The rewrite only affects the path component of the URL. Query strings (?status=pending) are passed through unchanged. This is the expected behaviour — the backend service receives the full query string as the caller specified it.
Production Annotations: Rate Limiting, Auth, and More
The ingress-nginx annotation system is where the real power lives. You can add rate limiting, basic auth, IP whitelisting, custom headers, CORS configuration, and connection limits — all without touching your application code, all in the Ingress manifest.
The scenario: Your payment API is being targeted by automated scanners. You need to add rate limiting to prevent abuse, restrict access to known partner IP ranges, and add CORS headers for your frontend's cross-origin requests — all without a deploy of the payment service itself.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: payment-api-ingress
namespace: production
annotations:
# --- Rate Limiting ---
nginx.ingress.kubernetes.io/limit-rps: "20"
# limit-rps: max 20 requests per second per client IP
# Clients exceeding this receive 503 Too Many Requests
nginx.ingress.kubernetes.io/limit-connections: "10"
# limit-connections: max 10 simultaneous connections per client IP
nginx.ingress.kubernetes.io/limit-burst-multiplier: "5"
# Burst up to 5× the rate limit before throttling kicks in
# Allows short legitimate spikes without dropping legitimate requests
# --- IP Allowlisting ---
nginx.ingress.kubernetes.io/whitelist-source-range: "203.0.113.0/24,198.51.100.0/24"
# Only allow traffic from these CIDRs — all other IPs get 403 Forbidden
# Useful for partner APIs, admin portals, internal tooling
# --- CORS (Cross-Origin Resource Sharing) ---
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-origin: "https://shop.com,https://admin.shop.com"
nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST, PUT, DELETE, OPTIONS"
nginx.ingress.kubernetes.io/cors-allow-headers: "Authorization, Content-Type, X-Request-ID"
nginx.ingress.kubernetes.io/cors-max-age: "86400"
# cors-max-age: browsers cache the preflight response for 24 hours (86400 seconds)
# Reduces OPTIONS preflight requests significantly
# --- Custom Response Headers ---
nginx.ingress.kubernetes.io/configuration-snippet: |
more_set_headers "X-Request-ID: $request_id";
more_set_headers "Cache-Control: no-store";
# configuration-snippet: inject arbitrary nginx config into the location block
# Use for per-Ingress config that isn't covered by a dedicated annotation
# --- Timeouts (per-Ingress override of controller defaults) ---
nginx.ingress.kubernetes.io/proxy-read-timeout: "120"
# Override the controller-wide read timeout for this specific service
# Payment processing can take longer — give it 2 minutes
spec:
ingressClassName: nginx
tls:
- hosts:
- api.shop.com
secretName: api-shop-tls
rules:
- host: api.shop.com
http:
paths:
- path: /payments
pathType: Prefix
backend:
service:
name: payment-svc
port:
number: 80
$ kubectl apply -f payment-api-ingress.yaml
ingress.networking.k8s.io/payment-api-ingress created
$ curl -I https://api.shop.com/payments/health
HTTP/2 200
x-request-id: a4f8b2d1e5c6g7h8
cache-control: no-store
access-control-allow-origin: https://shop.com
$ curl -I -H "Origin: https://evil.com" https://api.shop.com/payments/health
HTTP/2 200
(no Access-Control-Allow-Origin header — evil.com not in allowed list)
$ for i in {1..25}; do curl -s -o /dev/null -w "%{http_code} " https://api.shop.com/payments/health; done
200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 503 503 503 503 503What just happened?
Rate limiting at the edge — The burst test shows 20 successful requests followed by 503s. The rate limiter allowed the burst multiple (5×20=100... but it was already close to the limit when the burst was absorbed). This protection happens entirely in nginx — the payment service never receives the throttled requests. No load, no processing, no database queries from the abusive client.
CORS without application changes — Browsers making cross-origin requests from shop.com get the correct CORS headers. Requests from evil.com get no CORS header — the browser blocks them. The payment service code has zero CORS logic — it's entirely handled at the ingress layer.
configuration-snippet scope — The configuration-snippet annotation injects nginx config into the location block for this specific Ingress path only. It doesn't affect other Ingress rules. Use it for fine-grained control that standard annotations don't cover. Be careful: malformed nginx syntax here will cause the controller to emit an error event and potentially revert to the last valid config.
The Routing Decision Map
Here's how ingress-nginx evaluates an incoming request against all active Ingress rules — the exact precedence order:
ingress-nginx Routing Precedence
spec.rules[].host matches the request's Host header exactly. Wildcard hosts (*.shop.com) are supported but lower priority than exact matches.spec.defaultBackend (if defined) or the controller's global default backend — typically returning a 404 "default backend - 404" response.Debugging Ingress Routing
The scenario: A request to api.shop.com/orders/123 is returning a 404 from the ingress default backend instead of hitting the order service. Something is wrong with the routing.
kubectl describe ingress api-gateway -n production
# Full rule table: shows which paths map to which backends and their current endpoints
# If a backend shows "no endpoints" → the ClusterIP Service has no matching Pods
# If a path is missing entirely → check the YAML for typos
kubectl get ingress api-gateway -n production -o yaml
# Full YAML view: useful for checking that annotations were applied correctly
kubectl logs -l app.kubernetes.io/component=controller -n ingress-nginx --tail=100
# Controller logs: shows every request processed and any configuration errors
# Look for: "error obtaining endpoints" (service has no pods)
# "could not get service" (service name typo or wrong namespace)
# "configuration changed" (shows when a new Ingress rule was picked up)
kubectl exec -it -n ingress-nginx deploy/ingress-nginx-controller -- \
nginx -T | grep -A10 "server_name api.shop.com"
# nginx -T: dump the full nginx configuration the controller is running
# Grep for your hostname to see the exact location blocks it generated
# This is the ground truth — if the nginx config looks wrong, the Ingress is broken
$ kubectl logs -l app.kubernetes.io/component=controller -n ingress-nginx --tail=20 W0310 10:44:31 Unexpected error obtaining information about the endpoints for service "production/order-svc": no object matching key "production/order-svc" in local store $ kubectl get svc -n production | grep order (no output — service doesn't exist!) $ kubectl describe ingress api-gateway -n production | grep orders api.shop.com /orders(/|$)(.*) order-svc:80 (<error: endpoints "order-svc" not found>) (root cause: the order Service was accidentally deleted — controller can't find endpoints) $ kubectl apply -f order-service.yaml service/order-svc created $ kubectl logs -l app.kubernetes.io/component=controller -n ingress-nginx --tail=5 I0310 10:45:12 Configuration changes detected, backend reload required I0310 10:45:12 Backend successfully reloaded
What just happened?
nginx -T is the ultimate truth — When standard debugging doesn't reveal the issue, dumping the actual nginx config the controller generated shows you exactly what nginx is doing with your Ingress rules. Mismatched paths, wrong proxy_pass targets, and missing location blocks are all visible here. It's verbose but definitive.
Controller logs are chatty but useful — The ingress-nginx controller logs every reload, every configuration error, and every endpoint discovery failure. During debugging, --tail=100 and grepping for your service name is often faster than a full describe.
Auto-reload on fix — The moment the missing Service was re-created, the controller detected the change via its API watch and automatically reloaded nginx. The "Backend successfully reloaded" log message confirms the fix took effect. No manual intervention needed — the controller is continuously reconciling.
Teacher's Note: Keep Ingress objects small and focused
There are two valid schools of thought on Ingress organisation. The first: one big Ingress object per cluster or per namespace with all routes in it. Easy to see everything at a glance, but any change touches the shared object and risks merge conflicts in Git. The second: one Ingress per service team, each team owns their Ingress rules. Scales better with many teams, clean ownership, no conflicts.
The second approach is almost always better past 5 services. Each team deploys their Ingress rule alongside their Service and Deployment in the same PR. The ingress-nginx controller merges all Ingress objects in the namespace into one nginx config transparently. Teams don't need to know about each other's routes.
One annotation to be careful with: nginx.ingress.kubernetes.io/configuration-snippet. It injects raw nginx config and can break the entire controller if the syntax is wrong. Some security-conscious platform teams disable this annotation entirely (--allow-snippet-annotations=false on the controller) to prevent teams from accidentally or maliciously injecting arbitrary nginx directives.
Practice Questions
1. External clients call /api/v1/users but your user service expects requests at /v1/users. Which ingress-nginx annotation strips the /api prefix before forwarding to the backend?
2. Which annotation on an Ingress object tells cert-manager to automatically provision and renew a Let's Encrypt TLS certificate for the listed hosts?
3. You suspect a routing rule isn't being applied correctly. What command dumps the full nginx configuration the ingress-nginx controller is currently running — showing the exact generated location blocks for all Ingress rules?
Quiz
1. An Ingress has two rules for the same host: pathType: Exact, path: /orders/new and pathType: Prefix, path: /orders. A request arrives for /orders/new. Which rule wins?
2. An Ingress has rules for api.shop.com and admin.shop.com. A client sends a request directly to the load balancer IP address without a Host header. What happens?
3. You add the nginx.ingress.kubernetes.io/enable-cors: "true" annotation to an Ingress. What does this mean for your backend application code?
Up Next · Lesson 35
DNS in Kubernetes
How CoreDNS resolves service names to IPs inside the cluster, the full DNS search path that makes short names work, and the patterns for cross-namespace service discovery.