Ingress Controllers

Expose HTTP and HTTPS routes with Ingress controllers, path-based routing, and TLS certificates.

8 min read

Ingress Controllers

In the previous tutorial, we mastered Jobs and CronJobs — running one-time and scheduled tasks. Now let's solve a big problem: getting outside traffic into your cluster.

Remember Services with type LoadBalancer? They work, but each one gets its own external IP. And in the cloud, each one costs money. Running 20 services? That's 20 load balancers. Your finance team will not be happy.

Ingress is the solution — one entry point that routes to multiple services based on hostname or URL path. Think of it as the reception desk of your office building: one front door, but it knows exactly which floor and room to send each visitor to.

How Ingress Works

Internet → Ingress Controller → Ingress Rules → Services → Pods
                  ↓
           (nginx, traefik, etc.)

Two components:

  1. Ingress Controller: A Pod running a reverse proxy (nginx, traefik, HAProxy) — the actual bouncer
  2. Ingress Resource: YAML defining routing rules — the guest list

The controller watches for Ingress resources and configures itself automatically. You write the rules, it does the routing. How cool is that?

Install NGINX Ingress Controller

First things first — unlike most Kubernetes features, Ingress doesn't work out of the box. You need to install a controller.

On Minikube, it's a one-liner:

minikube addons enable ingress

Verify it's running:

kubectl get pods -n ingress-nginx
NAME                                        READY   STATUS    RESTARTS   AGE
ingress-nginx-controller-5c8d66c76d-abcde   1/1     Running   0          2m

For other environments, use Helm:

helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm install ingress-nginx ingress-nginx/ingress-nginx

Create Test Applications

Let's deploy two simple apps so we have something to route to:

# app1.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: app1
  template:
    metadata:
      labels:
        app: app1
    spec:
      containers:
      - name: app1
        image: hashicorp/http-echo
        args: ["-text=Hello from App 1"]
        ports:
        - containerPort: 5678
---
apiVersion: v1
kind: Service
metadata:
  name: app1-service
spec:
  selector:
    app: app1
  ports:
  - port: 80
    targetPort: 5678
# app2.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app2
spec:
  replicas: 1
  selector:
    matchLabels:
      app: app2
  template:
    metadata:
      labels:
        app: app2
    spec:
      containers:
      - name: app2
        image: hashicorp/http-echo
        args: ["-text=Hello from App 2"]
        ports:
        - containerPort: 5678
---
apiVersion: v1
kind: Service
metadata:
  name: app2-service
spec:
  selector:
    app: app2
  ports:
  - port: 80
    targetPort: 5678

Apply:

kubectl apply -f app1.yaml -f app2.yaml

Basic Ingress: Path-Based Routing

Now the fun part! Let's route different URL paths to different services. Like /app1 goes to App 1 and /app2 goes to App 2:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: path-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
  - http:
      paths:
      - path: /app1
        pathType: Prefix
        backend:
          service:
            name: app1-service
            port:
              number: 80
      - path: /app2
        pathType: Prefix
        backend:
          service:
            name: app2-service
            port:
              number: 80

Apply:

kubectl apply -f path-ingress.yaml

Check status:

kubectl get ingress
NAME           CLASS   HOSTS   ADDRESS        PORTS   AGE
path-ingress   nginx   *       192.168.49.2   80      30s

Test (on Minikube):

minikube ip
# 192.168.49.2

curl http://192.168.49.2/app1
# Hello from App 1

curl http://192.168.49.2/app2
# Hello from App 2

One IP, two apps. Your finance team just bought you lunch.

Path Types

TypeBehavior
PrefixMatches URL paths starting with the value
ExactOnly matches the exact path
ImplementationSpecificDepends on the Ingress controller

Host-Based Routing

"What if I want different domains to go to different services?"

That's host-based routing — like virtual hosts in Apache or nginx. Different hostnames, same IP:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: host-ingress
spec:
  ingressClassName: nginx
  rules:
  - host: app1.local
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: app1-service
            port:
              number: 80
  - host: app2.local
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: app2-service
            port:
              number: 80

Apply:

kubectl apply -f host-ingress.yaml

Add to /etc/hosts (so your machine knows where to find these fake domains):

192.168.49.2  app1.local app2.local

Test:

curl http://app1.local
# Hello from App 1

curl http://app2.local
# Hello from App 2

TLS/HTTPS

"What about HTTPS? I can't serve everything over plain HTTP like it's 2005."

Fair point. Let's add TLS.

Create a TLS Secret

