CertGrid CertGrid
Hands-on Lab·Kubernetes and Cloud Native Associate

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

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.

Two Pods on different workers. Which node a Pod lands on stops mattering the moment a Service is in front of it - that is the point of the exercise.
Server NameIP AddressOSRolesCPURAMHDD
CKA1001192.168.0.175Ubuntu 26.04 LTSControl Plane Node2 Core4 GB50 GB
CKA1001-NODE01192.168.0.176Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB
CKA1001-NODE02192.168.0.177Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB
CKA1001-NODE03192.168.0.178Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB

Before you start

  1. 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-node01

    Expected 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.

  2. Give it a name, and it stops answering ping

    kubectl expose writes a Service. It gets a ClusterIP from a different range than the Pods - here 10.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 loss

    Then ask it for a web page over the same address, by name:

    200

    A 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.local is the full DNS form - ..svc.cluster.local - and cluster DNS resolves it to the ClusterIP.

    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.local200

    Expected 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.

  3. 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.

    (Endpoints is the older object for this and is deprecated in favour of EndpointSlice since 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=true

    Expected 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.

  4. A Service that selects nothing

    Now the failure this guide exists for. kubectl create service clusterip orphan invents 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=null

    So the connection fails with an empty reply, not a DNS error:

    000
    command terminated with exit code 7

    A 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 7

    Expected 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

Official sources