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.

Core Takeaway
An Ingress is only a static routing specification stored in etcd. Nothing actually routes until you run an Ingress Controller—an active daemon (like NGINX, Traefik, or Envoy) that translates those declarations into actual proxy configs.

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 hits Node-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:

1. User Client Browser (e.g. app.example.com/checkout)
↓ Public DNS resolves to External IP
2. Cloud LoadBalancer & Ingress Controller (NGINX / Envoy)
↓ Controller evaluates Host & Path match rules
3a. Match: /api/*
Service: api-svc:80
3b. Match: /static/*
Service: web-svc:80
↓ Routes straight to active endpoint IP
4. Selected Backend Pod Container (e.g. 10.244.2.45:8080)

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:

YAMLingress-production.yaml
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. /api will match /api, but will not match /api/ or /api/v1.
  • Prefix: Matches by URL path prefixes separated by /. /api matches /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:

BASHCreate TLS 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)

Gotcha 1: The Trailing Slash & URL Rewrite Trap

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.

Gotcha 2: The 1MB Upload Limit (413 Payload Too Large)

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.

Gotcha 3: IngressClass Missing in K8s 1.18+

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

Can a single Ingress Controller handle multiple namespaces?
Yes. By default, controllers like 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.
What happens if two Ingress resources define the exact same domain and path?
This is an Ingress collision. The controller will process whichever rule it loads first (often lexicographical or based on creation timestamp) and ignore the duplicate, or log a warning in the controller pod logs. The second service simply will not receive traffic.
Can Ingress route non-HTTP traffic like MySQL or raw TCP/UDP streams?
Standard Ingress is strictly a Layer 7 (HTTP/HTTPS) specification. While some controllers (like NGINX) offer custom ConfigMaps to proxy specific TCP/UDP ports, raw Layer 4 traffic is usually better handled via standard type: LoadBalancer services or the newer TCPRoute / UDPRoute in the Gateway API.
Why does my Ingress show 504 Gateway Timeout?
A 504 indicates the controller reached the backend pod, but the pod took too long to answer. Check your application logs for hung database queries, memory thrashing, or increase the proxy timeout annotation (e.g., nginx.ingress.kubernetes.io/proxy-read-timeout: "120").
Do I still need a Cloud LoadBalancer if I use Ingress?
Yes, but you only need one. The Ingress Controller itself sits behind a single cloud LoadBalancer. All incoming traffic lands on that entry point, and the controller routes requests internally to your dozens of microservices, cutting your load balancer costs significantly.
What is the difference between pathType Prefix and Exact?
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

Popular posts from this blog

React Performance Optimization: Profiling, Reconciliation, and Rendering Boundaries

Mastering React Icons: Installation, Customization, and Best Practices (2026 Guide)

How to Configure Webpack 5 with React from Scratch (2026 Guide)