Kubernetes Networking & Storage

Ingress & Ingress Controllers

18 min Lesson 3 of 31

Ingress & Ingress Controllers

A Kubernetes Service of type LoadBalancer gives you one cloud load-balancer per service. At ten services that is ten public IPs, ten monthly bills, and ten TLS certificates to rotate. Real production clusters serve dozens to hundreds of HTTP workloads. Ingress is the Kubernetes API object that solves this: one entry point to the cluster, with L7 (HTTP/HTTPS) routing rules that fan traffic out to the right Services based on hostname and path — no extra cloud resources per service.

The Ingress API vs. the Ingress Controller

The Ingress API object is just a configuration declaration stored in etcd — it does nothing on its own. The heavy lifting is done by an Ingress Controller: a Deployment running inside the cluster that watches Ingress objects and programs an actual reverse proxy accordingly. The two most widely deployed controllers are:

  • ingress-nginx (maintained by the Kubernetes community) — wraps NGINX; the default choice for self-managed clusters and EKS/GKE bare setups.
  • AWS Load Balancer Controller — provisions an Application Load Balancer (ALB) per Ingress or shares one across Ingresses using target-group binding; mandatory on EKS when you need WAF or native AWS certificate management.

Other controllers include Traefik, HAProxy Ingress, and Contour. The Ingress spec is controller-agnostic: the same YAML works (mostly) across controllers; controller-specific behaviour is expressed via annotations on the Ingress object.

Key idea: The Ingress API object is a rule book. The Ingress Controller is the enforcer that reads that rule book and reconfigures NGINX (or ALB) in real time. Installing Kubernetes without an Ingress Controller and then creating Ingress objects has zero effect — a very common first-timer mistake.

Installing ingress-nginx

On a real cluster, ingress-nginx is installed via Helm. The chart creates the controller Deployment, a Service of type LoadBalancer (the single cloud LB), and all necessary RBAC.

# Add the ingress-nginx Helm repo helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx helm repo update # Install into its own namespace; keep 2 replicas for HA helm install ingress-nginx ingress-nginx/ingress-nginx \ --namespace ingress-nginx \ --create-namespace \ --set controller.replicaCount=2 \ --set controller.resources.requests.cpu=100m \ --set controller.resources.requests.memory=128Mi # Verify the controller pod and the LoadBalancer Service (grab the external IP/hostname) kubectl -n ingress-nginx get pods kubectl -n ingress-nginx get svc ingress-nginx-controller

Writing an Ingress Manifest

A minimal Ingress that routes two virtual hosts to two different Services:

apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: store-ingress namespace: production annotations: # Tell Kubernetes which controller should handle this Ingress kubernetes.io/ingress.class: "nginx" # Redirect all HTTP to HTTPS automatically nginx.ingress.kubernetes.io/ssl-redirect: "true" # Increase proxy timeouts for long-running API calls nginx.ingress.kubernetes.io/proxy-read-timeout: "120" spec: tls: - hosts: - api.example.com - www.example.com secretName: example-tls # must be a TLS Secret in the same namespace rules: - host: api.example.com http: paths: - path: /v1 pathType: Prefix backend: service: name: api-service port: number: 80 - host: www.example.com http: paths: - path: / pathType: Prefix backend: service: name: frontend-service port: number: 80

Key fields to understand:

  • spec.tls — enables HTTPS. The controller terminates TLS here and forwards plain HTTP to the backend Service.
  • spec.rules[*].host — exact hostname match; the controller uses the HTTP Host header to pick a rule.
  • pathType: Prefix — matches any path starting with the given string. Exact matches only the literal path. ImplementationSpecific delegates interpretation to the controller.
  • annotations — the escape hatch for anything not in the Ingress spec: rate limiting, CORS headers, auth, WAF rules, canary weights, etc.

TLS Termination in Depth

Ingress TLS terminates at the controller. The Secret must contain tls.crt and tls.key. The most production-grade way to manage this is cert-manager, which integrates with Let's Encrypt and rotates certificates automatically before expiry:

