A LoadBalancer Service for every microservice is how you accidentally spend $3,000 a month on cloud infrastructure. Ingress is how you expose everything through one door — intelligently.
Kubernetes Zero to Hero · Episode 07 ·
In Episode 04, we exposed services to the outside world using the LoadBalancer Service type. It worked. But here's what we didn't say out loud: in a real microservices application with 10, 20, or 50 services, creating a LoadBalancer for each one means provisioning 10, 20, or 50 cloud load balancers. At roughly $15–25/month per load balancer on AWS or GCP, that math gets painful fast.
There’s a better way. One load balancer. One entry point. Intelligent routing to every service behind it based on the URL path or hostname. That’s exactly what Ingress gives you.
This episode covers the complete Ingress model — what an Ingress Controller is, how Ingress rules work, path-based and host-based routing, TLS termination, and how to set it up on both a local cluster and a cloud cluster.
The Problem With One LoadBalancer Per Service
Let’s make the cost and complexity problem concrete.
Without Ingress:
Internet
├── LoadBalancer ($$$) → frontend-service:80
├── LoadBalancer ($$$) → api-service:8080
├── LoadBalancer ($$$) → auth-service:8081
├── LoadBalancer ($$$) → payments-service:8082
└── LoadBalancer ($$$) → admin-service:3000
5 services = 5 load balancers = 5 public IPs = $$$ every month
With Ingress:
Internet
└── LoadBalancer (one, shared) → Ingress Controller
├── / → frontend-service:80
├── /api → api-service:8080
├── /auth → auth-service:8081
├── /payments → payments-service:8082
└── /admin → admin-service:3000
5 services = 1 load balancer = 1 public IP = significantly less $$$
Beyond cost, Ingress gives you something equally valuable: a single, centralised place to manage SSL/TLS termination, authentication headers, rate limiting, request routing, and redirects — without changing your application code.
Two Distinct Things: Ingress vs Ingress Controller
This distinction trips up nearly everyone who encounters Ingress for the first time.
Ingress is a Kubernetes API object — a YAML manifest you write that defines routing rules. “Send requests for /api to api-service on port 8080." It's just configuration. By itself, it does nothing.
Ingress Controller is the software that reads those Ingress rules and actually implements them. It runs as a pod inside your cluster, watches the API Server for Ingress objects, and configures a reverse proxy accordingly.
You write: Ingress resource (routing rules)
↓ watched by
Ingress Controller (NGINX, Traefik, HAProxy, Istio...)
↓ configures
Reverse Proxy (the actual traffic handler)
↓ routes
Your Services and Pods
Kubernetes ships with the Ingress API but ships with zero Ingress Controllers. You must install one yourself. This is intentional — different teams have different needs, and the ecosystem has delivered excellent options.
Popular Ingress Controllers:
Controller → Best For → Notes
NGINX Ingress → General purpose, most widely used → Official k8s project, battle-tested
Traefik → Dynamic environments, auto-discovery → Great developer experience, built-in dashboard
HAProxy → High performance, complex routing → Enterprise-grade, excellent at TCP/UDP
AWS ALB Ingress → AWS-native deployments → Provisions actual ALB per Ingress, native AWS features
Istio Gateway → Service mesh environments → Full observability, mTLS, advanced traffic management
Nginx Plus → Enterprise with advanced features → Commercial, WAF, rate limiting built in
For this series, we’ll use NGINX Ingress Controller — the most widely deployed, best-documented, and works identically on any cluster.
Installing NGINX Ingress Controller
On Minikube (Local Development)
# One command — minikube ships NGINX as a built-in addon
minikube addons enable ingress
# Verify it's running
kubectl get pods -n ingress-nginx
NAME READY STATUS AGE
ingress-nginx-controller-7d4b9c8d6f-x2k9f 1/1 Running 30s
# Get the minikube IP — you'll use this to test
minikube ip
192.168.49.2
On Your kubeadm EC2 Cluster (From the Bonus Episode)
# Install NGINX Ingress Controller via official manifest
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.9.4/deploy/static/provider/baremetal/deploy.yaml
# Wait for the controller pod to be ready
kubectl wait --namespace ingress-nginx \
--for=condition=ready pod \
--selector=app.kubernetes.io/component=controller \
--timeout=120s
# Check the service — on bare metal it's NodePort
kubectl get service -n ingress-nginx
NAME TYPE PORT(S)
ingress-nginx-controller NodePort 80:30080/TCP,443:30443/TCP
On a Cloud Cluster (EKS / GKE / AKS)
# Install NGINX Ingress with cloud LoadBalancer support
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.9.4/deploy/static/provider/cloud/deploy.yaml
# The controller Service becomes type LoadBalancer automatically
kubectl get service -n ingress-nginx
NAME TYPE EXTERNAL-IP
ingress-nginx-controller LoadBalancer 34.120.45.210 ← one shared public IP
Your First Ingress Resource
Let’s deploy a simple application and expose it through Ingress.
# echo-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: echo-app
spec:
replicas: 2
selector:
matchLabels:
app: echo
template:
metadata:
labels:
app: echo
spec:
containers:
- name: echo
image: hashicorp/http-echo:0.2.3
args: ["-text=Hello from echo-app!"]
ports:
- containerPort: 5678
---
apiVersion: v1
kind: Service
metadata:
name: echo-service
spec:
selector:
app: echo
ports:
- port: 80
targetPort: 5678
# echo-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: echo-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: / # strip the path prefix before forwarding
spec:
ingressClassName: nginx # which Ingress Controller handles this
rules:
- http:
paths:
- path: /echo
pathType: Prefix
backend:
service:
name: echo-service
port:
number: 80
kubectl apply -f echo-deployment.yaml
kubectl apply -f echo-ingress.yaml
# Check Ingress status — ADDRESS should populate
kubectl get ingress echo-ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
echo-ingress nginx * 192.168.49.2 80 15s
# Test it
curl http://192.168.49.2/echo
Hello from echo-app!
Path-Based Routing: Multiple Services, One Entry Point
Now the real power. Let’s route different URL paths to different backend services.
# path-based-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: path-based-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: /()(.*) # root path → frontend
pathType: ImplementationSpecific
backend:
service:
name: frontend-service
port:
number: 80
- path: /api(/|$)(.*) # /api/* → API service
pathType: ImplementationSpecific
backend:
service:
name: api-service
port:
number: 80
- path: /admin(/|$)(.*) # /admin/* → admin service
pathType: ImplementationSpecific
backend:
service:
name: admin-service
port:
number: 80
kubectl apply -f path-based-ingress.yaml
# Test each path
curl http://192.168.49.2/
Frontend Service
curl http://192.168.49.2/api/users
API Service v1
curl http://192.168.49.2/admin/dashboard
Admin Panel
One IP. Three services. Clean path-based routing.
Path Types Explained
# Exact — only matches the exact path, nothing else
- path: /login
pathType: Exact
# matches: /login
# doesn't match: /login/, /login/reset
# Prefix — matches the path and anything after it
- path: /api
pathType: Prefix
# matches: /api, /api/, /api/users, /api/v1/anything
# ImplementationSpecific — regex patterns, controller-dependent
- path: /api(/|$)(.*)
pathType: ImplementationSpecific
# NGINX-specific regex with capture groups for rewriting
Host-Based Routing: Virtual Hosting
Route traffic based on the hostname in the HTTP request, not the URL path. Perfect when you have multiple domains or subdomains pointing to the same cluster.
# host-based-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: host-based-ingress
spec:
ingressClassName: nginx
rules:
- host: app.example.com # requests for this domain
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: frontend-service
port:
number: 80
- host: api.example.com # requests for this domain
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
- host: admin.example.com # requests for this domain
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: admin-service
port:
number: 80
# Test locally by spoofing the Host header
INGRESS_IP=192.168.49.2
curl -H "Host: app.example.com" http://$INGRESS_IP/
Frontend Service
curl -H "Host: api.example.com" http://$INGRESS_IP/users
API Service v1
curl -H "Host: admin.example.com" http://$INGRESS_IP/
Admin Panel
In production, you’d point your DNS records for all three subdomains to the Ingress Controller’s external IP. The Host header is set automatically by the browser or HTTP client.
TLS Termination: HTTPS in Kubernetes
The Ingress Controller handles TLS termination — it decrypts HTTPS traffic at the edge and forwards plain HTTP to your backend services. Your applications don’t need to know anything about TLS certificates.
Step 1: Create a TLS Secret
# Generate a self-signed certificate for testing
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout tls.key \
-out tls.crt \
-subj "/CN=app.example.com/O=MyOrg"
# Create the TLS Secret
kubectl create secret tls app-tls-secret \
--cert=tls.crt \
--key=tls.key
# Verify
kubectl get secret app-tls-secret
NAME TYPE DATA AGE
app-tls-secret kubernetes.io/tls 2 5s
Step 2: Reference the Secret in Your Ingress
# tls-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: tls-ingress
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true" # force HTTP → HTTPS redirect
spec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
secretName: app-tls-secret # reference your TLS Secret
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: frontend-service
port:
number: 80
kubectl apply -f tls-ingress.yaml
# Test HTTPS (skip cert verification for self-signed)
curl -k https://app.example.com/
Frontend Service
# HTTP automatically redirects to HTTPS
curl -v http://app.example.com/
# < HTTP/1.1 308 Permanent Redirect
# < Location: https://app.example.com/
Automatic TLS with cert-manager
For production, you don’t manage certificates manually. cert-manager automates the full Let’s Encrypt certificate lifecycle — issuance, renewal, storage as Secrets — all triggered by annotations on your Ingress.
# Install cert-manager
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml
# Verify
kubectl get pods -n cert-manager
NAME READY STATUS AGE
cert-manager-5c6866597-zw7kh 1/1 Running 30s
cert-manager-cainjector-577f6d9fd7-tr77l 1/1 Running 30s
cert-manager-webhook-787858fcdb-nlzsq 1/1 Running 30s
# cluster-issuer.yaml — Let's Encrypt production issuer
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: your-email@example.com # Let's Encrypt notifications
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
- http01:
ingress:
class: nginx # cert-manager uses your Ingress to verify domain
# auto-tls-ingress.yaml — cert-manager handles everything
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: auto-tls-ingress
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod # triggers cert-manager
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
secretName: app-tls-cert # cert-manager creates and manages this Secret
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: frontend-service
port:
number: 80
kubectl apply -f cluster-issuer.yaml
kubectl apply -f auto-tls-ingress.yaml
# cert-manager provisions the certificate automatically
kubectl get certificate
NAME READY SECRET AGE
app-tls-cert True app-tls-cert 2m
# Renewals happen automatically before expiry — you never touch this again
💡 cert-manager is effectively mandatory for production Kubernetes. Managing TLS certificates manually is operationally painful and error-prone. Let it handle issuance and renewal automatically.
Useful NGINX Ingress Annotations
Annotations are how you configure NGINX-specific behaviour on individual Ingress resources:
metadata:
annotations:
# Rate limiting
nginx.ingress.kubernetes.io/limit-rps: "10" # max 10 req/sec per IP
nginx.ingress.kubernetes.io/limit-connections: "5" # max 5 connections per IP
# Timeouts
nginx.ingress.kubernetes.io/proxy-connect-timeout: "30"
nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
nginx.ingress.kubernetes.io/proxy-send-timeout: "60"
# Request size
nginx.ingress.kubernetes.io/proxy-body-size: "50m" # max upload size
# CORS
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-origin: "https://app.example.com"
# Redirect HTTP to HTTPS
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
# Custom error pages
nginx.ingress.kubernetes.io/custom-http-errors: "404,503"
nginx.ingress.kubernetes.io/default-backend: error-page-service
# Basic auth
nginx.ingress.kubernetes.io/auth-type: basic
nginx.ingress.kubernetes.io/auth-secret: basic-auth-secret
nginx.ingress.kubernetes.io/auth-realm: "Authentication Required"
# Whitelist IPs
nginx.ingress.kubernetes.io/whitelist-source-range: "10.0.0.0/8,172.16.0.0/12"
Debugging Ingress
Ingress has no ADDRESS
kubectl get ingress my-ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
my-ingress nginx * 80 5m
# ↑ empty — Ingress Controller not running
# Check the Ingress Controller pod
kubectl get pods -n ingress-nginx
# Check controller logs
kubectl logs -n ingress-nginx \
$(kubectl get pods -n ingress-nginx -o name | grep controller) \
--tail=50
404 from Ingress but Service works directly
# 1. Does the ingressClassName match?
kubectl get ingress my-ingress -o yaml | grep ingressClassName
kubectl get ingressclass # must match
# 2. Describe the Ingress — check the Rules section carefully
kubectl describe ingress my-ingress
# 3. Do Endpoints exist for the backend Service?
kubectl get endpoints <backend-service>
# Empty = label mismatch between Service selector and pod labels
# 4. Check NGINX logs for the specific request
kubectl logs -n ingress-nginx \
$(kubectl get pods -n ingress-nginx -o name | grep controller) \
--tail=100 | grep "404"
502 Bad Gateway
# Ingress reached the Service but the backend pod is broken
kubectl get pods -l app=<your-app>
kubectl logs <pod-name>
# Does targetPort match the container's actual port?
kubectl describe service <service-name>
kubectl describe pod <pod-name> | grep -A5 Ports
TLS Certificate Not Working
# Check the Secret exists and has the right type
kubectl get secret <tls-secret-name>
kubectl describe secret <tls-secret-name> # must be kubernetes.io/tls type
# cert-manager certificate status
kubectl get certificate
kubectl describe certificate <cert-name>
# cert-manager logs
kubectl logs -n cert-manager deploy/cert-manager --tail=50
Hands-On Lab
# 1. Enable Ingress on minikube
minikube addons enable ingress
INGRESS_IP=$(minikube ip)
# 2. Deploy three echo services
for svc in frontend api admin; do
kubectl create deployment $svc \
--image=hashicorp/http-echo:0.2.3 \
-- -text="$svc response"
kubectl expose deployment $svc --port=80 --target-port=5678
done
# 3. Create path-based Ingress
kubectl apply -f path-based-ingress.yaml
# 4. Test each path
curl http://$INGRESS_IP/
curl http://$INGRESS_IP/api/test
curl http://$INGRESS_IP/admin/
# 5. Add TLS with a self-signed cert
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout tls.key -out tls.crt \
-subj "/CN=$INGRESS_IP"
kubectl create secret tls lab-tls --cert=tls.crt --key=tls.key
kubectl apply -f tls-ingress.yaml
# 6. Test HTTPS and the redirect
curl -k https://$INGRESS_IP/
curl -v http://$INGRESS_IP/ # watch for 308 redirect
# 7. Live rate limiting — add annotation, then hammer the endpoint
kubectl annotate ingress path-based-ingress \
nginx.ingress.kubernetes.io/limit-rps="2"
for i in $(seq 1 10); do
curl -s -o /dev/null -w "%{http_code}\n" http://$INGRESS_IP/
done
# 200 200 429 429 429 ... rate limiting in action
# 8. Clean up
kubectl delete deployment frontend api admin
kubectl delete service frontend api admin
kubectl delete ingress --all
kubectl delete secret lab-tls
Step 7 is the one to pay attention to. Watching 429 Too Many Requests kick in live — from a single annotation change, no application code touched — is what makes Ingress feel like a proper platform capability rather than just a routing layer.
What’s Next
In Episode 08, we tackle RBAC and Security Fundamentals — controlling who can do what in your cluster. As your team grows and your cluster hosts more workloads, you need fine-grained access control. We’ll cover Roles, ClusterRoles, RoleBindings, ServiceAccounts, and the security posture decisions that determine how locked down your cluster actually is.
Episode Topic
EP 01 Introduction to Kubernetes
EP 02 Kubernetes Architecture & Components
EP 03 Pods, Deployments & ReplicaSets
EP 04 Services & Networking
EP 05 ConfigMaps, Secrets & Environment Config
EP 06 Storage: PersistentVolumes & StatefulSets
Bonus Setting Up a Cluster with kubeadm on EC2
Bonus Network Policy Deep Dive
EP 07 ← You are here Ingress Controllers & Load Balancing
EP 08 RBAC & Security Fundamentals
EP 09 Monitoring with Prometheus & Grafana
EP 10 Production Hardening & Helm Charts
Before the Next Article
Make sure you can answer these without notes:
- What is the difference between an Ingress resource and an Ingress Controller?
- You have three services under app.example.com, api.example.com, and admin.example.com. How many Ingress resources could you write them in? What's the trade-off?
- Your Ingress has an ADDRESS but requests return 404. What are the first three things you check?
- Why is cert-manager considered effectively mandatory for production?
One entry point. Infinite routing possibilities. TLS handled automatically. Rate limiting, auth, CORS — all configurable without touching your application code. Ingress is where Kubernetes stops being just a container scheduler and starts feeling like a proper application platform. See you in Episode 08.