Kubernetes Ingress controllers don’t actually route traffic themselves; they’re just the configuration front-end for an underlying proxy that does all the heavy lifting.

Let’s see it in action. Imagine you have a simple Kubernetes cluster with a couple of applications.

# deployment-app1.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app1
spec:
  replicas: 2
  selector:
    matchLabels:
      app: app1
  template:
    metadata:
      labels:
        app: app1
    spec:
      containers:
      - name: app1
        image: nginxdemos/hello:plain-text
        ports:
        - containerPort: 80
# service-app1.yaml
apiVersion: v1
kind: Service
metadata:
  name: app1-service
spec:
  selector:
    app: app1
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
# deployment-app2.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app2
spec:
  replicas: 2
  selector:
    matchLabels:
      app: app2
  template:
    metadata:
      labels:
        app: app2
    spec:
      containers:
      - name: app2
        image: nginxdemos/hello:plain-text
        ports:
        - containerPort: 80
# service-app2.yaml
apiVersion: v1
kind: Service
metadata:
  name: app2-service
spec:
  selector:
    app: app2
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80

Now, to expose these to the outside world, we need an Ingress resource. This tells the Ingress controller how to route traffic.

# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app-ingress
spec:
  rules:
  - host: app1.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: app1-service
            port:
              number: 80
  - host: app2.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: app2-service
            port:
              number: 80

When you apply this ingress.yaml, you’re not directly telling traffic where to go. Instead, you’re giving instructions to an Ingress controller (like Nginx Ingress, Traefik, or HAProxy Ingress) that’s already running in your cluster. This controller watches for these Ingress resources. Upon detecting my-app-ingress, it reads the rules: "If traffic arrives at app1.example.com, send it to the app1-service on port 80. If it arrives at app2.example.com, send it to app2-service on port 80." The controller then dynamically reconfigures its underlying proxy (e.g., Nginx) to enforce these rules.

The real work of routing happens in the proxy managed by the Ingress controller. The Ingress resource is just the Kubernetes-native way to define that routing logic. The controller’s job is to translate your declarative Ingress resources into the imperative configuration for its chosen proxy.

The core problem Ingress solves is providing a single, external entry point for multiple internal Kubernetes Services, abstracting away the complexity of exposing each Service individually. It enables host-based routing (like app1.example.com vs. app2.example.com) and path-based routing (e.g., example.com/api to one service, example.com/ui to another) without needing multiple LoadBalancers or NodePorts.

Internally, an Ingress controller typically runs as a Deployment within your cluster. It has access to the Kubernetes API to watch for Ingress, Service, and Endpoint resources. When an Ingress resource is created or updated, the controller reads its configuration. It then generates a configuration file for the proxy it manages (e.g., /etc/nginx/nginx.conf for Nginx Ingress). This configuration includes server blocks for each host, location blocks for each path, and directives to proxy requests to the correct Kubernetes Service’s ClusterIP and port. The controller then signals the proxy to reload its configuration, applying the new routing rules.

The external IP address you associate with your Ingress controller (often via a LoadBalancer Service type) is the single point of entry. All traffic for app1.example.com and app2.example.com hits this IP, and the Ingress controller’s proxy inspects the Host header to decide where to forward the request.

A key piece of information often overlooked is how the controller discovers the actual IP addresses of your application pods. It doesn’t just point to the Service’s ClusterIP. Instead, it watches Kubernetes Endpoints (or EndpointSlices). When a Service has backing pods, Kubernetes creates an Endpoint resource listing the IP addresses and ports of those ready pods. The Ingress controller reads these Endpoints and configures its proxy to load balance directly across these pod IPs, bypassing the Service’s kube-proxy layer for improved performance and direct pod-to-pod communication.

If you’re using TLS termination with Ingress, the Ingress controller is also responsible for fetching the correct TLS certificates (usually stored as Kubernetes Secrets) based on the tls section of your Ingress resource and presenting them to clients. This offloads TLS decryption from your application pods.

The next thing you’ll likely grapple with is configuring TLS certificates for your Ingress rules, which involves creating Kubernetes Secrets and referencing them in your Ingress definition.

Want structured learning?

Take the full Containers & Kubernetes course →