TT Lab
Get started
Learn Learning paths Courses

CKA — Kubernetes Administrator

A Service Is a Promise, Not a Proxy

Continue in TT Lab

Summary

A Service object does not carry traffic. It is only a declaration that builds a backend list (EndpointSlice) from a selector, and what actually moves packets is the node's data plane (kube-proxy's iptables/nftables or the CNI's eBPF). If you know this separation, you can split "the Service exists but I can't connect" into three layers.

Why this matters

Pods die and come back up with different IPs. So a name and a stable virtual IP were needed. A Service gives you both.

The problem is who maintains the backend list. When you write a selector in a Service, the endpoints controller finds the Pods that match those labels and are Ready, and fills in the EndpointSlice. If the selector is off by even one character, the EndpointSlice is created normally, but empty. No error appears. The Service, the Deployment, and the Pods are all Ready, yet you just cannot connect.

That is why the first question in diagnosing a service failure is always the same: "Does the endpoint have an IP?" If it does, it is a data plane problem; if not, it is a selector or Pod Ready problem.

How it works

Type What it does Where it is used
ClusterIP One virtual IP inside the cluster Default
NodePort Opens ports 30000–32767 on every node Temporary access from outside
LoadBalancer NodePort + provisioning of an external LB Cloud / MetalLB
ExternalName Returns only a CNAME, with no IP Pointing at a system outside the cluster
headless (clusterIP None) Returns Pod IPs directly, with no VIP StatefulSet, client-side LB

If there is more than one port, each port must have a name. This is so that an Ingress or other objects can refer to a port by name.

The place people get wrong most often in a NetworkPolicy is the structure of the from array.

It is a difference of a single dash (-) in YAML, but the meaning is the opposite. This is where it is decided, both in the exam and in practice.

What it looks like in the field

Case 1 — A cluster with no kube-proxy at all. When I rebuilt a homelab with kubeadm 1.34, I did not install kube-proxy from the start, using --skip-phases=addon/kube-proxy, and brought up Cilium 1.20.1 with kubeProxyReplacement=true. A claim alone was not enough, so I actually counted.

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

In a cluster that uses kube-proxy, the KUBE-SERVICES, KUBE-SVC-*, and KUBE-SEP-* chains exist in the dozens to hundreds, in proportion to the number of Services. There were 0. So how do Services work? If you dump the eBPF maps, this is what you get.

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)

The frontends and backends are placed directly in a hash map inside the kernel. With iptables the rule chains grow linearly as Services increase, but eBPF is a hash map lookup, so the cost is constant regardless of the number of Services. The Service objects stay the same and only the data plane was swapped out — this is the moment that separation is confirmed in practice.

A bootstrap problem comes with this. When you turn on kubeProxyReplacement, you must also give k8sServiceHost=10.0.0.120 and k8sServicePort=6443. Without kube-proxy there is nothing to translate the ClusterIP (10.96.0.1) of the kubernetes Service into the real apiserver address, and Cilium itself, which would play that role, has not come up yet. It is a chicken-and-egg problem, so you have to tell it the real address directly.

Case 2 — L7 cannot be done with iptables. On the same cluster I tried a policy that separates GET with 200 and POST with 403. Same IP, same port, but split by HTTP method. iptables works at L3/L4 and cannot see the method, so it is impossible in principle. The standard NetworkPolicy likewise goes only up to L3/L4. It is a good example for drawing the line on how far a NetworkPolicy within the CKA scope can go.

Case 3 — Endpoints is now a thing of the past. The Endpoints API was deprecated in v1.33. If your code or scripts read Endpoints directly, change them to query EndpointSlice with the kubernetes.io/service-name label. It is a staple of pre-upgrade checklists.

What to do in the next lab

In the first lab you create ClusterIP, NodePort, headless, multi-port, ExternalName, and Ingress, and you yourself create and fix a situation where a selector typo leaves the endpoints empty. In the second lab you start a NetworkPolicy from default deny and narrow it down through six forms. This lab environment has no CNI to enforce policies, so you cannot verify actual traffic blocking, and only the object spec is graded. Instead it concentrates on spec-level traps such as the difference between AND and OR.