# Install cert-manager (manages TLS certs lifecycle) helm repo add jetstack https://charts.jetstack.io helm repo update helm install cert-manager jetstack/cert-manager \ --namespace cert-manager \ --create-namespace \ --set crds.enabled=true # Create a ClusterIssuer for Let's Encrypt production cat <<EOF | kubectl apply -f - apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-prod spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: ops@example.com privateKeySecretRef: name: letsencrypt-prod-key solvers: - http01: ingress: class: nginx EOF # Then annotate your Ingress to trigger automatic cert issuance: # cert-manager.io/cluster-issuer: "letsencrypt-prod" # cert-manager will create and keep the Secret fresh automatically.
Production practice: Never commit TLS private keys to git. Use cert-manager with Let's Encrypt for internet-facing workloads, or AWS ACM / GCP-managed certs for managed-cloud clusters. Set nginx.ingress.kubernetes.io/ssl-redirect: "true" globally so HTTP is never served in production.

Ingress Traffic Flow Diagram

Ingress L7 Traffic Flow: Client to Pod via Ingress Controller Client Browser/CLI HTTPS Cloud LB Service type: LoadBalancer TCP:443 Ingress Controller (NGINX Pod) TLS termination Host/path routing Reads Ingress rules api.example.com www.example.com api-service ClusterIP frontend-svc ClusterIP api Pod web Pod cert-manager Auto-renews TLS Secret
L7 routing flow: a single Cloud Load Balancer forwards traffic to the Ingress Controller, which terminates TLS (via cert-manager) and routes by hostname/path to the correct ClusterIP Service and ultimately to Pods.

Production Failure Modes

Understanding what breaks in production — and why — separates engineers who configure Ingress from those who operate it:

  • Missing ingressClassName / annotation: If you run multiple controllers (nginx and ALB simultaneously) and omit the class selector, neither controller claims the Ingress. Traffic never arrives. Always set spec.ingressClassName: nginx or the equivalent annotation.
  • Service port mismatch: The Ingress backend references port 80 but the Service exposes port 8080. The controller silently returns 503. Always verify with kubectl describe ingress <name> — look for Endpoints in the output; if it shows <none>, the backend mapping is wrong.
  • TLS Secret in wrong namespace: The Ingress and the tls.secretName Secret must be in the same namespace. A cross-namespace Secret reference silently fails, leaving the controller serving a self-signed fallback certificate — the worst kind of failure because HTTPS still works, but with the wrong cert.
  • Controller not HA: A single-replica ingress-nginx Deployment means that a Pod restart or rolling update of the controller drops all inbound traffic for several seconds. Always run at least 2 replicas and configure a PodDisruptionBudget with minAvailable: 1.
  • Annotation typo: NGINX annotations are read as raw strings — a typo like nginx.ingress.kubernetes.io/ssl-redirct is ignored without error. Validate your manifest with kubectl apply --dry-run=server and check the controller logs after applying.
Production pitfall — 413 Request Entity Too Large: The default NGINX client_max_body_size is 1 MB. File uploads and large JSON payloads hit this limit immediately. Set the annotation nginx.ingress.kubernetes.io/proxy-body-size: "50m" on the Ingress for any service that accepts uploads, or override it globally in the controller ConfigMap. Debugging this is infuriating because the error appears in the client, not in the application logs.

Checking Ingress Health

A three-command workflow to diagnose any Ingress problem:

# 1. See the routing table the controller built and whether backends resolved kubectl describe ingress store-ingress -n production # Look for: Rules section (correct hosts/paths?), Default backend, Endpoints under each service # 2. Tail the controller logs — every request is logged with upstream selection kubectl -n ingress-nginx logs -l app.kubernetes.io/name=ingress-nginx --tail=50 # 3. Check that the TLS Secret exists and is well-formed kubectl -n production get secret example-tls -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -subject -dates

Ingress + ingress-nginx + cert-manager is the standard L7 entry-point stack for Kubernetes. In the next lesson you will see the Gateway API — the next-generation successor to Ingress that solves its multi-tenancy and expressiveness limitations while keeping the same L7 routing intent.