Kubernetes: NGINX LoadBalancer Service
A LoadBalancer Service asks the infrastructure underneath Kubernetes for a load balancer of its own, with a single external address that clients connect to. Kubernetes does not implement that load balancer itself — it writes the request into the Service object and waits for a controller to fulfil it. Understanding that division is the difference between a Service that works in seconds and one that sits at <pending> forever.
This guide deploys NGINX, scales it to three replicas and exposes it with a LoadBalancer Service. It assumes a running cluster — see installing a Kubernetes cluster on Ubuntu if you need one.
What a LoadBalancer Service actually does
It does not replace the other Service types, it stacks on top of them. Creating one LoadBalancer Service gives you all three layers at once:
- a ClusterIP, for traffic originating inside the cluster;
- a node port on every node, exactly as described in exposing NGINX with a NodePort Service;
- a request to the cloud controller manager to provision an external load balancer that forwards to those node ports.
When the load balancer has been created, its address appears in .status.loadBalancer.ingress and shows up as the EXTERNAL-IP column in kubectl get svc.
⚠️ On bare metal, EXTERNAL-IP stays <pending>
The controller that provisions the load balancer comes from your cloud provider. On AWS, GCP, Azure or a managed service it is already running, and the address appears within a minute. On a self-built cluster — a kubeadm lab, VMs, Raspberry Pis — nothing is listening for that request, so the Service is created successfully and then waits indefinitely. It is not a broken Service; there is simply no one to answer it.
Two practical ways forward: install a bare-metal load balancer implementation such as MetalLB, which hands out addresses from a pool you define, or skip the Service type entirely and put your own proxy in front of the cluster — see running an external NGINX load balancer for a Kubernetes cluster.
Step 1: Optional — allow deployments on control plane nodes
On a small lab cluster, remove the control plane taint so replicas have somewhere to be scheduled:
kubectl taint nodes --all node-role.kubernetes.io/control-plane-
Step 2: Create the Deployment
kubectl create deployment nginx --image=nginx
Step 3: Scale the Deployment
kubectl scale deployment/nginx --replicas=3
Step 4: Expose the Deployment with a LoadBalancer
kubectl expose deployment/nginx --type=LoadBalancer --port=80 --target-port=80 --name=nginx-lb
Here --port=80 is the port the load balancer will listen on, and --target-port=80 is the container port NGINX serves on. They are independent: --port=443 --target-port=80 would front the same pods on a different external port. A node port is still allocated automatically in the background, from the range 30000–32767.
Step 5: Watch the external address being assigned
kubectl get service nginx-lb --watch
On a cluster with a load balancer controller, the EXTERNAL-IP field flips from <pending> to a real address:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-lb LoadBalancer 10.96.31.42 <pending> 80:31650/TCP 5s
nginx-lb LoadBalancer 10.96.31.42 203.0.113.24 80:31650/TCP 48s
Note the 80:31650/TCP in both lines: that is the node port the load balancer forwards to, confirming the layering described above. If the address is still pending after a couple of minutes, check the Service events for a reason:
kubectl describe service nginx-lb
No events at all usually means no controller is watching — the bare-metal case in the note above.
Step 6: Test the Service
Once an address has been assigned, request it directly:
curl http://203.0.113.24
Repeat the request a few times, then check which pods handled the traffic:
kubectl logs -l app=nginx --tail=5
You should see access log lines spread across the replicas.
Choosing between LoadBalancer, NodePort and Ingress
- LoadBalancer gives one service one external address. That is clean, but on a cloud provider each Service typically provisions its own billed load balancer, so a dozen services means a dozen of them.
- NodePort needs no external infrastructure at all, at the cost of high-numbered ports and clients that must know node addresses.
- Ingress puts a single LoadBalancer Service in front of an ingress controller, which then routes by hostname and path to many backing services and terminates TLS centrally. This is the usual answer once you are running more than a couple of HTTP services.
Common mistakes
- Waiting for an address that will never arrive. If
kubectl describe serviceshows no events, install MetalLB or use an external proxy rather than waiting. - Assuming the external IP reaches pods directly. Traffic still passes through a node port, so node firewall rules on 30000–32767 can break an otherwise healthy load balancer.
- Leaving test load balancers running. On a cloud provider, a forgotten LoadBalancer Service keeps costing money after the pods are gone. Deleting the Service releases it.
- Expecting TLS. A LoadBalancer Service forwards TCP; it does not terminate HTTPS or redirect HTTP for you. That belongs to an ingress controller or to NGINX itself — see the Nginx reverse proxy and TLS guide.
Step 7: Cleanup
Remove the Deployment and Service when you are done, which also releases the external load balancer:
kubectl delete deployment nginx
kubectl delete service nginx-lb