ClusterIP Services and Virtual IPs
A Service gets an IP address that answers HTTP and refuses ping, because nothing owns it. What makes it work is the endpoint list behind the name - and a Service whose selector matches no Pod has an empty one, which is the single most common reason a name resolves and the connection still fails.
Networking and Discovery Guide 17 of 46 Beginner
- Kubernetes1.36.4
- Cluster4 nodes
- CNICalico v3.32.1
- Runtimecontainerd 2.2.6
- TimeAbout 14 min
- Reviewed22 August 2026
Written against the versions above. Service behaviour here is core Kubernetes, not CNI-specific: the ClusterIP is programmed by kube-proxy on every node, and the endpoint list is maintained by the control plane. The addresses you see will differ; the shapes will not.
| 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 running cluster and kubectl pointed at it. Any conformant cluster works - nothing here is vendor-specific.
- A namespace to work in. This session uses
netlab, with a Pod namedserverrunning nginx and labelledapp=web, and aclientPod with curl and ping available.
-
Two Pods, two real IPs
Before the Service, the plain fact underneath it: every Pod gets its own IP, routable from any other Pod in the cluster. These two are on different workers.
That is the Kubernetes network model - one flat address space, no NAT between Pods. It is also why you do not need a Service to reach a Pod. You need one because Pod IPs do not survive a restart, and the next Pod gets a different address.
bash Example session kubectl get pods -n netlab -o custom-columns=NAME:.metadata.name,IP:.status.podIP,NODE:.spec.nodeNameNAME IP NODEclient 10.244.100.36 cka1001-node02server 10.244.86.253 cka1001-node01Expected resultTwo Pods, two addresses from the Pod CIDR, on two different nodes.
Success conditionEach Pod has its own IP and they are not on the same node.
-
Give it a name, and it stops answering ping
kubectl exposewrites a Service. It gets a ClusterIP from a different range than the Pods - here10.106.244.224, from the service range, not the Pod range.Now the part that confuses everyone the first time. Ping it:
2 packets transmitted, 0 received, 100% packet lossThen ask it for a web page over the same address, by name:
200A ClusterIP is not a host. Nothing is listening on it. There is no interface anywhere in the cluster holding that address - it is a rule, programmed on every node, that rewrites traffic for that IP and port to a real Pod IP. Ping is ICMP with no port, so there is no rule to match and nothing answers. TCP to port 80 matches, and gets rewritten.
This is why "I pinged the Service and it is down" is never evidence of anything. The name is also doing work here:
web-clusterip.netlab.svc.cluster.localis the full DNS form -- and cluster DNS resolves it to the ClusterIP.. .svc.cluster.local bash Example session kubectl expose pod server -n netlab --name=web-clusterip --port=80 --target-port=80service/web-clusterip exposedkubectl get svc web-clusterip -n netlabNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEweb-clusterip ClusterIP 10.106.244.224 <none> 80/TCP 0skubectl exec client -n netlab -- ping -c 2 -W 2 $(kubectl get svc web-clusterip -n netlab -o jsonpath='{.spec.clusterIP}') 2>&1 | tail -32 packets transmitted, 0 received, 100% packet loss, time 1059ms command terminated with exit code 1kubectl exec client -n netlab -- curl -s -o /dev/null -w '%{http_code}\n' http://web-clusterip.netlab.svc.cluster.local200Expected resultPing loses every packet and exits 1. curl over the DNS name returns 200.
Success conditionYou can state why the same address refuses ping and serves HTTP.
-
The endpoint list is the Service
A Service is two things: a stable name and address, and a list of Pod IPs that currently match its selector. That list is a separate object - an EndpointSlice - and it is what the node-level rules are built from.
Three Services here, all selecting the same single Pod, so all three slices carry the same address. Then a Deployment scaled from three replicas to one, and the list follows it down. Nobody edited a Service; the control plane rewrites the endpoint list as Pods come and go, which is the whole reason a Service is worth having.
(
Endpointsis the older object for this and is deprecated in favour ofEndpointSlicesince 1.33. Read the slice.)bash Example session kubectl get endpointslices -n netlabNAME ADDRESSTYPE PORTS ENDPOINTS AGEweb-clusterip-vrf8c IPv4 80 10.244.86.253 14sweb-lb-ncgxh IPv4 80 10.244.86.253 11sweb-nodeport-qcj9k IPv4 80 10.244.86.253 11skubectl get endpointslice -n netlab -l kubernetes.io/service-name=fleet -o jsonpath="{range .items[*].endpoints[*]}{.addresses}{\" \"}{.nodeName}{\" ready=\"}{.conditions.ready}{\"\n\"}{end}"["10.244.93.41"] cka1001-node03 ready=true["10.244.86.254"] cka1001-node01 ready=truekubectl scale deployment fleet -n netlab --replicas=1deployment.apps/fleet scaledsleep 12; kubectl get endpointslice -n netlab -l kubernetes.io/service-name=fleet -o jsonpath="{range .items[*].endpoints[*]}{.addresses}{\" ready=\"}{.conditions.ready}{\"\n\"}{end}"["10.244.100.37"] ready=trueExpected resultThree slices pointing at one Pod, then a fleet endpoint list that shrinks from two to one after scaling.
Success conditionThe endpoint list changed without anyone touching the Service.
-
A Service that selects nothing
Now the failure this guide exists for.
kubectl create service clusterip orphaninvents a selector from the Service's own name -app=orphan- and no Pod carries that label.The Service exists. The name resolves. The EndpointSlice exists too, and its endpoint list is
null:orphan-4mp28 endpoints=nullSo the connection fails with an empty reply, not a DNS error:
000 command terminated with exit code 7A name that resolves tells you nothing about whether anything is behind it. curl exit 7 is "failed to connect", which is what an empty endpoint list produces. When a Service does not work, the first thing to read is its endpoint list, and the usual cause is a selector that does not match the labels the Pods actually have - a typo, or a Deployment whose template labels were changed.
bash Example session kubectl create service clusterip orphan -n netlab --tcp=80:80service/orphan createdkubectl get svc orphan -n netlab -o jsonpath="{.spec.selector}{\"\n\"}"{"app":"orphan"}kubectl get endpointslice -n netlab -l kubernetes.io/service-name=orphan -o jsonpath="{range .items[*]}{.metadata.name}{\" endpoints=\"}{.endpoints}{\"\n\"}{end}"orphan-4mp28 endpoints=nullkubectl exec client -n netlab -- curl -s -o /dev/null -w '%{http_code}\n' --max-time 5 http://orphan 2>&1 | tail -2000command terminated with exit code 7Expected resultA Service with a selector nothing matches, an endpoint list of null, and curl exiting 7.
Success conditionYou checked the endpoint list before blaming DNS.
Troubleshooting
The Service name resolves but every connection times out or is refused.
Why: The endpoint list is empty. The selector does not match any running, ready Pod - most often a label typo, or Pods that are running but not passing their readiness probe.
Fix:Read the slice:
kubectl get endpointslice -l kubernetes.io/service-name=<svc>. If it is null, comparekubectl get svc <svc> -o jsonpath='{.spec.selector}'againstkubectl get pods --show-labels.Ping to a ClusterIP fails and it looks like the Service is down.
Why: Nothing holds a ClusterIP, so there is nothing to answer ICMP. Only the ports the Service declares are programmed.
Fix:Test the port instead -
curl,nc -z, orwgetagainst the declared port. Ping is not a Service health check.A Pod cannot resolve the Service name at all.
Why: A DNS problem rather than a Service problem - usually cluster DNS not running, or the wrong namespace in a short name.
Fix:Short names only resolve inside the same namespace. Across namespaces use
<service>.<namespace>, and check that the DNS Pods inkube-systemare Ready.