Skip to content
Codeloom
Kubernetes

Ingress Controllers in Kubernetes

A practical guide to Kubernetes Ingress: Ingress resources, NGINX Ingress Controller setup, TLS termination, path-based and host-based routing, annotations, and production configuration.

·7 min read · By Codeloom
Intermediate 14 min read

What you'll learn

  • What Ingress resources are and how they differ from Services
  • Installing and configuring NGINX Ingress Controller
  • Path-based and host-based routing rules
  • TLS termination with certificates
  • Annotations for rate limiting, redirects, and more

Prerequisites

  • Kubernetes fundamentals (Pods, Deployments, Services)
  • Basic understanding of HTTP, DNS, and TLS
  • Familiarity with kubectl and Helm

A Service of type LoadBalancer gives each service its own external IP and load balancer. If you have 20 services, you get 20 load balancers and 20 IPs. Ingress solves this by providing a single entry point that routes traffic to different services based on the hostname or URL path. One load balancer, one IP, and a set of routing rules that direct traffic to the right backend.

Ingress vs Services

Without Ingress:
  Client → LoadBalancer A → Service A → Pods
  Client → LoadBalancer B → Service B → Pods
  Client → LoadBalancer C → Service C → Pods
  (3 external IPs, 3 load balancers)

With Ingress:
  Client → Ingress Controller (1 LoadBalancer) → Service A → Pods
                                                → Service B → Pods
                                                → Service C → Pods
  (1 external IP, 1 load balancer)

An Ingress resource is a set of rules. An Ingress Controller is the software that reads those rules and configures itself to route traffic accordingly. The Ingress resource without a controller does nothing.

Installing NGINX Ingress Controller

The most common Ingress Controller is NGINX. Install it with Helm:

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

helm install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx \
  --create-namespace \
  --set controller.replicaCount=2
# Verify installation
kubectl get pods -n ingress-nginx
kubectl get svc -n ingress-nginx

# The controller creates a LoadBalancer service
# NAME                       TYPE           CLUSTER-IP     EXTERNAL-IP    PORT(S)
# ingress-nginx-controller   LoadBalancer   10.96.200.50   203.0.113.10   80:30080/TCP,443:30443/TCP

The EXTERNAL-IP is what you point your DNS records to.

Basic Ingress resource

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-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-service
                port:
                  number: 80
kubectl apply -f ingress.yaml
kubectl get ingress
kubectl describe ingress app-ingress

This routes all traffic for app.example.com to frontend-service on port 80.

Path-based routing

Route different URL paths to different backend services.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
spec:
  ingressClassName: nginx
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api-service
                port:
                  number: 8080
          - path: /admin
            pathType: Prefix
            backend:
              service:
                name: admin-service
                port:
                  number: 8080
          - path: /
            pathType: Prefix
            backend:
              service:
                name: frontend-service
                port:
                  number: 80
app.example.com/api/*    → api-service:8080
app.example.com/admin/*  → admin-service:8080
app.example.com/*        → frontend-service:80

Rules are evaluated in order from most specific to least specific. The /api and /admin rules match before the catch-all /.

pathType options

# Prefix: matches the path prefix
# /api matches /api, /api/, /api/users, /api/users/123
- path: /api
  pathType: Prefix

# Exact: matches only the exact path
# /api matches only /api, not /api/ or /api/users
- path: /api
  pathType: Exact

# ImplementationSpecific: behavior depends on the IngressClass
- path: /api
  pathType: ImplementationSpecific

Host-based routing

Route traffic based on the hostname. Multiple domains pointing to the same Ingress Controller.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: multi-host-ingress
spec:
  ingressClassName: nginx
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: main-app
                port:
                  number: 80
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: api-service
                port:
                  number: 8080
    - host: docs.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: docs-service
                port:
                  number: 80

Each hostname resolves to the same Ingress Controller IP, but the controller routes to different services based on the Host header.

TLS termination

Ingress can terminate TLS so your backend services receive plain HTTP traffic.

Create a TLS Secret

# From existing certificate files
kubectl create secret tls app-tls \
  --cert=./tls.crt \
  --key=./tls.key \
  -n default

Configure TLS in Ingress

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

Automated certificates with cert-manager

cert-manager automates TLS certificate issuance and renewal from Let’s Encrypt.

# Install cert-manager
helm repo add jetstack https://charts.jetstack.io
helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --set crds.enabled=true
# ClusterIssuer for Let's Encrypt
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: admin@example.com
    privateKeySecretRef:
      name: letsencrypt-prod-key
    solvers:
      - http01:
          ingress:
            class: nginx
# Ingress with automatic certificate
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - app.example.com
      secretName: app-tls-auto  # cert-manager creates this Secret
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: frontend-service
                port:
                  number: 80

cert-manager watches for Ingress resources with the cert-manager.io/cluster-issuer annotation, requests a certificate, stores it in the named Secret, and renews it before expiry.

NGINX Ingress annotations

Annotations configure NGINX-specific behavior on a per-Ingress basis.

Rate limiting

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

Redirects

metadata:
  annotations:
    # Permanent redirect
    nginx.ingress.kubernetes.io/permanent-redirect: "https://new-site.example.com"

    # HTTP to HTTPS
    nginx.ingress.kubernetes.io/ssl-redirect: "true"

    # www to non-www
    nginx.ingress.kubernetes.io/from-to-www-redirect: "true"

Timeouts and buffer sizes

metadata:
  annotations:
    nginx.ingress.kubernetes.io/proxy-connect-timeout: "10"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
    nginx.ingress.kubernetes.io/proxy-body-size: "50m"

CORS

metadata:
  annotations:
    nginx.ingress.kubernetes.io/enable-cors: "true"
    nginx.ingress.kubernetes.io/cors-allow-origin: "https://app.example.com"
    nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST, PUT, DELETE, OPTIONS"
    nginx.ingress.kubernetes.io/cors-allow-headers: "Authorization, Content-Type"

Custom NGINX configuration

metadata:
  annotations:
    nginx.ingress.kubernetes.io/configuration-snippet: |
      more_set_headers "X-Frame-Options: DENY";
      more_set_headers "X-Content-Type-Options: nosniff";
      more_set_headers "X-XSS-Protection: 1; mode=block";

Default backend

Handle requests that do not match any Ingress rule.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: default-ingress
spec:
  ingressClassName: nginx
  defaultBackend:
    service:
      name: default-backend
      port:
        number: 80
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: frontend-service
                port:
                  number: 80

Requests to unrecognized hosts or paths go to default-backend, which typically serves a custom 404 page.

Health checks

The NGINX Ingress Controller can configure health check endpoints for backends.

metadata:
  annotations:
    nginx.ingress.kubernetes.io/health-check-path: "/healthz"
    nginx.ingress.kubernetes.io/health-check-interval: "10"
    nginx.ingress.kubernetes.io/health-check-timeout: "5"

Monitoring Ingress

# Check Ingress status
kubectl get ingress -A

# View NGINX configuration generated by the controller
kubectl exec -n ingress-nginx deploy/ingress-nginx-controller -- cat /etc/nginx/nginx.conf

# View controller logs
kubectl logs -n ingress-nginx deploy/ingress-nginx-controller -f

# NGINX metrics (if enabled)
# The controller exposes Prometheus metrics on port 10254
kubectl port-forward -n ingress-nginx svc/ingress-nginx-controller-metrics 10254:10254
curl http://localhost:10254/metrics

Alternative Ingress Controllers

NGINX is the most common but not the only option:

  • Traefik — Auto-discovery, built-in Let’s Encrypt, middleware system
  • HAProxy — High-performance, TCP/UDP support, advanced load balancing algorithms
  • Kong — API gateway features, plugins for auth/rate-limiting/transformations
  • AWS ALB Ingress Controller — Creates AWS Application Load Balancers directly
  • Istio Gateway — Part of the Istio service mesh, advanced traffic management

Each controller has different strengths. NGINX is a safe default with the largest community and most documentation. Choose alternatives when you need specific features like API gateway functionality (Kong) or service mesh integration (Istio).

Ingress is the standard way to expose HTTP services in Kubernetes. Start with NGINX Ingress Controller, add cert-manager for automated TLS, and use annotations to configure behavior per-service. For production, always run at least two controller replicas and monitor the controller logs and metrics.