Kubernetes Administration — Level by Level›05 · Ingress: getting traffic in

Lesson 05 of 32 · Level 1 — Foundations

Ingress: getting traffic in

Get HTTP traffic into the cluster with an Ingress: install an NGINX ingress controller, expose it with a load balancer or NodePort, write host and path rules, and follow one request from the browser to the right pod and back.

Beginner → Practitioner
Key wordsIngressIngressClassingress controlleringress-nginxhost and path rulesNodePortLoadBalancerEndpointSlicesdefault backendrequest flow
DNS shop.example.com Browser https://shop… 1 Load balancer external IP (or NodePort) 2 Kubernetes cluster Ingress object host + path → Service ingress-nginx controller pod(s) reads Ingress rules watches 3 Service shop ClusterIP + EndpointSlices looks up pod IPs pod shop-1 app: shop pod shop-2 app: shop pod shop-3 app: shop 4 5 response returns the same way
One request through an NGINX ingress controller and back.

Why Ingress?

In lesson 04 you exposed an app with a NodePort or LoadBalancer Service. That works, but each app then needs its own port or its own load balancer and IP, and nothing handles hostnames, URL paths or TLS.

An Ingress gives you one entry point for many HTTP apps:

  • shop.example.com → the shop Service
  • shop.example.com/api → the API Service
  • blog.example.com → the blog Service

with TLS in one place (lesson 19).

Think of a big office building with one reception desk. Visitors don't wander in through random doors (NodePorts). Everyone comes to reception (the ingress controller), says who they're visiting ("shop, please"), and the receptionist looks at the visitor list (the Ingress rules) and walks them to the right office (the pods). When the visit is over, they leave the same way.

The three pieces

Piece What it is
Ingress controller A proxy running as pods (here: NGINX), exposed by its own Service
IngressClass Names a controller (nginx), so a cluster can have more than one
Ingress Your rules: host + path → Service + port

An Ingress on its own does nothing. A controller must be installed.

How a request travels

  1. DNS resolves shop.example.com to the address in front of the controller (a load-balancer IP, or node IPs).
  2. The browser connects to that address (port 80/443).
  3. The load balancer (or a NodePort on any node) forwards the connection to an ingress controller pod.
  4. NGINX matches the Host header and path against the Ingress rules, picks a pod IP from the Service's EndpointSlices, and proxies the request straight to that pod. (ingress-nginx sends traffic to pod endpoints directly; the Service is used to find them.)
  5. The pod's response goes back through NGINX and the load balancer to the browser.

Lab: ingress-nginx on kind

kind needs a cluster created with host ports 80/443 mapped to a node labelled for ingress. Delete your old lab cluster (or use a new name) and create this one:

# kind-ingress.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: ingress-lab
nodes:
  - role: control-plane
    kubeadmConfigPatches:
      - |
        kind: InitConfiguration
        nodeRegistration:
          kubeletExtraArgs:
            node-labels: "ingress-ready=true"
    extraPortMappings:
      - containerPort: 80
        hostPort: 80
        protocol: TCP
      - containerPort: 443
        hostPort: 443
        protocol: TCP
  - role: worker
$ kind create cluster --config kind-ingress.yaml
$ kubectl apply -f https://kind.sigs.k8s.io/examples/ingress/deploy-ingress-nginx.yaml
$ kubectl wait -n ingress-nginx --for=condition=ready pod \
    --selector=app.kubernetes.io/component=controller --timeout=180s
$ kubectl get ingressclass
NAME    CONTROLLER             PARAMETERS   AGE
nginx   k8s.io/ingress-nginx   <none>       1m

