5.3 LoadBalancer — Cloud-Native Exposure
⏱️ 5 min read · 6 min hands-on · 🟡 Intermediate
TL;DR: A LoadBalancer Service provisions a real cloud load balancer (AWS ELB, Azure ALB, GCP CLB) with a public IP automatically. On Minikube, use
minikube tunnelto simulate this. In production, this is the standard way to expose services to the internet.
After this section you will be able to:
- Provision cloud provider load balancers automatically with
type: LoadBalancer- Simulate external cloud load balancers locally in Minikube using
minikube tunnel- Configure traffic policies (
externalTrafficPolicy: Local) to preserve client source IPs
How LoadBalancer Works
# service-loadbalancer.yaml
apiVersion: v1
kind: Service
metadata:
name: web-lb
spec:
type: LoadBalancer
selector:
app: web
ports:
- port: 80
targetPort: 80
graph LR
Internet -->|"Public IP<br/>203.0.113.42:80"| LB["Cloud Load Balancer<br/>(AWS ELB / Azure ALB / GCP CLB)"]
LB --> N1[Node 1<br/>NodePort]
LB --> N2[Node 2<br/>NodePort]
N1 & N2 --> SVC[ClusterIP Service]
SVC --> P1[Pod] & P2[Pod] & P3[Pod]
A LoadBalancer Service is really three services stacked:
- A ClusterIP (internal routing)
- A NodePort (on each node)
- A cloud load balancer pointed at those NodePorts
When you kubectl apply a LoadBalancer Service in a cloud cluster, the cloud controller manager calls the cloud API to provision a real load balancer. The external IP appears in the EXTERNAL-IP column after ~30–60 seconds.
# Cloud cluster — shows a real external IP
kubectl get svc web-lb
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# web-lb LoadBalancer 10.96.10.5 203.0.113.42 80:31456/TCP 45s
On Minikube: minikube tunnel
Minikube doesn’t have a cloud controller, so LoadBalancer Services stay in pending state without help:
kubectl get svc web-lb
# EXTERNAL-IP: <pending> ← stuck without tunnel
Fix this with minikube tunnel — it creates a network route on your machine that acts as the load balancer:
# Run in a separate terminal (requires sudo on macOS/Linux)
minikube tunnel
# Now in your original terminal:
kubectl get svc web-lb
# EXTERNAL-IP: 127.0.0.1 ← now accessible on localhost!
curl http://127.0.0.1:80
⚠️ Warning:
minikube tunnelrequires elevated privileges and must stay running. If you close the terminal, the tunnel closes andEXTERNAL-IPgoes back topending.
When to Use LoadBalancer vs Ingress
| Scenario | Use |
|---|---|
| Single service to expose | LoadBalancer — simple, one command |
| Multiple services on same domain | Ingress — HTTP routing, one LB shared |
| TCP/UDP (non-HTTP) | LoadBalancer — Ingress is HTTP-only |
| TLS termination at L7 | Ingress — with TLS certs |
| Cost-conscious (cloud bills per LB) | Ingress — one LB for many services |
💡 Tip: In production, most teams use one LoadBalancer that fronts an Ingress Controller (like NGINX), then use Ingress resources for path/host routing. This avoids paying for a separate cloud load balancer per service.
Try It
# Deploy app
kubectl create deployment web --image=nginx:1.25 --replicas=2
# Create LoadBalancer Service
kubectl expose deployment web --type=LoadBalancer --port=80
# Watch EXTERNAL-IP (starts as <pending>)
kubectl get svc web -w &
# In a new terminal, start the tunnel
# minikube tunnel ← run this in another terminal
# After tunnel starts, EXTERNAL-IP becomes 127.0.0.1
# Kill the watch
kill %1
# Access it (with tunnel running)
curl http://127.0.0.1
# Cleanup
kubectl delete deployment web
kubectl delete svc web
Key Takeaways
| # | Concept | One-liner |
|---|---|---|
| 1 | LoadBalancer = cloud LB + NodePort + ClusterIP | Three layers stacked |
| 2 | Cloud controller provisions the LB | Calls AWS/Azure/GCP API automatically |
| 3 | minikube tunnel simulates it locally | Routes 127.0.0.1 to the Service |
| 4 | Prefer Ingress for HTTP in production | One LB shared across many services = cost savings |
✅ Quick Check
Q1: You apply a LoadBalancer Service on a bare-metal cluster with no cloud provider. What is EXTERNAL-IP?
Answer
`pending` — indefinitely. Without a cloud controller manager to provision an external load balancer, the IP is never assigned. Solutions for bare-metal include **MetalLB** (assigns IPs from a configured pool) or exposing via NodePort + an external load balancer configured manually.Q2: You have 20 microservices. Should each get its own LoadBalancer Service?
Answer
No — that's 20 cloud load balancers, each costing money (~$20–30/month each on major clouds). The standard pattern is one LoadBalancer in front of an NGINX Ingress Controller, then one Ingress resource per service for HTTP routing. Chapter 6 covers this in detail.Q3: A LoadBalancer Service has port: 443. Does this mean TLS is terminated at the load balancer?