Service Types: ClusterIP, NodePort and LoadBalancer
A NodePort answers on all four nodes, including the ones running nothing. A LoadBalancer stays <pending> indefinitely and logs no event, because nobody is listening for the request. And a Pod that is Running is not necessarily in the endpoint list.
Networking and Discovery Guide 19 of 46 Beginner
- Kubernetes1.36.4
- Cluster4 nodes
- Runtimecontainerd 2.2.6
- CNICalico v3.32.1
- TimeAbout 17 min
- Reviewed23 August 2026
Written against the versions above. ClusterIP and NodePort are core Kubernetes and behave the same everywhere. LoadBalancer depends entirely on what is watching for it: a managed cluster has that built in, and a self-managed one needs something like MetalLB installed before the type does anything at all.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| CKA1001 | 192.168.0.175 | Ubuntu 26.04 LTS | Control Plane Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE01 | 192.168.0.176 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE02 | 192.168.0.177 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE03 | 192.168.0.178 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
Before you start
- A cluster with more than one node, so the NodePort step can be tested from a node that is not running the Pod.
- A Deployment with a Service in front of it. This session uses
netlab(aserverPod and afleetDeployment) andmlb(awebDeployment), and the nodes are reachable on 192.168.0.175-178.
-
NodePort opens the same port on every node
A NodePort Service is a ClusterIP with one addition: the same high port is opened on every node in the cluster.
NodePort port=80 nodePort=30080portis the Service's own port;nodePortis what is open on the machines. Now ask all four nodes for it:192.168.0.175 -> 200 192.168.0.176 -> 200 192.168.0.177 -> 200 192.168.0.178 -> 200All four answer, and only one of them is running the Pod. That is the part worth holding on to. Every node forwards traffic for that port to wherever the endpoints actually are, so you can point anything - a load balancer you manage yourself, a DNS record, a laptop - at any node and it works.
The cost is the port range. NodePorts come from 30000-32767 by default, they are cluster-wide, and two Services cannot share one. That is why NodePort is a fine way to expose a handful of things and a poor way to expose fifty.
bash Example session kubectl get svc web-nodeport -n netlab -o jsonpath="{.spec.type}{\" port=\"}{.spec.ports[0].port}{\" nodePort=\"}{.spec.ports[0].nodePort}{\"\n\"}"NodePort port=80 nodePort=30080for n in 192.168.0.175 192.168.0.176 192.168.0.177 192.168.0.178; do printf '%s -> ' $n; curl -s -o /dev/null -w '%{http_code}\n' --max-time 10 http://$n:30080; done192.168.0.175 -> 200192.168.0.176 -> 200192.168.0.177 -> 200192.168.0.178 -> 200Expected resultOne nodePort, and an HTTP 200 from every node in the cluster.
Success conditionYou got a 200 from a node that is not running the Pod.
-
A LoadBalancer with nobody listening
Now
type: LoadBalanceron a cluster with no cloud provider:web-lb LoadBalancer 10.99.38.35 <pending> 80:30987/TCP 20sIt stays that way. Read the fields and notice what is and is not there:
type=LoadBalancer clusterIP=10.99.38.35 nodePort=30987 externalIP=It already got a ClusterIP and a nodePort. A LoadBalancer Service is a NodePort Service plus a request for an external address - so the internal half works immediately and only the external half is outstanding.
And the events say nothing:
Events: <none>That silence is the whole lesson.
type: LoadBalanceris not an instruction to Kubernetes; it is a request left out for something else to collect. On a managed cluster the cloud provider's controller collects it and creates a real load balancer. Here nothing is watching, so there is nobody to report a failure - the Service simply waits, correctly, forever.bash Example session sleep 15; kubectl get svc web-lb -n mlb --no-headers | awk '{print $1, $2, $4}'web-lb LoadBalancer <pending>kubectl get svc web-lb -n mlb -o jsonpath='type={.spec.type} clusterIP={.spec.clusterIP} nodePort={.spec.ports[0].nodePort} externalIP={.status.loadBalancer.ingress}{"\n"}'type=LoadBalancer clusterIP=10.99.38.35 nodePort=30987 externalIP=kubectl describe svc web-lb -n mlb | sed -n '/Events/,$p' | head -5Events: <none>Expected resultA LoadBalancer holding a ClusterIP and a nodePort, an empty externalIP, and no events.
Success conditionYou can say why there is no error message.
-
Give it something to collect the request
Install MetalLB - a controller whose whole job is to watch for pending LoadBalancer Services on clusters that have no cloud provider - and the address appears:
web-lb LoadBalancer 192.168.0.240 80:30987/TCPTwo details from that install are worth knowing, because both surprise people.
Installing the controller was not enough on its own. It has to be told which addresses it may hand out, with an
IPAddressPool; until that existed the Service stayedexactly as before. A controller with no configuration behaves identically to no controller.And the pool is finite. This one holds five addresses. The sixth Service to ask gets:
Warning AllocationFailed metallb-controller Failed to allocate IP for "mlb/web-lb7": no available IPsNote the difference from step 2: now there is an event. Something is listening, so a failure can be reported. A
LoadBalancer with events is a controller telling you why; aLoadBalancer with none usually means no controller at all.bash Example session sleep 20; kubectl get svc web-lb -n mlb --no-headers | awk '{print $1, $2, $4, $5}'web-lb LoadBalancer 192.168.0.240 80:30987/TCPsleep 20; kubectl get svc web-lb7 -n mlb --no-headers | awk '{print $1, $4}'web-lb7 <pending>kubectl describe svc web-lb7 -n mlb | sed -n '/Events/,$p' | tail -2 ---- ------ ---- ---- ------- Warning AllocationFailed 20s metallb-controller Failed to allocate IP for "mlb/web-lb7": no available IPsExpected resultAn external IP from the pool, and an explicit AllocationFailed once the pool is exhausted.
Success conditionYou can tell a pending LoadBalancer with a controller from one without.
-
Running is not the same as receiving traffic
One more thing every Service type shares. This Pod carries the
app: fleetlabel, so it matches the Service, and it has a readiness probe pointed at a path that does not exist:notready 0/1 RunningPhase
Running, readiness0/1. And in the endpoint list:["10.244.100.37"] ready=true serving=true ["10.244.93.42"] ready=true serving=true ["10.244.86.255"] ready=true serving=true ["10.244.93.43"] ready=false serving=falseIt is in the list, marked
ready=false, and gets no traffic. That is the mechanism behind every rolling update: a new Pod is a member of the Service from the moment it exists, and its readiness decides whether requests reach it.So when a Service is only sometimes working, the endpoint list is the first thing to read - not the Pod list.
kubectl get podsshowing everything Running tells you nothing about how many of them are actually serving.bash Example session sleep 25; kubectl get pod notready -n netlab --no-headers | awk '{print $1, $2, $3}'notready 0/1 Runningkubectl get endpointslice -n netlab -l kubernetes.io/service-name=fleet -o jsonpath="{range .items[*].endpoints[*]}{.addresses}{\" ready=\"}{.conditions.ready}{\" serving=\"}{.conditions.serving}{\"\n\"}{end}"["10.244.100.37"] ready=true serving=true["10.244.93.42"] ready=true serving=true["10.244.86.255"] ready=true serving=true["10.244.93.43"] ready=false serving=falseExpected resultA Running Pod present in the endpoint list with ready=false.
Success conditionYou looked at the endpoint list rather than the Pod list.
Troubleshooting
A LoadBalancer Service never gets an external IP and there are no events.
Why: Nothing is watching for the request. Self-managed clusters have no cloud provider, so the type does nothing until a controller is installed.
Fix:Use the nodePort it already has, or install a controller such as MetalLB - and give it an IPAddressPool, without which it stays pending.
A LoadBalancer is pending and there IS an event about allocation.
Why: A controller exists and is refusing. Usually an exhausted address pool, or a requested loadBalancerIP outside the configured range.
Fix:
kubectl describe svc <name>for the reason, then widen the pool or drop the specific IP request.A NodePort works on one node and not another.
Why: Almost always a firewall or security group on the node, not Kubernetes. Every node opens the port whether or not it runs the Pod.
Fix:Test each node's IP directly on the nodePort, then check that the port range 30000-32767 is permitted inbound on the ones that fail.
A Service intermittently returns errors while every Pod shows Running.
Why: Some endpoints are ready=false. Running only means a container started; readiness decides whether traffic is sent.
Fix:
kubectl get endpointslice -l kubernetes.io/service-name=<svc> -o jsonpath='{range .items[*].endpoints[*]}{.addresses} ready={.conditions.ready}{"\n"}{end}', then look at the failing Pods' readiness probes.