Week 12: Ingress, Load Balancing & TLS

A LoadBalancer Service works, but one per public-facing app means one cloud load balancer — and one monthly bill — per app. Ingress solves this properly: a single entry point that routes to many backend Services based on hostname or path, with TLS termination handled in one place instead of scattered across every service. This is how a real cluster exposes many apps through one address.

Module 12 of 17 Week 12 of 18 ~3.5 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Explain the relationship between an Ingress resource and an Ingress controller
  • Route to multiple backend Services by hostname and by path
  • Terminate TLS at the Ingress, with certificates issued automatically

1. Ingress Resource vs. Ingress Controller

This distinction trips almost everyone up the first time: an Ingress resource, by itself, does nothing. It's a routing declaration — rules saying "traffic for this hostname/path should go to that Service." An Ingress controller is a separate piece of software, running as Pods in your cluster, that actually reads Ingress resources and does the routing.

the split, explicitly
Ingress resource     # a YAML object declaring routing RULES. Inert on its own —
                      # creating one with no controller installed does nothing.

Ingress controller   # actual running software (commonly NGINX Ingress, but
                      # Traefik and cloud-specific ones exist too) that watches
                      # for Ingress resources and configures a real reverse
                      # proxy/load balancer to match.

This mirrors a pattern you've now seen repeatedly: a declarative Kubernetes object paired with a controller that reconciles reality toward it — the same shape as Deployments and ReplicaSets, applied here to routing instead of Pod count.

Managed clusters usually don't ship one by default

EKS, GKE and AKS typically expect you to install an Ingress controller yourself (or use their cloud-specific one). If your Ingress resources aren't doing anything, checking "is a controller actually installed and running?" should be your very first debugging step.

2. Host- & Path-based Routing

One Ingress can route many different hostnames and paths to different backend Services — this is the actual mechanism that lets one public IP serve an entire cluster's worth of apps.

ingress.yaml — routing by hostname AND path
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: main-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
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: /v1
            pathType: Prefix
            backend:
              service:
                name: api
                port:
                  number: 80

app.example.com and api.example.com can share the exact same public IP and load balancer — the Ingress controller inspects the incoming request's Host header (and path) and routes accordingly, all before the request ever reaches a Service.

Every backend here is a ClusterIP Service

You almost never expose frontend or api as LoadBalancer Services themselves once Ingress is in front of them — internal ClusterIP is enough, since the Ingress controller is the single public entry point routing to them.

3. Installing NGINX Ingress

Getting a controller running on your local cluster:

install & verify
# Minikube ships an addon:
minikube addons enable ingress

# kind / other clusters — install via the official manifest:
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/cloud/deploy.yaml

kubectl get pods -n ingress-nginx     # confirm the controller Pods are Running
kubectl get ingress                    # your Ingress resources now show an assigned ADDRESS

Once installed, apply your ingress.yaml from Section 2 and, for local testing without real DNS, map hostnames to your cluster's IP in your machine's hosts file:

local testing without real DNS
# /etc/hosts (or C:\Windows\System32\drivers\etc\hosts on Windows)
127.0.0.1  app.example.com
127.0.0.1  api.example.com

# For Minikube specifically, you may need: minikube tunnel
curl http://app.example.com
Real production DNS points at the Ingress's IP

On a real cloud cluster, you'd point an actual DNS A record (app.example.com) at the LoadBalancer IP the Ingress controller itself is exposed through — the hosts-file trick above is purely a local-development stand-in for that.

4. TLS with cert-manager

Terminating HTTPS at the Ingress means your backend Services never have to handle TLS themselves — one certificate, in one place, for the whole cluster's public traffic. cert-manager automates obtaining and renewing those certificates, commonly from Let's Encrypt, for free.

ingress.yaml — with TLS
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: main-ingress
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - app.example.com
      secretName: app-tls-cert    # cert-manager populates this Secret automatically
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: frontend
                port:
                  number: 80

Note that a TLS certificate ends up stored as — unsurprisingly — a Kubernetes Secret, the exact object type from Week 10. cert-manager handles the entire domain-validation and renewal process automatically once configured, which is the difference between a certificate that quietly expires one day and one that never does.

This step needs a real, publicly-resolvable domain

Let's Encrypt validates domain ownership over the public internet — it can't issue a certificate for a local-only hostname like app.example.com in your hosts file. This piece is worth understanding conceptually now and trying hands-on once you have a real domain and a cloud cluster, in Week 18's capstone.

5. Hands-on Exercise

Hands-on

Route two apps through one Ingress, by hostname and by path

Install a real Ingress controller locally, then expose two different backend Services through a single entry point using both routing styles.

Part 1 — Install and route by path:

  1. Install the NGINX Ingress controller on your local cluster, and confirm its Pods are Running.
  2. Deploy two simple apps (reuse images from earlier weeks, or any two containers that each respond distinctly, e.g. two different httpd containers each serving different static text) as separate Deployments + ClusterIP Services.
  3. Write an Ingress routing /app1 to the first Service and /app2 to the second, both on the same host.
  4. Confirm both paths route correctly to the right backend.
Hint

If path-based routing serves the wrong app's static assets (a common symptom: HTML loads but CSS/JS 404), it's usually because the app itself doesn't know it's being served from a subpath — the rewrite-target annotation from Section 2 is often necessary to strip the prefix before it reaches the backend.

Part 2 — Add host-based routing:

  1. Add two entries to your hosts file mapping two fake hostnames to your cluster's local IP (127.0.0.1 for most local setups).
  2. Rewrite your Ingress to route by hostname instead of path — one Service per hostname, both back to root path /.
  3. Confirm each hostname correctly reaches its own app, and that a request to a hostname NOT listed in your Ingress rules gets a 404 from the Ingress controller itself (not from either backend).
  4. Run kubectl describe ingress main-ingress and identify exactly which section of the output shows the host-to-backend mapping you configured.
Hint

A 404 for an unrecognized hostname coming from the Ingress controller itself (check the response headers for "nginx") — rather than from either of your apps — is proof the routing decision is happening at the Ingress layer, before your Services are ever involved.

6. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

You applied an Ingress resource but requests aren't being routed at all. What should you check first?

Whether an Ingress controller is actually installed and running in the cluster. An Ingress resource by itself is only a routing declaration — inert without a controller (like NGINX Ingress) actively watching for it and configuring a real proxy to match.

Q2

Why can two completely different apps share the same public IP and load balancer through Ingress?

The Ingress controller inspects the incoming request's Host header and/or path before deciding which backend Service to route to — this lets one entry point (and one cloud load balancer) multiplex traffic for many different hostnames and paths to many different backend apps.

Q3

Once Ingress is in front of your apps, what Service type do the backend Services typically need?

ClusterIP. The Ingress controller is the single public entry point, so backend Services generally only need to be reachable from inside the cluster — exposing each of them individually as LoadBalancer Services would be redundant and defeat the purpose of consolidating through Ingress.

Q4

Where does a TLS certificate obtained by cert-manager actually get stored?

As a Kubernetes Secret — the same object type covered in Week 10 — referenced by the Ingress's tls.secretName field. cert-manager automates populating and renewing that Secret's contents from a certificate authority like Let's Encrypt.