TT Lab
Get started
Learn Learning paths Courses

CKAD — Kubernetes Application Developer

A Service Sells a Name, Not an IP

Continue in TT Lab

In one line

A Pod IP is a volatile address that changes on every restart. A Service layers an unchanging name and virtual IP on top of it, and keeps updating the Pods behind it through a label selector. So the essence of a Service is not load balancing but indirection (an indirect address).

Why this was needed

Pods die and come back at any time. Each time they come up, the IP is different. If the frontend knows the backend Pod IPs directly, you have to fix the frontend every time the backend restarts. When you scale out, there are several IPs, and who manages that list is also a problem.

A Service solves this with "one layer of names." The client knows only the name backend, and Kubernetes manages which IPs that name actually resolves to. The name resolves through DNS — backend within the same namespace, backend.other-ns from another namespace, and the full form is backend.other-ns.svc.cluster.local.

The value of indirection shows up most painfully when it is absent. When I expanded my homelab cluster from 3 nodes to 7 and made the control plane 3 machines, I opened kubeadm-config and found this.

controlPlaneEndpoint: 10.0.0.120:6443     # ← cp-1 의 물리 IP

What was embedded was neither a VIP nor DNS but the real IP of the first node. So when cp-1 dies, etcd quorum is fine at 2/3 and the other apiserver processes are alive, but the kubelets of the 7 workers and every kubeconfig know only that address, so the state becomes nobody can find the door. On top of that, the apiserver certificate SAN did not contain the IPs of the other control plane nodes, so even connecting directly failed TLS verification. This is exactly what a Service does for Pods — whatever changes behind it, the name the client knows stays the same.

How it works

The thing people get wrong most often in a Service spec is the three ports.

Field Who looks at it Meaning of the value
port Client The port opened on the Service's ClusterIP
targetPort Pod The port the actual container listens on (a number or a port name)
nodePort Outside the cluster The port opened on every node (30000–32767)

If you write the container port's name in targetPort (targetPort: api), you do not have to change the Service even if the actual port number differs per container. That is the point of a named port.

Service types stack upward. ClusterIP (cluster-internal only) → NodePort (ClusterIP + a node port) → LoadBalancer (NodePort + an external LB). There are also two special forms with no selector. ExternalName creates only a DNS CNAME, and a headless Service (clusterIP: None) creates no virtual IP and has DNS return the Pod IPs as they are.

A headless Service pairs with a StatefulSet. If you specify a headless Service in the StatefulSet's serviceName, each Pod gets its own DNS name.

<파드이름>.<서비스이름>.<네임스페이스>.svc.cluster.local
cache-0.cache-hs.ckad-net.svc.cluster.local

For a workload that has roles such as "Pod 0 is the primary," as in DB replication, individual addresses are needed rather than load balancing, so you use this combination.

An Ingress is L7. From a single entry point it splits by host and path and sends to several Services. pathType matters: Prefix matches a prefix on path-segment boundaries (/api also catches /api/users), and Exact is a complete match. An Ingress object alone does nothing; real routing happens only when an Ingress controller exists.

A NetworkPolicy defaults to allow. If there is no policy at all, every Pod communicates with every other. But the moment even one policy applies to a Pod, that Pod goes into allowlist mode and only what is specified is permitted. If you write only Ingress in policyTypes, outgoing traffic is not controlled. And policies only add up — when several policies apply, the union is permitted.

What it looks like in the field

When I rebuilt my homelab cluster with kubeadm 1.34 + Cilium 1.20, I did not install kube-proxy at all (--skip-phases=addon/kube-proxy). I then verified by measurement that Services still work.

kube-proxy 파드 수: 0
노드의 iptables KUBE- 체인 개수: 0

In a cluster that uses kube-proxy, there would be dozens to hundreds of KUBE-SERVICES, KUBE-SVC-*, and KUBE-SEP-* chains, in proportion to the number of Services. There were 0. Instead, the frontends and backends were directly in the eBPF map.

SERVICE ADDRESS        BACKEND ADDRESS (REVNAT_ID) (SLOT)
10.96.0.10:53/TCP (1)  10.244.0.150:53/TCP (4) (1)
10.96.0.1:443/TCP (1)  10.0.0.120:6443/TCP (1) (1)

What you learn here is that the Service abstraction stays the same while the implementation can be swapped out completely. Not a single character of the manifest changed, and the verification of connecting from a Pod by Service name and getting HTTP 200 passed as before.

There was also a measurement on the NetworkPolicy side. With Cilium's L7 policy, traffic was split by HTTP method even though the IP and port were the same.

정책 적용 전:  GET / → 200,  POST / → 200
정책 적용 후:  GET / → 200,  POST / → 403

A standard NetworkPolicy can express only up to L3/L4 (Pod selector, port), and per-method or per-path rules are the domain of CNI extensions (CiliumNetworkPolicy) or a service mesh. The CKAD scope is the standard NetworkPolicy, but it is good to know where the standard ends and the extensions begin.

Finally, one selector incident. If a Service's selector differs from the Pod labels by even one character, the endpoints are created empty. There is no error and no event. If kubectl get endpoints <이름> shows <none> (the placeholder is the Service name), it is almost certainly a selector typo.

What you will do in the next lab

In the ckad-net namespace, you create ClusterIP and NodePort Services and specify port/targetPort/nodePort distinctly. You connect targetPort with a named port, check the per-Pod DNS name rules with a headless Service + StatefulSet, write host and path routing with an Ingress, and build an allowlist with a NetworkPolicy that permits only frontend-to-backend traffic. This environment has no real CNI data plane, so you cannot verify whether a policy actually blocks packets. The goal is to write the objects correctly, and the working principles are checked in the quiz.