Loading content...
In the previous page, we explored Kubernetes Services—the L4 (transport layer) abstraction for routing TCP/UDP traffic to Pods. Services work remarkably well for internal communication and simple external exposure. But they have a significant limitation: Services are blind to HTTP semantics.
Consider these common requirements:
api.example.com/users to the User Service and api.example.com/orders to the Order ServiceServices cannot do any of this. They operate at Layer 4—they see IP addresses and ports, not HTTP paths, headers, or hostnames. This is where Ingress enters the picture.
By the end of this page, you will understand Ingress resources, Ingress Controllers, and how they enable sophisticated L7 traffic management. You'll learn to configure path-based and host-based routing, TLS termination, and choose between popular Ingress Controller implementations.
Ingress is a Kubernetes API resource that defines HTTP/HTTPS routing rules for external traffic. Unlike Services, which are implemented natively by kube-proxy, Ingress requires a separate component called an Ingress Controller to function.
External Traffic
│
▼
┌─────────────────┐
│ Load Balancer │ ← Cloud LB or bare metal LB
└────────┬────────┘
│
▼
┌─────────────────┐
│ Ingress │ ← Ingress Controller Pod(s)
│ Controller │ (NGINX, Traefik, etc.)
└────────┬────────┘
│ Routes based on
│ host/path rules
┌────┴────┐
▼ ▼
┌───────┐ ┌───────┐
│ Svc A │ │ Svc B │ ← ClusterIP Services
└───┬───┘ └───┬───┘
│ │
▼ ▼
Pods Pods
Key Components:
Creating an Ingress resource without an Ingress Controller does nothing. The Ingress API object is just data—the Controller is what reads it and configures the actual proxy. Every cluster needs at least one Ingress Controller deployed.
You could expose each service with its own LoadBalancer Service. But consider the costs:
| Approach | 10 Services | Monthly Cost (AWS) | TLS Certificates |
|---|---|---|---|
| 10 LoadBalancer Services | 10 LBs | ~$180/month | 10 separate certs |
| 1 Ingress + 1 LoadBalancer | 1 LB | ~$18/month | 1 wildcard or multi-SAN cert |
Ingress consolidates external access into a single entry point, dramatically reducing cost and operational complexity.
An Ingress resource defines how external HTTP(S) traffic should be routed to Services. Let's explore the full capabilities:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: main-ingress namespace: production annotations: # Controller-specific annotations (NGINX example) nginx.ingress.kubernetes.io/rewrite-target: / nginx.ingress.kubernetes.io/ssl-redirect: "true" nginx.ingress.kubernetes.io/proxy-body-size: "10m" nginx.ingress.kubernetes.io/proxy-read-timeout: "60" # CORS configuration nginx.ingress.kubernetes.io/enable-cors: "true" nginx.ingress.kubernetes.io/cors-allow-origin: "https://app.example.com" # Rate limiting nginx.ingress.kubernetes.io/limit-rps: "100" nginx.ingress.kubernetes.io/limit-connections: "50"spec: # Specify which controller should handle this Ingress ingressClassName: nginx # TLS configuration tls: - hosts: - api.example.com - app.example.com secretName: example-com-tls # Secret with TLS cert + key - hosts: - admin.example.com secretName: admin-tls # Default backend for unmatched requests defaultBackend: service: name: default-backend port: number: 80 # Routing rules rules: # Host-based routing: api.example.com - host: api.example.com http: paths: # Path-based routing within the host - path: /users pathType: Prefix # Match /users, /users/123, /users/123/profile backend: service: name: user-service port: number: 80 - path: /orders pathType: Prefix backend: service: name: order-service port: number: 80 - path: / pathType: Prefix # Catch-all for api.example.com backend: service: name: api-gateway port: number: 80 # Another host: app.example.com - host: app.example.com http: paths: - path: / pathType: Prefix backend: service: name: web-frontend port: number: 80 # Wildcard host: *.staging.example.com - host: "*.staging.example.com" http: paths: - path: / pathType: Prefix backend: service: name: staging-router port: number: 80Kubernetes supports three pathType values, and understanding them is crucial for correct routing:
| pathType | Behavior | Example Path: /api |
|---|---|---|
| Exact | Must match exactly | Only /api, not /api/ or /api/v1 |
| Prefix | Matches path prefix by segment | /api, /api/, /api/v1, /api/users |
| ImplementationSpecific | Controller decides | Varies by controller |
Important: Prefix matching is segment-based. Path /api will match /api/v1 but NOT /apikey (different segment).
| Rule Path | pathType | /api | /api/ | /api/v1 | /apikey |
|---|---|---|---|---|---|
| /api | Exact | ✅ Match | ❌ No | ❌ No | ❌ No |
| /api | Prefix | ✅ Match | ✅ Match | ✅ Match | ❌ No |
| /api/ | Prefix | ❌ No | ✅ Match | ✅ Match | ❌ No |
| / | Prefix | ✅ Match | ✅ Match | ✅ Match | ✅ Match |
Ingress Controllers handle TLS termination at the edge, decrypting HTTPS traffic before forwarding plain HTTP to backend Services. This centralizes certificate management and offloads encryption from application Pods.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
# Step 1: Create TLS Secret with certificate and private keyapiVersion: v1kind: Secretmetadata: name: example-com-tls namespace: productiontype: kubernetes.io/tlsdata: # Base64-encoded certificate chain (PEM format) tls.crt: LS0tLS1CRUdJTi... # base64 -w 0 cert.pem # Base64-encoded private key (PEM format) tls.key: LS0tLS1CRUdJTi... # base64 -w 0 key.pem ---# Or create imperatively:# kubectl create secret tls example-com-tls \# --cert=cert.pem \# --key=key.pem \# -n production ---# Step 2: Reference in IngressapiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: secure-ingress namespace: production annotations: # Force HTTPS redirect nginx.ingress.kubernetes.io/ssl-redirect: "true" # HSTS header nginx.ingress.kubernetes.io/hsts: "true" nginx.ingress.kubernetes.io/hsts-max-age: "31536000" nginx.ingress.kubernetes.io/hsts-include-subdomains: "true"spec: ingressClassName: nginx tls: - hosts: - api.example.com - www.example.com secretName: example-com-tls # Must be in same namespace rules: - host: api.example.com http: paths: - path: / pathType: Prefix backend: service: name: api-service port: number: 80cert-manager is the de facto standard for automated certificate management in Kubernetes. It integrates with Let's Encrypt and other CAs to automatically provision and renew certificates.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
# Step 1: Install cert-manager (Helm)# helm repo add jetstack https://charts.jetstack.io# helm install cert-manager jetstack/cert-manager \# --namespace cert-manager \# --create-namespace \# --set installCRDs=true ---# Step 2: Create ClusterIssuer for Let's EncryptapiVersion: cert-manager.io/v1kind: ClusterIssuermetadata: name: letsencrypt-prodspec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: platform-team@example.com privateKeySecretRef: name: letsencrypt-prod-account-key solvers: # HTTP-01 challenge solver (requires Ingress) - http01: ingress: class: nginx # OR DNS-01 challenge (for wildcards, requires DNS provider) # - dns01: # cloudDNS: # project: my-gcp-project # serviceAccountSecretRef: # name: clouddns-dns01-solver-svc-acct # key: key.json ---# Step 3: Annotate Ingress for automatic certificateapiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: auto-tls-ingress namespace: production annotations: # Tell cert-manager to issue certificate cert-manager.io/cluster-issuer: "letsencrypt-prod" nginx.ingress.kubernetes.io/ssl-redirect: "true"spec: ingressClassName: nginx tls: - hosts: - api.example.com secretName: api-example-com-tls # cert-manager creates this automatically! rules: - host: api.example.com http: paths: - path: / pathType: Prefix backend: service: name: api-service port: number: 80 # cert-manager will:# 1. Detect the annotation and tls.hosts# 2. Create a Certificate resource# 3. Solve the ACME challenge (HTTP-01 or DNS-01)# 4. Store the issued cert in the Secret# 5. Renew automatically before expiry• Use ClusterIssuer for shared issuers across namespaces; use Issuer for namespace-scoped. • Start with Let's Encrypt staging for testing to avoid rate limits. • For wildcard certificates, DNS-01 challenge is required. • Set up monitoring for certificate expiration (cert-manager exports Prometheus metrics).
Kubernetes doesn't ship with a default Ingress Controller—you must choose and deploy one. The ecosystem offers numerous options, each with different strengths. Here's an in-depth comparison of the most popular choices:
| Controller | Base Technology | Best For | Key Features |
|---|---|---|---|
| NGINX Ingress | NGINX | General purpose, high performance | Lua scripting, canary deployments, rate limiting, WAF |
| Traefik | Go (native) | Dynamic environments, GitOps | Auto-discovery, Let's Encrypt, middlewares, observability |
| HAProxy Ingress | HAProxy | High-performance L4/L7 | Connection queuing, advanced LB algorithms |
| Kong Ingress | Kong (NGINX) | API Gateway features | Plugins (auth, rate limiting), developer portal |
| Istio Gateway | Envoy | Service mesh environments | mTLS, traffic management, observability |
| AWS ALB | AWS ALB | AWS-native deployments | Native ALB features, WAF, Cognito auth |
| GKE Ingress | Google Cloud LB | GKE clusters | Global LB, Cloud CDN, Cloud Armor |
| Contour | Envoy | Multi-team clusters | HTTPProxy CRD, delegation, rate limiting |
The NGINX Ingress Controller is the most widely deployed option. It comes in two variants:
123456789101112131415161718192021222324252627282930
# Install NGINX Ingress Controller (Helm method - recommended)helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginxhelm repo update # Basic installationhelm install ingress-nginx ingress-nginx/ingress-nginx \ --namespace ingress-nginx \ --create-namespace # Production installation with recommended settingshelm install ingress-nginx ingress-nginx/ingress-nginx \ --namespace ingress-nginx \ --create-namespace \ --set controller.replicaCount=3 \ --set controller.nodeSelector."node.kubernetes.io/purpose"=ingress \ --set controller.resources.requests.memory=256Mi \ --set controller.resources.requests.cpu=100m \ --set controller.autoscaling.enabled=true \ --set controller.autoscaling.minReplicas=3 \ --set controller.autoscaling.maxReplicas=10 \ --set controller.service.externalTrafficPolicy=Local \ --set controller.metrics.enabled=true \ --set controller.metrics.serviceMonitor.enabled=true # Verify installationkubectl get pods -n ingress-nginxkubectl get svc -n ingress-nginx # Check the external IP/hostnamekubectl get svc ingress-nginx-controller -n ingress-nginx -o jsonpath='{.status.loadBalancer.ingress[0]}'Traefik is designed for dynamic environments with automatic service discovery. It's particularly popular in Docker and smaller Kubernetes deployments.
123456789101112131415
# Install Traefik via Helmhelm repo add traefik https://helm.traefik.io/traefikhelm repo update helm install traefik traefik/traefik \ --namespace traefik \ --create-namespace \ --set deployment.replicas=3 \ --set ingressRoute.dashboard.enabled=true \ --set providers.kubernetesIngress.enabled=true \ --set providers.kubernetesCRD.enabled=true \ --set metrics.prometheus.enabled=true # Traefik uses its own CRD (IngressRoute) for advanced features# but also supports standard Kubernetes Ingress123456789101112131415161718192021222324252627282930313233343536373839
# Traefik's native CRD offers more features than standard IngressapiVersion: traefik.containo.us/v1alpha1kind: IngressRoutemetadata: name: my-ingressroute namespace: productionspec: entryPoints: - websecure # HTTPS entrypoint routes: - match: Host(`api.example.com`) && PathPrefix(`/v1`) kind: Rule services: - name: api-v1 port: 80 weight: 100 middlewares: - name: rate-limit - name: api-auth - match: Host(`api.example.com`) && PathPrefix(`/v2`) kind: Rule services: - name: api-v2 port: 80 tls: secretName: api-example-com-tls ---# Middleware for rate limitingapiVersion: traefik.containo.us/v1alpha1kind: Middlewaremetadata: name: rate-limit namespace: productionspec: rateLimit: average: 100 burst: 200 period: 1sProduction Ingress configurations often require advanced patterns beyond basic routing. Here are the most commonly used patterns:
Gradually shift traffic to a new version to validate before full rollout:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
# Primary (stable) Ingress - receives most trafficapiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: api-ingress-stable annotations: nginx.ingress.kubernetes.io/canary: "false"spec: ingressClassName: nginx rules: - host: api.example.com http: paths: - path: / pathType: Prefix backend: service: name: api-service-stable port: number: 80 ---# Canary Ingress - receives percentage of trafficapiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: api-ingress-canary annotations: nginx.ingress.kubernetes.io/canary: "true" # Weight-based: 10% of traffic to canary nginx.ingress.kubernetes.io/canary-weight: "10" # OR Header-based: route specific users to canary # nginx.ingress.kubernetes.io/canary-by-header: "X-Canary" # nginx.ingress.kubernetes.io/canary-by-header-value: "true" # OR Cookie-based: route users with specific cookie # nginx.ingress.kubernetes.io/canary-by-cookie: "canary"spec: ingressClassName: nginx rules: - host: api.example.com http: paths: - path: / pathType: Prefix backend: service: name: api-service-canary port: number: 80Switch between two complete environments:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
# Single Ingress pointing to the active environment# Switch by updating the backend service nameapiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: api-ingressspec: ingressClassName: nginx rules: - host: api.example.com http: paths: - path: / pathType: Prefix backend: service: name: api-service-blue # Switch to api-service-green port: number: 80 ---# Blue environment serviceapiVersion: v1kind: Servicemetadata: name: api-service-bluespec: selector: app: api version: blue ports: - port: 80 targetPort: 8080 ---# Green environment serviceapiVersion: v1kind: Servicemetadata: name: api-service-greenspec: selector: app: api version: green ports: - port: 80 targetPort: 8080Rewrite request paths before forwarding to backends:
12345678910111213141516171819202122232425262728293031
apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: rewrite-ingress annotations: # Rewrite /api/users/(.*) → /$1 (strip /api/users prefix) nginx.ingress.kubernetes.io/rewrite-target: /$2 # Use regex in path matching nginx.ingress.kubernetes.io/use-regex: "true"spec: ingressClassName: nginx rules: - host: api.example.com http: paths: # /api/users/profile → backend receives /profile - path: /api/users(/|$)(.*) pathType: ImplementationSpecific backend: service: name: user-service port: number: 80 # /api/orders/123 → backend receives /123 - path: /api/orders(/|$)(.*) pathType: ImplementationSpecific backend: service: name: order-service port: number: 80Integrate external authentication services:
1234567891011121314151617181920212223242526272829303132
apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: protected-ingress annotations: # External auth service nginx.ingress.kubernetes.io/auth-url: "https://auth.example.com/verify" nginx.ingress.kubernetes.io/auth-signin: "https://auth.example.com/login?rd=$scheme://$host$request_uri" nginx.ingress.kubernetes.io/auth-response-headers: "X-User-Id,X-User-Email,X-User-Roles" # Basic auth (alternative - simpler) # nginx.ingress.kubernetes.io/auth-type: basic # nginx.ingress.kubernetes.io/auth-secret: basic-auth-secret # nginx.ingress.kubernetes.io/auth-realm: "Authentication Required"spec: ingressClassName: nginx rules: - host: admin.example.com http: paths: - path: / pathType: Prefix backend: service: name: admin-dashboard port: number: 80 ---# Basic auth secret (for basic-auth approach)# Create: htpasswd -c auth admin# kubectl create secret generic basic-auth-secret --from-file=authLarge organizations often run multiple Ingress Controllers for different purposes:
The ingressClassName field directs each Ingress to the correct controller.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
# IngressClass for public traffic (NGINX)apiVersion: networking.k8s.io/v1kind: IngressClassmetadata: name: nginx-public annotations: ingressclass.kubernetes.io/is-default-class: "true" # Default for unspecifiedspec: controller: k8s.io/ingress-nginx parameters: apiGroup: k8s.nginx.org kind: IngressNginxConfig name: nginx-public-config ---# IngressClass for internal traffic (separate NGINX deployment)apiVersion: networking.k8s.io/v1kind: IngressClassmetadata: name: nginx-internalspec: controller: k8s.io/ingress-nginx-internal # Different controller name ---# Public-facing APIapiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: public-apispec: ingressClassName: nginx-public # Routed by public controller rules: - host: api.example.com http: paths: - path: / pathType: Prefix backend: service: name: public-api port: number: 80 ---# Internal admin dashboardapiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: internal-adminspec: ingressClassName: nginx-internal # Routed by internal controller rules: - host: admin.internal.example.com http: paths: - path: / pathType: Prefix backend: service: name: admin-dashboard port: number: 80Before Kubernetes 1.18, controller selection was done via the kubernetes.io/ingress.class annotation. The ingressClassName field is the modern, standardized approach. Most controllers support both for backward compatibility.
Ingress Controllers are critical infrastructure—if they fail, external traffic stops. Comprehensive monitoring is essential.
1234567891011121314151617181920212223242526272829303132333435
# Enable metrics in NGINX Ingress (Helm values)controller: metrics: enabled: true port: 10254 service: annotations: prometheus.io/scrape: "true" prometheus.io/port: "10254" serviceMonitor: enabled: true namespace: monitoring additionalLabels: release: prometheus ---# ServiceMonitor for Prometheus OperatorapiVersion: monitoring.coreos.com/v1kind: ServiceMonitormetadata: name: nginx-ingress namespace: monitoring labels: release: prometheusspec: selector: matchLabels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/component: controller namespaceSelector: matchNames: - ingress-nginx endpoints: - port: metrics interval: 30s| Metric | Description | Alert Threshold |
|---|---|---|
nginx_ingress_controller_requests | Total requests by status code | High 5xx rate |
nginx_ingress_controller_request_duration_seconds | Request latency histogram | p99 > 1s |
nginx_ingress_controller_nginx_process_connections | Active connections | Near max connections |
nginx_ingress_controller_ssl_certificate_expiry_seconds | Cert expiry | < 7 days |
nginx_ingress_controller_config_reload_success | Config reload status | Any failure |
Ingress adds a layer of abstraction that can make troubleshooting complex. Here's a systematic debugging approach:
123456789101112131415161718192021222324252627282930313233343536
# Step 1: Check Ingress Controller is runningkubectl get pods -n ingress-nginxkubectl logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx -f # Step 2: Verify Ingress resource is created correctlykubectl get ingress my-ingress -o yamlkubectl describe ingress my-ingress # Step 3: Check IngressClass assignmentkubectl get ingressclasskubectl describe ingressclass nginx # Step 4: Verify backend Service and Endpointskubectl get svc my-backend-servicekubectl get endpoints my-backend-service # Should show Pod IPs # Step 5: Check controller configuration (NGINX specific)# Enter the controller pod and check generated configkubectl exec -n ingress-nginx deploy/ingress-nginx-controller -- \ cat /etc/nginx/nginx.conf | grep -A 20 "server_name my-host" # Step 6: Test from inside clusterkubectl run -it --rm debug --image=curlimages/curl -- \ curl -H "Host: api.example.com" http://ingress-nginx-controller.ingress-nginx/path # Step 7: Check TLS certificateskubectl get secret my-tls-secret -o yaml# Decode and verify: echo "<base64>" | base64 -d | openssl x509 -text # Step 8: View controller logs for specific hostkubectl logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx | \ grep "api.example.com" # Step 9: Check for configuration errorskubectl exec -n ingress-nginx deploy/ingress-nginx-controller -- \ nginx -t # Test NGINX config syntax| Symptom | Likely Cause | Solution |
|---|---|---|
| 404 Not Found | Path doesn't match any rule | Check pathType and path values; verify host header |
| 503 Service Unavailable | No healthy backends | Check Endpoints; verify readinessProbe |
| SSL Error | Certificate mismatch or missing | Verify TLS secret exists; check cert hostnames |
| Ingress not working at all | Controller not watching namespace | Check controller logs; verify IngressClass |
| Redirect loop | SSL redirect + backend also redirecting | Disable annotation or fix backend |
| Slow responses | Backend slow or controller overloaded | Check backend latency; scale controller |
| WebSocket fails | Timeouts too short | Increase proxy-read-timeout annotation |
We've covered Ingress Controllers comprehensively. Let's consolidate the key takeaways:
What's Next:
Ingress controls traffic at the edge, but what about internal traffic between Pods? In the next page, we'll explore Network Policies—Kubernetes' built-in firewall for controlling Pod-to-Pod communication and implementing zero-trust networking.
You now understand Ingress Controllers—how they provide L7 traffic management, TLS termination, and advanced routing capabilities. You can configure Ingress resources, choose appropriate controllers, and troubleshoot common issues. Next, we'll secure internal traffic with Network Policies.