Kubernetes Ingress 2025 — Route External Traffic with Nginx and Cloud Controllers

Sanjeev SharmaSanjeev Sharma
6 min read

Advertisement

Introduction

Why This Matters

Every Kubernetes cluster that serves external HTTP/HTTPS traffic needs an Ingress solution. Without Ingress, you would need a separate cloud load balancer for every Service — expensive and operationally painful. A single Ingress controller handles routing for all services in the cluster, enabling path-based routing, host-based routing, TLS termination, rate limiting, and authentication at the edge.

In 2025, Ingress is being supplemented by the Kubernetes Gateway API (GA since Kubernetes 1.28), which provides more expressive routing rules and better multi-team support. Understanding both Ingress and Gateway API is important as the ecosystem transitions.

Ingress vs Service LoadBalancer

Without Ingress:

  • Each Service needs a LoadBalancer — expensive (one cloud LB per service)
  • No shared TLS termination
  • No path-based routing between services

With Ingress:

  • One cloud load balancer feeds traffic to the Ingress controller
  • The controller routes to multiple services based on hostname and path
  • Centralized TLS termination and SSL certificate management

Installing Nginx Ingress Controller

# Using Helm (recommended)
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 \
  --set controller.nodeSelector."kubernetes\.io/os"=linux
 
# Verify
kubectl get pods -n ingress-nginx
kubectl get service ingress-nginx-controller -n ingress-nginx
# EXTERNAL-IP shows the cloud load balancer's IP/hostname

Basic Ingress Resource

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-ingress
  namespace: production
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: api-service
            port:
              number: 80
kubectl apply -f ingress.yaml
kubectl get ingress -n production
kubectl describe ingress api-ingress -n production

Path-Based Routing

Route different URL paths to different backend services:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: main-ingress
  namespace: production
  annotations:
    nginx.ingress.kubernetes.io/use-regex: "true"
spec:
  ingressClassName: nginx
  rules:
  - host: app.example.com
    http:
      paths:
      - path: /api/v1
        pathType: Prefix
        backend:
          service:
            name: api-v1-service
            port:
              number: 80
      - path: /api/v2
        pathType: Prefix
        backend:
          service:
            name: api-v2-service
            port:
              number: 80
      - path: /static
        pathType: Prefix
        backend:
          service:
            name: static-service
            port:
              number: 80
      - path: /
        pathType: Prefix
        backend:
          service:
            name: frontend-service
            port:
              number: 80

Host-Based Routing

Route different hostnames to different services:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: multi-host-ingress
  namespace: production
spec:
  ingressClassName: nginx
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: api-service
            port:
              number: 80
  - host: admin.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: admin-service
            port:
              number: 80
  - host: www.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: web-service
            port:
              number: 80

TLS with cert-manager (Automated Certificates)

cert-manager automatically provisions and renews Let's Encrypt certificates:

# Install cert-manager
helm repo add jetstack https://charts.jetstack.io
helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --set installCRDs=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: ops@example.com
    privateKeySecretRef:
      name: letsencrypt-prod-key
    solvers:
    - http01:
        ingress:
          class: nginx
# Ingress with TLS and automatic certificate provisioning
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: tls-ingress
  namespace: production
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - api.example.com
    - www.example.com
    secretName: example-tls-cert  # cert-manager creates this Secret
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: api-service
            port:
              number: 80

Common Nginx Ingress Annotations

metadata:
  annotations:
    # Rate limiting
    nginx.ingress.kubernetes.io/limit-rps: "100"
    nginx.ingress.kubernetes.io/limit-connections: "50"
 
    # Request size limit
    nginx.ingress.kubernetes.io/proxy-body-size: "50m"
 
    # Timeouts
    nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
 
    # CORS
    nginx.ingress.kubernetes.io/enable-cors: "true"
    nginx.ingress.kubernetes.io/cors-allow-origin: "https://www.example.com"
 
    # Authentication (using oauth2-proxy)
    nginx.ingress.kubernetes.io/auth-url: "https://auth.example.com/oauth2/auth"
    nginx.ingress.kubernetes.io/auth-signin: "https://auth.example.com/oauth2/start"
 
    # Canary deployment (route 10% of traffic to new version)
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"
 
    # Rewrite URL path
    nginx.ingress.kubernetes.io/rewrite-target: /$2

AWS Application Load Balancer Controller

On EKS, use the AWS Load Balancer Controller for ALB-native Ingress:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-alb-ingress
  namespace: production
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-east-1:123:certificate/abc
    alb.ingress.kubernetes.io/ssl-redirect: '443'
    alb.ingress.kubernetes.io/healthcheck-path: /health
spec:
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: api-service
            port:
              number: 80

Kubernetes Gateway API (Next Generation)

Gateway API is the successor to Ingress, now GA in Kubernetes 1.28+:

# Gateway (cluster-level, managed by platform team)
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: main-gateway
  namespace: production
spec:
  gatewayClassName: nginx
  listeners:
  - name: https
    port: 443
    protocol: HTTPS
    tls:
      mode: Terminate
      certificateRefs:
      - name: example-tls-cert
 
---
# HTTPRoute (namespace-level, managed by app teams)
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: api-route
  namespace: production
spec:
  parentRefs:
  - name: main-gateway
  hostnames:
  - api.example.com
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /api
    backendRefs:
    - name: api-service
      port: 80

Common Mistakes

  • Not setting ingressClassName — the ingress may be ignored if the cluster has multiple controllers
  • Forgetting to redirect HTTP to HTTPS (ssl-redirect: "true") after enabling TLS
  • Using the wrong pathTypeExact matches only that path; Prefix matches all subpaths
  • Not installing cert-manager before deploying TLS Ingress with cert-manager.io annotations
  • Creating Ingress resources across namespaces without understanding that each namespace needs its own Ingress or uses a shared gateway

Best Practices

  • Use cert-manager with Let's Encrypt for all HTTPS — never manage certificates manually
  • Set rate limiting annotations on public-facing Ingresses to prevent abuse
  • Use ssl-redirect: "true" to force HTTPS on all Ingress resources
  • Monitor Ingress controller metrics (request rate, error rate, latency) with Prometheus annotations
  • Migrate to Gateway API for new clusters — it provides better role separation between platform and application teams

Key Takeaways

  • Ingress routes external HTTP/HTTPS traffic to internal Services based on hostname and URL path
  • A single Ingress controller (Nginx, AWS ALB) handles routing for all services — one cloud load balancer for the whole cluster
  • cert-manager with Let's Encrypt automates TLS certificate provisioning and renewal for Ingress resources
  • Nginx Ingress annotations enable rate limiting, CORS, authentication, and canary deployments without code changes
  • AWS ALB Ingress Controller on EKS integrates natively with ACM certificates, WAF, and target group health checks
  • Gateway API (GA in K8s 1.28+) replaces Ingress with better role separation and more expressive routing rules
  • pathType: Prefix matches the path and all sub-paths; pathType: Exact matches only the exact path
  • Always monitor the Ingress controller itself — it is a single point of failure for all external traffic

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading