Kubernetes Ingress with NGINX and TLS (cert-manager) Explained

kubernetes ingress nginx tls - custom-ing-featured.png

A Kubernetes Service gives an app a stable address inside the cluster; an Ingress gives it a public front door.
Instead of one load balancer per app, a single ingress controller accepts HTTP(S) traffic and routes it by hostname and path to the right Service.

This guide assumes a working cluster (see kubeadm setup) and uses
Helm to install the controller. Restrict east-west traffic separately with
NetworkPolicy.

What an Ingress Actually Is

An Ingress is only a set of routing rules. Nothing happens until an ingress controller (here ingress-nginx) watches those rules and configures a real reverse proxy.

architectureingress-flow.txt
  internet --> [ LoadBalancer / NodePort ] --> [ ingress-nginx Pod ]
                                                  |-- app.example.com/     -> svc/app
                                                  |-- app.example.com/api  -> svc/api
Ingress architecture
One entry, many services

Install the Ingress Controller

Install into its own namespace and note the external address it receives; your DNS records will point there. On bare metal without a cloud load balancer, use MetalLB or a NodePort.

bashinstall
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx -n ingress-nginx --create-namespace
bashfind the address
kubectl get svc -n ingress-nginx ingress-nginx-controller
Installing ingress-nginx
Helm install and external IP

Deploy a Sample App and Service

Ingress routes to Services, so start with a Deployment and a ClusterIP Service.

yamlapp.yaml
apiVersion: apps/v1
kind: Deployment
metadata: { name: app }
spec:
  replicas: 2
  selector: { matchLabels: { app: app } }
  template:
    metadata: { labels: { app: app } }
    spec:
      containers:
        - name: web
          image: nginxdemos/hello
          ports: [{ containerPort: 80 }]
---
apiVersion: v1
kind: Service
metadata: { name: app-svc }
spec:
  selector: { app: app }
  ports: [{ port: 80, targetPort: 80 }]
Sample app
Deployment and Service

Create a Host-Based Ingress Rule

Point a DNS record for app.example.com at the controller address, then declare the rule.

yamlingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app
spec:
  ingressClassName: nginx
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service: { name: app-svc, port: { number: 80 } }
bashapply and test
kubectl apply -f app.yaml -f ingress.yaml
curl -H 'Host: app.example.com' http://<controller-ip>/
Ingress rule
Host, path, backend

Route Several Paths to Several Services

One hostname can front multiple services. Longer prefixes are more specific and win over shorter ones.

yamlpaths (excerpt)
paths:
  - { path: /api,  pathType: Prefix, backend: { service: { name: api-svc,  port: { number: 80 } } } }
  - { path: /docs, pathType: Prefix, backend: { service: { name: docs-svc, port: { number: 80 } } } }
  - { path: /,     pathType: Prefix, backend: { service: { name: web-svc,  port: { number: 80 } } } }
Path routing
/, /api, /docs

Issue TLS Certificates with cert-manager

cert-manager requests and renews Let’s Encrypt certificates automatically. Create a ClusterIssuer once, then annotate each Ingress.

bashinstall cert-manager
helm repo add jetstack https://charts.jetstack.io
helm install cert-manager jetstack/cert-manager -n cert-manager --create-namespace --set crds.enabled=true
yamlclusterissuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata: { name: letsencrypt-prod }
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: [email protected]
    privateKeySecretRef: { name: letsencrypt-prod-key }
    solvers:
      - http01: { ingress: { ingressClassName: nginx } }
yamlingress TLS additions
metadata:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  tls:
    - hosts: [app.example.com]
      secretName: app-tls
TipTest with Let’s Encrypt’s staging server first to avoid rate limits.
cert-manager TLS
Issuer, annotation, secret

Tune Behaviour with Annotations

Per-Ingress annotations adjust the generated nginx config: upload size, HTTPS redirect, basic rate limiting.

yamlannotations
metadata:
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: "20m"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/limit-rps: "10"
Annotations
Body size, redirects, limits

Debug 404 and 502 Errors

A 404 means no rule matched (wrong host or path, or the wrong ingressClassName). A 502/503 means the rule matched but no healthy Pod answered.

bashchecks
kubectl describe ingress app
kubectl get endpoints app-svc
kubectl logs -n ingress-nginx deploy/ingress-nginx-controller --tail=50
Debugging ingress
Rules, endpoints, logs

Quick Reference

  • Controller + Service + Ingress rule; DNS to the controller address
  • cert-manager for TLS; empty endpoints explains most 502s

Related tutorials

Diagrams are original illustrations by Gnome IT Solutions. Tutorial text © Gnome IT Solutions.