(This follows kind's ingress guide; check it for the current manifest URL if it has moved.)

About ingress-nginx

The community ingress-nginx project was retired in 2026: it no longer gets releases or security fixes. It's still the clearest way to learn how Ingress works, and you'll meet it in many existing clusters. For new production clusters, choose a maintained controller (for example F5's NGINX Ingress Controller, Traefik, HAProxy, or a Gateway API implementation, lesson 20). The Ingress rules in this lesson work the same way with any controller.

Deploy two apps and route to them

# apps.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: apple
spec:
  replicas: 2
  selector:
    matchLabels: { app: apple }
  template:
    metadata:
      labels: { app: apple }
    spec:
      containers:
        - name: echo
          image: hashicorp/http-echo:1.0
          args: [ "-text=apple" ]
          ports: [ { containerPort: 5678 } ]
---
apiVersion: v1
kind: Service
metadata:
  name: apple
spec:
  selector: { app: apple }
  ports: [ { port: 80, targetPort: 5678 } ]
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: banana
spec:
  replicas: 2
  selector:
    matchLabels: { app: banana }
  template:
    metadata:
      labels: { app: banana }
    spec:
      containers:
        - name: echo
          image: hashicorp/http-echo:1.0
          args: [ "-text=banana" ]
          ports: [ { containerPort: 5678 } ]
---
apiVersion: v1
kind: Service
metadata:
  name: banana
spec:
  selector: { app: banana }
  ports: [ { port: 80, targetPort: 5678 } ]
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: fruit
spec:
  ingressClassName: nginx
  rules:
    - host: fruit.127.0.0.1.nip.io        # nip.io resolves this name to 127.0.0.1
      http:
        paths:
          - path: /apple
            pathType: Prefix
            backend:
              service: { name: apple, port: { number: 80 } }
          - path: /banana
            pathType: Prefix
            backend:
              service: { name: banana, port: { number: 80 } }
$ kubectl apply -f apps.yaml -f ingress.yaml
$ curl http://fruit.127.0.0.1.nip.io/apple
apple
$ curl http://fruit.127.0.0.1.nip.io/banana
banana
$ curl -s -o /dev/null -w '%{http_code}\n' http://fruit.127.0.0.1.nip.io/cherry
404

No internet DNS? Send the Host header yourself: curl -H 'Host: fruit.127.0.0.1.nip.io' http://localhost/apple.

Exposing the controller on real clusters

On kind, host ports do the job. Elsewhere, the controller's Service decides how traffic arrives:

Option How Use when
LoadBalancer Cloud LB, or MetalLB/kube-vip on bare metal gives an external IP The normal choice where an LB exists
NodePort + external LB Controller Service is NodePort (e.g. 30080/30443); an external LB (HAProxy, F5, cloud LB) forwards to all nodes on those ports Bare metal with an existing load balancer
hostNetwork / DaemonSet Controller listens on 80/443 of chosen nodes Small or edge clusters, with care
$ kubectl get svc -n ingress-nginx ingress-nginx-controller
NAME                       TYPE           CLUSTER-IP     EXTERNAL-IP    PORT(S)
ingress-nginx-controller   LoadBalancer   10.96.41.20    203.0.113.10   80:31080/TCP,443:31443/TCP

Point DNS for your hostnames at the EXTERNAL-IP (or at the external LB in front of the NodePorts). Keep the nodes themselves behind the load balancer: users should never need to know node IPs.

When it doesn't work

Symptom Usually means Check
404 from NGINX No rule matched (host or path) Host header, pathType, kubectl describe ingress
503 Rule matched, no ready pods kubectl get endpointslices, pod readiness, Service selector/port
Ingress has no ADDRESS No controller picked it up ingressClassName, controller running?
EXTERNAL-IP <pending> No load balancer provider NodePort + external LB, or MetalLB (lesson 11)
Works by IP, not by name DNS dig shop.example.com

Try it: follow the request

  1. Build the kind cluster, install ingress-nginx and deploy the apple/banana example.
  2. Tail the controller's access log (kubectl logs -n ingress-nginx deploy/ingress-nginx-controller -f) and curl both paths; find the upstream pod IP in each log line and match it with kubectl get pods -o wide.
  3. Scale banana to 0 and curl /banana: which status code, and why?
  4. Add a second host rule (veg.127.0.0.1.nip.io) pointing at a new Service, without touching the first rule.
  5. Change the controller Service to NodePort on a non-kind cluster (if you have one) and reach it via a node IP and the NodePort.

Going deeper: Ingress in production

  • Run at least two controller replicas spread across nodes, with a PodDisruptionBudget, on dedicated nodes if traffic is heavy.
  • Set externalTrafficPolicy: Local on the controller Service to preserve client IPs (and understand the node health-check behaviour it brings).
  • Keep public and internal traffic on separate controllers/IngressClasses.
  • The Ingress API is frozen; new features arrive in the Gateway API (lesson 20). TLS is lesson 19, and the full request path in depth is in Networking Deep Dive.

Recap

  • Ingress = rules; the controller (NGINX pods) does the work; IngressClass links them.
  • Flow: DNS → LB/NodePort → controller → pod (via EndpointSlices) → back.
  • Expose the controller with LoadBalancer (cloud/MetalLB) or NodePort behind an external LB.
  • 404 = no rule matched; 503 = no ready endpoints.

This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.