What an Ingress Controller Does (and What It Does Not)
Goal: route external HTTP/HTTPS traffic to the right Kubernetes Service based on hostnames and URL paths.
An Ingress is a Kubernetes API object that describes HTTP routing rules. An Ingress controller is the actual data-plane component (typically a reverse proxy like NGINX, HAProxy, or Envoy) that watches Ingress resources and programs itself to enforce those rules. The Ingress resource by itself does nothing until a controller is installed and selected to act on it.
It helps to separate responsibilities: the Ingress controller terminates connections, matches requests (host/path), and forwards them to Services; your application Pods still handle app logic. Ingress is not a general TCP/UDP load balancer (that is typically handled by other resources or controller-specific features). Also, Ingress does not replace internal service-to-service routing; it is primarily for north-south (client-to-cluster) HTTP(S) traffic.
Choosing and Targeting an Ingress Controller
Key idea: multiple Ingress controllers can coexist in one cluster, but each Ingress must be “claimed” by exactly one controller.
Modern Kubernetes uses IngressClass to select which controller should implement a given Ingress. A controller advertises a controller string (for example, k8s.io/ingress-nginx), and an IngressClass maps a friendly name (like nginx) to that controller. Then an Ingress references the class via spec.ingressClassName.
Continue in our app.
You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.
Or continue reading below...Download the app
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: nginx
spec:
controller: k8s.io/ingress-nginx
When you create an Ingress, you target the controller like this:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example
spec:
ingressClassName: nginx
rules: []
If you omit ingressClassName, behavior depends on cluster defaults and controller configuration (some controllers watch “unclassified” Ingresses; others require an explicit class). For predictable routing, always set ingressClassName.
HTTP Routing Model: Hostnames, Paths, and Backends
Mental model: an Ingress rule is a set of match conditions (host + path) and a backend (Service + port) to forward to.
At runtime, the controller evaluates each incoming request: it reads the Host header (or SNI for TLS), then compares the request path (like /api/v1/users) against the configured path rules. If a match is found, it proxies the request to the selected Service port.
In Kubernetes networking.k8s.io/v1, each rule has a host and an http.paths list. Each path entry has a path, a pathType, and a backend Service reference.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web-ingress
spec:
ingressClassName: nginx
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
Path Matching: Prefix vs Exact (and Why It Matters)
Practical impact: path matching determines which backend receives a request, especially when multiple routes overlap.
Kubernetes defines three path types: Exact, Prefix, and ImplementationSpecific. In practice, you should prefer Exact and Prefix because they are portable and predictable.
- Exact: matches only the exact path string.
/loginmatches/loginbut not/login/or/login/reset. - Prefix: matches based on a URL path prefix, with path element boundaries.
/apimatches/api,/api/, and/api/v1. - ImplementationSpecific: controller-defined behavior. Some controllers treat it like a regex or like a prefix; portability suffers.
When multiple paths could match, controllers typically choose the “most specific” match (for example, the longest matching path). But the exact tie-breaking rules can vary by controller, especially when mixing ImplementationSpecific with other types. Keep your rules unambiguous: use Exact for single endpoints and Prefix for route trees.
Hostname Routing and Virtual Hosts
Use case: serve multiple domains (or subdomains) from one external IP / load balancer.
Ingress supports name-based virtual hosting: each rule can specify a different host. The controller uses the HTTP Host header (and for HTTPS, usually SNI during TLS negotiation) to select the correct virtual host configuration.
Example: route app.example.com to a frontend Service and api.example.com to an API Service.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: vhosts
spec:
ingressClassName: nginx
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: frontend
port:
number: 80
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api
port:
number: 8080
For local testing, you can map these hostnames to the Ingress endpoint IP in your /etc/hosts (or Windows hosts file). In real environments, you create DNS records (A/AAAA or CNAME) pointing to the Ingress controller’s external address.
Path-Based Routing: One Host, Many Apps
Use case: serve multiple applications under one domain, such as example.com with / for frontend and /api for API.
This is common when you want a single public hostname but separate backends. The controller matches the path and forwards accordingly.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: paths
spec:
ingressClassName: nginx
rules:
- host: example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api
port:
number: 8080
- path: /
pathType: Prefix
backend:
service:
name: frontend
port:
number: 80
Notice the ordering in the YAML: Kubernetes does not guarantee evaluation order based on list position. Controllers generally pick the most specific match, which is why /api will win over /. Still, keep your paths clean and avoid ambiguous overlaps like /api and /api-v2 unless you are certain how your controller matches prefixes.
Step-by-Step: Create a Host + Path Ingress You Can Test
Objective: expose two Services behind one hostname using path rules, then verify routing with curl.
Step 1: Confirm the controller is running and has an address
List Ingress controller Pods and the Service that exposes them (names vary by installation):
kubectl get pods -n ingress-nginx
kubectl get svc -n ingress-nginx
You are looking for an external endpoint. Depending on your environment, it may be an external IP, a node port, or a local address. Also check that your IngressClass exists:
kubectl get ingressclass
Step 2: Create (or identify) two backend Services
You need two Services that respond differently so you can see routing work. For example, frontend on port 80 and api on port 8080. Verify they exist:
kubectl get svc frontend api
If you are using a namespace for your app, remember that Ingress routes to Services in the same namespace as the Ingress object.
Step 3: Apply an Ingress with host and path rules
Create an Ingress like the following (adjust names and ports):
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-routing
spec:
ingressClassName: nginx
rules:
- host: demo.example.test
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api
port:
number: 8080
- path: /
pathType: Prefix
backend:
service:
name: frontend
port:
number: 80
Apply it and inspect status:
kubectl apply -f ingress.yaml
kubectl get ingress example-routing
kubectl describe ingress example-routing
In the describe output, confirm that the rules are recognized and that the controller has assigned an address (or at least has processed the object without errors).
Step 4: Make the hostname resolve to the Ingress endpoint
For a quick test, map demo.example.test to the controller’s reachable IP. If you have an external IP, add a hosts entry. If you only have a node port, you can still test by sending the Host header explicitly with curl.
Example using curl with an explicit Host header (replace INGRESS_IP):
curl -i http://INGRESS_IP/ -H 'Host: demo.example.test'
curl -i http://INGRESS_IP/api -H 'Host: demo.example.test'
You should see different responses (or at least different upstream behavior) depending on whether you hit / or /api.
Default Backends and 404 Behavior
Expectation setting: if no host/path rule matches, you will typically get a 404 from the controller, not from your app.
Ingress controllers usually have a “default backend” that serves a generic 404 page. This is useful because it prevents accidental exposure of an unintended Service. If you want a custom default response, some controllers allow you to configure a default backend Service globally, but this is controller-specific and not part of the core Ingress API.
Within a single Ingress, you can approximate a “catch-all” by using a rule with no host (meaning it matches all hosts) and a / prefix path. Be careful: a catch-all can unintentionally route traffic for unknown hostnames to your app, which may be undesirable.
Rewrites, Trailing Slashes, and “/api” Gotchas
Common problem: your backend expects paths without the external prefix, but the Ingress forwards the full path.
Suppose your API service expects /v1/users, but externally you want /api/v1/users. With plain Ingress path routing, the backend receives /api/v1/users. Many controllers support path rewriting via annotations or custom resources, but the mechanism is not standardized in the Ingress spec.
Because rewrite configuration is controller-specific, treat it as an implementation detail: document it, test it, and keep it consistent across environments. If you need portability, consider designing your services to accept the external path structure (or use a dedicated API gateway layer that standardizes rewriting behavior).
Trailing slashes can also cause surprises. With Exact paths, /api and /api/ are different. With Prefix, both typically match. Decide on a canonical URL format and enforce it at the application or edge (redirect /api to /api/, for example) if consistency matters.
TLS Hosts and HTTPS Routing (Conceptual View)
What changes with HTTPS: the controller terminates TLS and selects the correct certificate based on the requested hostname.
Ingress supports TLS configuration via spec.tls, where you list hostnames and a Secret containing the certificate and key. The controller uses SNI to pick the right certificate for a given hostname, then applies the same host/path routing rules after decryption.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: tls-example
spec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
secretName: app-example-com-tls
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: frontend
port:
number: 80
Even if you are not ready to manage real certificates, it is important to understand that hostname routing and TLS are linked: for HTTPS, the hostname must be known early (SNI) to present the correct certificate, which is why accurate DNS and host configuration matters.
Operational Checks: Debugging Host and Path Rules
Approach: validate the Kubernetes objects, then validate the controller’s interpretation, then validate the request.
- Check the Ingress object:
kubectl describe ingressfor events like “no IngressClass found” or “service not found”. - Check endpoints: if the Service has no ready endpoints, the controller may return 502/503. Inspect with
kubectl get endpointsorkubectl get endpointSlice. - Check controller logs: most controllers log configuration reloads and routing decisions. Look for warnings about invalid paths, missing Services, or rejected annotations.
- Test with explicit Host header: when DNS is not set up, use
curl -H 'Host: ...'to ensure you are testing the correct virtual host. - Verify path matching: test both
/apiand/api/, and a deeper path like/api/health, to confirm prefix behavior.
Design Patterns for Clean Ingress Rules
Pattern 1: One Ingress per application boundary: keep a single Ingress resource for a domain like app.example.com and route only that app’s paths there. This simplifies ownership and reduces accidental coupling between teams.
Pattern 2: One Ingress per hostname: if you have many paths, group them under the same host in one Ingress so you can reason about the full routing table for that domain in one place.
Pattern 3: Separate “edge” and “internal” concerns: use Ingress only for edge routing (host/path). Keep authentication, rate limiting, and advanced traffic shaping in a dedicated gateway or mesh layer if you need consistent behavior across controllers and environments.
Pattern 4: Avoid ImplementationSpecific unless necessary: if you rely on regex paths or special matching, you may lock yourself into one controller. Prefer explicit Prefix/Exact and application-friendly URL structures.