Generate a self-signed certificate (for testing — don't use this in production unless you enjoy browser warnings):

openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout tls.key -out tls.crt \
  -subj "/CN=myapp.local"

Create secret:

kubectl create secret tls myapp-tls --key tls.key --cert tls.crt

Or via YAML:

apiVersion: v1
kind: Secret
metadata:
  name: myapp-tls
type: kubernetes.io/tls
data:
  tls.crt: <base64-encoded-cert>
  tls.key: <base64-encoded-key>

Ingress with TLS

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: tls-ingress
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - myapp.local
    secretName: myapp-tls
  rules:
  - host: myapp.local
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: app1-service
            port:
              number: 80

Apply:

kubectl apply -f tls-ingress.yaml

Test:

curl -k https://myapp.local
# Hello from App 1

The -k flag ignores certificate validation (because our cert is self-signed and browsers/curl will complain about it).

Useful Annotations

NGINX Ingress supports a ton of annotations. Here are the ones you'll actually use:

Redirect HTTP to HTTPS

Because unencrypted traffic in 2024 is a no-go:

metadata:
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"

URL Rewriting

Strip prefixes so your backend doesn't need to know about the routing structure:

metadata:
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
  rules:
  - http:
      paths:
      - path: /api(/|$)(.*)
        pathType: Prefix
        backend:
          service:
            name: api-service
            port:
              number: 80

/api/users becomes /users when forwarded to the backend. Your API doesn't need to know it's behind an /api prefix. Sneaky and clean.

Rate Limiting

Protect your backend from overeager clients:

metadata:
  annotations:
    nginx.ingress.kubernetes.io/limit-rps: "10"
    nginx.ingress.kubernetes.io/limit-connections: "5"

Timeouts

metadata:
  annotations:
    nginx.ingress.kubernetes.io/proxy-connect-timeout: "10"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "300"

CORS

metadata:
  annotations:
    nginx.ingress.kubernetes.io/enable-cors: "true"
    nginx.ingress.kubernetes.io/cors-allow-origin: "https://example.com"

Default Backend

What happens when someone hits a URL that doesn't match any rule? Without a default backend, they get a generic 404. Let's do better:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-with-default
spec:
  ingressClassName: nginx
  defaultBackend:
    service:
      name: default-service
      port:
        number: 80
  rules:
  - host: myapp.local
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: app1-service
            port:
              number: 80

Combining Rules

Here's what a real-world Ingress looks like — multiple hosts, multiple paths, TLS, the works:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: production-ingress
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - api.example.com
    - www.example.com
    secretName: example-tls
  rules:
  - host: www.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: frontend-service
            port:
              number: 80
  - host: api.example.com
    http:
      paths:
      - path: /v1
        pathType: Prefix
        backend:
          service:
            name: api-v1-service
            port:
              number: 80
      - path: /v2
        pathType: Prefix
        backend:
          service:
            name: api-v2-service
            port:
              number: 80

That's a production-grade Ingress. One resource, two domains, API versioning, HTTPS everywhere. Pretty slick, right?

Troubleshooting

Ingress can be finicky. Here's your debugging playbook.

Check Ingress Status

kubectl describe ingress <ingress-name>

Look for:

  • Events showing configuration updates
  • Address field (should have IP)
  • Backend status

View Controller Logs

kubectl logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx

Common Issues

No Address assigned:

  • Ingress controller not running (did you enable the addon?)
  • Wrong ingressClassName (typo, perhaps?)

404 errors:

  • Service name or port wrong (double-check your YAML)
  • Path doesn't match (Prefix vs Exact matters!)

502 Bad Gateway:

  • Backend pods not ready (check readiness probes from the previous tutorial!)
  • Service selector doesn't match pods (labels, labels, labels)

Debug with curl

curl -v http://myapp.local

The -v flag shows headers and connection details. Look for clues about what the Ingress controller is doing with your request.

Clean Up

kubectl delete ingress path-ingress host-ingress tls-ingress 2>/dev/null
kubectl delete -f app1.yaml -f app2.yaml 2>/dev/null
kubectl delete secret myapp-tls 2>/dev/null

What's Next?

Nice work! You now know how to expose your apps to the outside world with proper HTTP routing, TLS, path-based and host-based rules. One entry point, many services. Your cluster is starting to look production-ready.

But here's the thing — all this traffic flowing around inside your cluster... who decides what can talk to what? Right now, anything can reach anything else. That's where Network Policies come in — the firewall rules of Kubernetes. Let's go!