Chapter 5
Services & Networking — How Pods Find Each Other
Before you read, guessHow do Services automatically locate their backend pods without manual updates?
Take ten seconds and guess — even a wrong guess makes the answer stick. Tap to see where the chapter lands, or just read on.
Services find their backend pods with label selectors — the same mechanism ReplicaSets use. No manual updating needed as pods come and go.
Imagine calling a company and getting a different phone number every time: Monday it's an intern's cell phone, Tuesday a desk phone three rooms over, Wednesday that person already quit. You'd never get through. Kubernetes has this exact problem. Every time a Deployment replaces a pod, the replacement shows up with a brand new IP address. So how does your frontend pod reliably reach "the backend" when its address keeps changing under it? That's what we solve today.
The problem: pod IPs are quicksand
A Deployment (from Post 4) keeps a fixed number of pods running — it does not keep the same pods running. When a pod crashes, or gets replaced during a rollout, Kubernetes doesn't bring the old pod back. It creates a brand new pod, with a brand new IP address. A pod's IP is never something you can count on staying the same.
Say you hardcode your frontend to call your backend at 10.244.1.7. That works — until the backend pod gets rescheduled and comes back as 10.244.2.15. Now your frontend keeps knocking on a door nobody lives behind anymore. Multiply that across every service talking to every other service, and hardcoded IPs fall apart fast. Kubernetes needs one thing that stays put while everything behind it keeps moving. That thing is a Service.
What a Service actually is
A Service is a stable address — a virtual IP and a DNS name — that sits in front of a group of pods. You create it once, and that address never changes, no matter how many times the pods behind it get destroyed and replaced. The Service also load-balances: it spreads incoming traffic across whichever pods happen to be alive right now.
How does a Service know which pods are "behind" it? The same way a ReplicaSet does (Post 4): label selectors. A label is just a key-value tag you attach to a pod, like app: nginx. You give the Service a selector — say, app: nginx — and any pod carrying that exact label becomes a valid destination. A pod is added the instant it starts running with a matching label, and dropped the instant it disappears. You never update the Service by hand. It watches labels continuously and keeps its list of backend pods current — the same reconciliation instinct from Post 1, now applied to networking.
Three kinds of Services, and when you'd use each
ClusterIP is the default type, and the one you'll use most. "Cluster" means reachable only from inside the cluster: it hands out a virtual IP that other pods can call, but nothing outside the cluster can see it. That's exactly right for internal traffic, like your frontend pod calling your backend pod. Nothing outside the cluster should need to reach your backend directly anyway.
NodePort opens one specific port, somewhere in the range 30000–32767, on every node in the cluster. Send traffic to <any-node-ip>:<nodeport>, and it gets forwarded to the Service. It's a quick way to reach a Service from outside the cluster during local development. In production it's clunky — you'd have to know individual node IPs, and nodes can be added or removed just like pods.
LoadBalancer is what you actually use in production, once you're running on a cloud provider like GCP. It does everything NodePort does, plus one more thing: it asks the cloud provider to spin up a real external load balancer with a public IP address. That public IP is what the outside world — your users' browsers — actually connects to. You'll reach for this type once this series gets to deploying on GCP.
kube-proxy and DNS: what actually makes it work
Post 2 mentioned kube-proxy in one line: "handles networking rules, more later." This is the "more later." kube-proxy is a small program that runs on every single node. It watches every Service and every pod behind it, and it continuously writes the low-level networking rules that make sure traffic sent to a Service's virtual IP actually lands on a real pod. You never write or touch these rules yourself.
Here's the part worth understanding: a Service's virtual IP isn't attached to any real network card anywhere. It's a promise, not a place. kube-proxy is what makes that promise real, on every node, rewriting its rules the instant pods come or go. It's a small reconciliation loop, quietly running in the background, same as the ones you met in Post 1.
Every Service also gets a DNS name automatically, in the form <service-name>.<namespace>.svc.cluster.local. In practice, if you're calling a Service from a pod in the same namespace, the plain Service name is enough. Your checkout pod can just request http://inventory. Under the hood, three things happen automatically: DNS resolves that name to the Service's stable virtual IP, and kube-proxy's rules forward the request to whichever inventory pod is alive right now. This name-based lookup is the foundation of how microservices talk to each other, and we'll come back to it later in the series.
Let's build one
Start by spinning up a Deployment, same as Post 4:
$ kubectl create deployment nginx --image=nginx --replicas=3
deployment.apps/nginx created
Now expose it. One command creates a ClusterIP Service that targets port 80 on any pod matching the Deployment's labels:
$ kubectl expose deployment nginx --port=80 --target-port=80
service/nginx exposed
Check what got created:
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 3d
nginx ClusterIP 10.96.142.88 <none> 80/TCP 12s
10.96.142.88 is the stable virtual IP — the front-desk number. It stays exactly the same no matter what happens to the pods behind it. Now check who's actually answering the phone right now:
$ kubectl get endpoints nginx
NAME ENDPOINTS AGE
nginx 10.244.1.4:80,10.244.1.5:80,10.244.1.6:80 34s
Those three IPs are your three nginx pods — the employees currently at their desks. This Endpoints object is the actual list kube-proxy reads to build its forwarding rules. It updates automatically, every single time a pod comes or goes.
To see DNS-based discovery for yourself, spin up a throwaway pod and shell into it:
$ kubectl run tmp-shell --rm -it --image=busybox -- sh
Inside that shell, run wget -qO- nginx (or curl nginx). You'll get nginx's welcome page back. Notice what you didn't do: you never typed an IP address anywhere. You said "nginx," and DNS, the Service, and kube-proxy handled everything else. Type exit when you're done — the --rm flag deletes the pod for you automatically.
kubectl delete pod <pod-name> and immediately re-check kubectl get endpoints nginx. Watch the IP list update itself as the Deployment replaces the pod — with zero changes made to the Service.The exam trap: selectors that almost match
Here's a bug that trips up almost everyone eventually — and it shows up constantly on the CKA exam, because it shows up constantly in real clusters. A Service's selector has to match your pods' labels exactly: same keys, same values, same capitalization. Write a Service with selector app: nginx, but label your pods App: nginx or app: nginx-web — one capital letter off, one extra word — and here's what happens: the Service still gets created successfully. kubectl get svc shows it looking perfectly healthy. And it silently routes to nothing. No error message, no crash. Just requests that time out, for reasons that aren't obvious anywhere in the Service definition itself.
kubectl get endpoints <service-name> first. An empty list means the selector doesn't match any pod's labels — that's the whole problem, right there. Compare it against your pods' actual labels with kubectl get pods --show-labels, fix the mismatch, and the list populates itself immediately. This is the fastest diagnostic step in Kubernetes networking, and it's exactly the instinct the CKA exam expects you to have.Key Takeaways
- Pod IPs are never stable. A Service gives you a fixed virtual IP and DNS name that stays constant, no matter how many times the pods behind it get replaced.
- Services find their backend pods with label selectors — the same mechanism ReplicaSets use. No manual updating needed as pods come and go.
- ClusterIP (the default) is internal-only. NodePort opens a port on every node, mainly useful for local dev. LoadBalancer provisions a real cloud load balancer with a public IP — your production path once you're on GCP.
- kube-proxy runs on every node, continuously writing the networking rules that route traffic from a Service's virtual IP to a real, current pod IP.
- Every Service gets a DNS name automatically, so pods call each other by name (like
http://inventory) instead of tracking IPs — the backbone of how microservices talk to each other. - If a Service seems broken, check
kubectl get endpoints <name>first. An empty list almost always means your selector doesn't exactly match your pods' labels.