Kubernetes Ingress: Architecture, Routing Rules, and Real-World Setup
Kubernetes Ingress: Architecture, Routing Rules, and Real-World Setup
You have three separate microservices deployed inside your cluster. A customer types api.yourcompany.com/checkout into their browser, expecting their cart to process in under 200 milliseconds. How does that packet actually find its way through the cloud provider's network, pierce the cluster perimeter, bypass 40 irrelevant pods, and land directly inside the checkout container on port 8080?
If you ask three engineers, you might get three answers involving NodePorts, cloud LoadBalancers, or Ingress resources. But in real-world clusters, relying solely on basic services quickly runs into practical bottlenecks: running out of random high ports, paying hundreds of dollars every month for individual cloud load balancers, or lacking basic path-based routing.
That is where Kubernetes Ingress comes in. Let’s break down how Ingress actually works under the hood, how to build routing rules that hold up in production, and how to avoid the common networking snags that cause unexpected 502 Bad Gateway and 404 Not Found errors during rollout.
Why Simple Services Fall Short for Edge Routing
To understand why Ingress exists, consider the built-in ways Kubernetes exposes pods:
- ClusterIP: The default. Gives you an internal virtual IP accessible only from inside the cluster. Completely invisible to the outside world.
- NodePort: Opens a dedicated port (usually in the range
30000–32767) on every worker node in your cluster. If someone hitsNode-IP:31254, kube-proxy forwards it to your pod. It works, but managing port numbers across dozens of apps is messy, and exposing high, non-standard ports to public users looks unprofessional. - LoadBalancer: Asks your cloud provider (AWS, GCP, Azure, DigitalOcean) to spin up an external load balancer (such as an AWS NLB/ALB) pointing directly to your NodePort.
The problem with using type: LoadBalancer for every service is simple economics and architecture. If you run 25 microservices and create a LoadBalancer service for each one, your cloud provider provisions 25 distinct load balancers. At ~$20–$30 base price per balancer on most clouds, you are spending $500–$750 every month just to get packets into your cluster, before factoring in data transfer charges.
| Method | OSI Layer | External IP Count | Path / Host Routing | Typical Cost Profile |
|---|---|---|---|---|
| ClusterIP | Layer 4 (TCP/UDP) | 0 (Internal only) | No | Free |
| NodePort | Layer 4 (TCP/UDP) | All Nodes (High ports) | No | Free (Manual DNS needed) |
| LoadBalancer | Layer 4 / Layer 7 | 1 per Service | Cloud-specific only | $$$ (Scales with services) |
| Ingress | Layer 7 (HTTP/HTTPS) | 1 for Entire Cluster | Native (Host + Path) | $ (Single Cloud LB point) |
The Two Pillars: Ingress Resource vs. Ingress Controller
This is the single most common stumbling block for engineers new to Kubernetes networking. You apply an Ingress YAML file using kubectl apply -f ingress.yaml, check kubectl get ingress, see an empty address column, and wonder why nothing works.
Kubernetes splits this mechanism into two distinct entities:
1. The Ingress Resource
A declarative configuration object. It lists your desired domains, URL subpaths, backend service targets, and SSL certificate secrets. It has no networking engine behind it by default; it is purely data in Kubernetes' etcd database.
2. The Ingress Controller
A running application (typically deployed as a Deployment or DaemonSet) running reverse proxy software like NGINX, HAProxy, Envoy, or Traefik. It continuously watches the Kubernetes API server for Ingress objects and dynamically updates its internal routing table.
Without the controller, the resource is a dormant record. Once the controller starts, it inspects your rules, allocates an entry point (usually through a single cloud LoadBalancer backing the controller itself), and starts directing external calls to your pods.
How Traffic Moves Through Ingress (Step-by-Step)
Let’s follow a client request to see how the pieces fit together during runtime:
Service: api-svc:80
Service: web-svc:80
Notice an important nuance at step 4: while your Ingress manifest points to a Kubernetes Service name, modern controllers like ingress-nginx bypass the ClusterIP virtual IP and kube-proxy iptables entirely. The controller queries the Kubernetes Endpoints/EndpointSlices API directly and proxies the connection straight to the Pod IP (10.244.x.x). This reduces latency by eliminating an extra network hop.
Practical Manifest: Path-Based and Host-Based Routing
Here is a complete, production-style manifest showing how to route multiple subdomains and paths through one controller using standard networking.k8s.io/v1:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: primary-ingress
namespace: production
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/proxy-body-size: "20m"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
ingressClassName: nginx
tls:
- hosts:
- dashboard.example.com
- api.example.com
secretName: example-tls-cert
rules:
- host: dashboard.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: dashboard-frontend-svc
port:
number: 80
- host: api.example.com
http:
paths:
- path: /users(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: user-service
port:
number: 8080
- path: /orders(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: order-service
port:
number: 3000
Understanding Path Types
The pathType field is critical. Choosing the wrong one is a frequent cause of 404s:
Exact: Matches the URL path exactly, case-sensitively./apiwill match/api, but will not match/api/or/api/v1.Prefix: Matches by URL path prefixes separated by/./apimatches/api,/api/, and/api/v1/users, but will not match/apidocs.ImplementationSpecific: Relies on the underlying controller. This is required when using regular expressions in controllers like NGINX.
Handling TLS / HTTPS Termination
Terminating SSL certificates inside every individual pod wastes compute resources and creates configuration sprawl. Ingress handles this cleanly at the edge.
When you provide a TLS block in the Ingress manifest, the controller decrypts incoming HTTPS connections on port 443, checks the SNI (Server Name Indication) header, and sends plain HTTP traffic to your backend pods across the internal overlay network (unless strict zero-trust mTLS is configured).
To store your certificate manually, place your base64-encoded cert and private key into a Kubernetes Secret:
kubectl create secret tls example-tls-cert \
--cert=path/to/tls.crt \
--key=path/to/tls.key \
-n production
In automated setups, pair Ingress with cert-manager. By including the annotation cert-manager.io/cluster-issuer: letsencrypt-prod, cert-manager observes the Ingress resource, triggers an ACME challenge with Let's Encrypt, generates the secret automatically, and handles renewal every 60 days without manual intervention.
Popular Ingress Controllers Compared
You aren't locked into a single implementation. The ecosystem provides several options depending on your operational focus:
| Controller | Underlying Proxy | Key Strengths | Best Fit For |
|---|---|---|---|
| ingress-nginx (Community) | NGINX Open Source | Massive community support, familiar annotations, extensive documentation | Standard production workloads, bare-metal clusters |
| Traefik | Traefik (Go) | Native dynamic configuration, built-in Let's Encrypt support, modern UI dashboard | Fast-moving microservices, environments wanting zero-reload config updates |
| Emissary-Ingress / Contour | Envoy | High throughput, low memory footprint, robust observability and gRPC support | Large-scale clusters, modern service mesh integration |
| HAProxy Ingress | HAProxy | Extreme connection handling speed, rock-solid stability under heavy load | High-volume API gateways, low-latency transaction processing |
Common Production Mistakes (And What Actually Happens)
A user requests example.com/catalog. Your backend API expects routes defined at the root (/). Without a rewrite rule, the request arrives at your container as GET /catalog, returning a fast 404.
If you apply nginx.ingress.kubernetes.io/rewrite-target: / naively without regex capture groups, every single URL is rewritten to root. /catalog/item/42 becomes /, completely losing the subpath. Use regex capture groups (/$2) to forward paths cleanly.
By default, NGINX Ingress sets client_max_body_size to 1 megabyte. The moment a user attempts to upload a 3MB profile picture or CSV file, NGINX rejects it with an immediate 413 Request Entity Too Large. Fix this by adding nginx.ingress.kubernetes.io/proxy-body-size: "25m" to the Ingress metadata annotations.
Older tutorials rely on the deprecated annotation kubernetes.io/ingress.class: "nginx". On newer Kubernetes versions, if you do not specify the spec.ingressClassName: nginx field, the controller will ignore the resource entirely, leaving the Address field permanently blank.
Ingress vs. The Kubernetes Gateway API
As Kubernetes matured, teams hit the architectural ceilings of the original Ingress spec. Ingress combines host rules, path routes, TLS setup, and proxy-specific flags into a single flat file. In large organizations, this creates conflicts: cluster administrators who manage domains and certificates step on the toes of application developers who only want to register a path.
The Gateway API addresses this with role-oriented resources:
GatewayClass: Managed by infrastructure providers to define the controller type.Gateway: Managed by cluster admins to configure IP addresses, ports, and TLS secrets.HTTPRoute: Managed by application developers to route individual paths to services.
Should you scrap your Ingress setups today? In practical terms: no. Ingress remains fully supported, simpler to grasp for straightforward apps, and supported by almost every tool in the ecosystem. However, for greenfield enterprise clusters requiring advanced traffic splitting, canary deployments, or header-based routing without relying on controller-specific annotations, the Gateway API is the long-term successor.
Frequently Asked Questions
ingress-nginx watch Ingress resources across all namespaces in your cluster. If an Ingress in namespace payments and an Ingress in namespace marketing define non-overlapping hosts, the controller handles both smoothly. You can scope a controller to a single namespace using the --watch-namespace flag.type: LoadBalancer services or the newer TCPRoute / UDPRoute in the Gateway API.nginx.ingress.kubernetes.io/proxy-read-timeout: "120").Prefix matches any URL starting with that segment (split by /), meaning /api matches /api/v1. Exact matches only the specified string, so /api will reject /api/v1 with a 404.Summary
Kubernetes Ingress isn't a magical piece of software—it is a standardized translation layer that sits between human-readable routing declarations and robust edge reverse proxies. By moving your external routing from raw NodePorts and sprawling cloud load balancers to an Ingress Controller, you consolidate your ingress footprint to a single IP, automate TLS certificates, and keep application routing managed alongside the rest of your cluster code.
Comments