Skip to the document
Madhuopen lab
The Kubernetes Ninja PathTrack 1 — Kubernetes from the ground up

Chapter 5

Services & Networking — How Pods Find Each Other

6 min read read1,682 wordsIntermediate6 recall cards

Before you read, guess

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

Analogy: A Service is a company's front-desk phone number, printed on the website and never changing. The Service is that number. The pods are the employees — they answer the phone, but they come and go: someone's out sick today, someone quit last week, someone new started this morning. A pod's IP address is which desk that employee happens to sit at today, different every day. The label selector is the rule the front desk uses to decide who's eligible to pick up: "anyone on the support team." You never dial an employee's desk directly, and you don't even know which desk they're at. You dial the one number, and whoever's eligible and free right now picks up. That's exactly what your frontend does when it calls a Service — it never touches a pod's IP directly.

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.

Try it yourself: Run the sequence above on your own cluster. Then delete one nginx pod with 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.

Exam trap: When a Service "isn't working," don't start by staring at the Service YAML. Run 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.

Next up: we'll look at how to configure and store the settings and secrets your pods need, without baking them into your container images.

Before you go

In one sentence, what was this chapter about?

From memory, without scrolling up. Writing it is what makes it yours; the grade is only to show you what you had.

How sure?