Kubernetes basics · Lesson 8 of 12 · about 30 minutes

Services: how traffic finds your pods

Pods come and go, and their addresses change every time. A Service gives them one stable name and address, and CoreDNS makes that name work anywhere in the cluster.

By the end you'll know

  • Why you can't just send traffic to a pod's IP address
  • How a Service finds its pods, and spreads traffic across them
  • How pods find a Service by name, thanks to CoreDNS
  • The different Service types, and how traffic gets in from the internet
  • How to spot the classic "it's running but I can't reach it" problem

The problem: pods don't stay put

Every pod gets its own IP address. You saw it in Lesson 3 with kubectl get pod -o wide. But in Lesson 5 you watched a Deployment delete pods and create new ones during every update, and each new pod gets a new address. Scale up, and there are new addresses. A node fails, and its pods come back somewhere else, with new addresses again.

So if one app talked to another by pod IP address, it would break the first time anything changed. And with three copies of an app, which address would you even pick?

The fix: a Service

A Service gives a group of pods one stable name and one stable address. Traffic sent to the Service is passed on to one of the pods behind it. The pods can come and go; the Service stays put.

It finds its pods the same way a Deployment does, by label. A Service with selector: app: hello sends traffic to every running pod labeled app: hello, and keeps that list up to date by itself as pods appear and disappear.

How a Service finds its podsA client pod asks CoreDNS for the name hello, and gets back the Service's stable address. It sends its request to the Service hello, which selects pods labeled app equals hello. The Service passes each request to one of three matching pods. A fourth pod, labeled app equals other, doesn't match the selector and gets no traffic from this Service.Client podcallshttp://helloCoreDNShello is 10.96.12.401. look up the name2. send requestService: hellostable address 10.96.12.40selector: app=hellopicks one matching pod3. one ofPod app=hello10.244.1.5Pod app=hello10.244.2.7Pod app=hello10.244.1.9Pod app=otherdoesn't match: no trafficPod addresses change all the time. The Service's name and address never do.
The client never needs to know a pod's address. It uses the Service's name, CoreDNS turns that into the Service's address, and the Service passes the request to any pod whose labels match.

Try it: put a Service in front of your pods

Start a cluster, and run the three-copy hello Deployment from Lesson 5:

BASH
kind create cluster --name learn
kubectl create deployment hello --image=nginx:1.28 --replicas=3

kubectl create deployment labels the pods app: hello for you. Now save this as service.yaml:

YAML
apiVersion: v1
kind: Service
metadata:
  name: hello            # the name other apps will use
spec:
  selector:
    app: hello           # send traffic to pods with this label
  ports:
    - port: 80           # the port the Service listens on
      targetPort: 80     # the port the app listens on, inside the pods

Apply it and look:

BASH
kubectl apply -f service.yaml
kubectl get service hello
TEXT
NAME    TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
hello   ClusterIP   10.96.12.40    <none>        80/TCP    10s

The CLUSTER-IP is the Service's stable address. Yours will be different, and it won't change for as long as the Service exists. Now see which pods it found:

BASH
kubectl describe service hello

Look for the Endpoints line. It lists three addresses, one per pod. Those are the pods the Service will send traffic to.

Reach it by name

Your laptop isn't inside the cluster, so test from a pod that is. This starts a throwaway pod, fetches the page, and deletes the pod when it's done:

BASH
kubectl run tmp --rm -it --image=busybox:1.36 --restart=Never -- wget -qO- http://hello

It's a long command, but each part has one job:

PartWhat it does
kubectl run tmpStart a pod named tmp
--image=busybox:1.36Use BusyBox, a tiny Linux image with basic tools like wget and nslookup
--restart=NeverMake it a one-off pod: when the command ends, don't restart it
-itShow the output live in your terminal, as if you ran it on your laptop
--rmDelete the pod when it's done, so nothing is left behind
--Everything after this runs inside the container, not as an option for kubectl
wget -qO- http://helloFetch the page and print it to the screen, without progress messages

Keep this one handy. It's the quickest way to test anything from inside a cluster, and you'll use it again below.

You'll see the HTML of the nginx welcome page. You didn't use an IP address at all, just the name hello. That's CoreDNS at work, the cluster's phone book from Lesson 2. Every Service automatically gets a DNS name, and every pod is set up to ask CoreDNS first.

The full name has four parts:

TEXT
hello.default.svc.cluster.local
  │      │     │       └── the cluster's domain
  │      │     └── it's a Service
  │      └── the namespace it lives in
  └── the Service's name

From a pod in the same namespace, the short name hello is enough. From a pod in a different namespace, use at least hello.default, or the full name. Namespaces are Lesson 10. Look the name up yourself:

BASH
kubectl run tmp --rm -it --image=busybox:1.36 --restart=Never -- nslookup hello.default.svc.cluster.local

The answer is the Service's CLUSTER-IP from before.

Watch it keep up

Delete one of the pods, then look at the Service again:

BASH
kubectl delete $(kubectl get pods -l app=hello -o name | head -1)
kubectl describe service hello

The Deployment replaces the pod, with a new IP address, and the Service's Endpoints list updates by itself to include it. Anything calling http://hello never noticed. That's the whole point: Lesson 5 keeps the right number of pods running, and the Service keeps traffic finding them.

When you send many requests, the Service spreads them across all the matching pods. It's simple spreading, not smart balancing, but it means no single copy gets all the traffic.

Service types: who can reach it

kubectl get service showed TYPE: ClusterIP. That's the default, and there are others:

TypeWho can reach itTypical use
ClusterIPOnly things inside the clusterApps talking to each other: an API calling its database or cache
NodePortAnyone who can reach a node, on a high port (30000 to 32767) opened on every nodeQuick tests, or setups with their own load balancer in front
LoadBalancerThe internet. The cloud provider creates a real load balancer with a public addressThe front door of a cluster on a cloud

On kind, a LoadBalancer Service stays at EXTERNAL-IP: <pending>, because there's no cloud to create one. To reach a Service from your laptop while testing, use port forwarding instead:

BASH
kubectl port-forward service/hello 8080:80

Then open http://localhost:8080. Press Ctrl+C to stop.

In practice, clusters rarely give every app its own load balancer, because each one costs money. Instead there's usually one front door, an Ingress or a Gateway, that receives all the outside traffic and routes it by domain name or path to the right ClusterIP Service inside. Lesson 12 shows how to set one up.

In my real cluster

Here's the path a request to api.civicgrid.org takes. It reaches Cloudflare first, which passes it through a Cloudflare Tunnel: a connection that a small pod inside my cluster opened outward to Cloudflare. From there it goes to Envoy Gateway, the front door. A routing rule says "requests for api.civicgrid.org go to the Service civicgrid-civicgrid-api", a plain ClusterIP Service, which passes the request to an API pod.

Notice what's missing: there's no LoadBalancer Service anywhere. Every Service in my cluster is ClusterIP. I used to have one in front of the gateway, until a quick test showed that anyone who found its IP address could reach my API directly and skip Cloudflare's protection. Moving to the tunnel removed the public entry point altogether, and the same test now can't connect.

Inside the cluster, the API reaches its Redis cache by Service name too, using the full form: civicgrid-redis.civicgrid.svc.cluster.local. No IP addresses appear anywhere in my configuration.

Break it on purpose

This is one of the most common problems in Kubernetes: everything is running, but nothing can reach it.

Change the selector in service.yaml to a typo, app: helo, and apply it. Then try the request again:

BASH
kubectl apply -f service.yaml
kubectl run tmp --rm -it --image=busybox:1.36 --restart=Never -- wget -qO- -T 5 http://hello

It fails. The pods are fine and the Service exists, but nothing connects them. Check:

BASH
kubectl describe service hello

Endpoints is now empty (<none>). The Service is looking for pods labeled app: helo, and there aren't any. Whenever a Service doesn't respond, check its endpoints first: empty endpoints almost always means the selector doesn't match the pods' labels. Compare them with:

BASH
kubectl get pods --show-labels

Fix the selector back to app: hello, apply, and the endpoints and the requests come back.

The same symptom, a Service that doesn't respond, can also come from a wrong targetPort: the Service finds the pods but knocks on a port nothing is listening on. Endpoints first, then ports.

Clean up

BASH
kind delete cluster --name learn

Check yourself

  1. Why shouldn't one app call another by pod IP address?

  2. How does a Service decide which pods to send traffic to?

  3. A pod in the shop namespace needs a Service called db in the data namespace. Which name works?

  4. Which Service type is only reachable from inside the cluster?

  5. Your pods are running, but requests to the Service fail. kubectl describe service shows Endpoints: none. What's the likely cause?

  6. On a cloud cluster, why do most teams use one Ingress or Gateway instead of a LoadBalancer Service for every app?

Key terms

  • Service: a stable name and address in front of a group of pods, chosen by label.
  • Selector: the labels a Service uses to find its pods.
  • Endpoints: the current list of pod addresses behind a Service.
  • ClusterIP, NodePort, LoadBalancer: Service types, from inside-only to internet-facing.
  • CoreDNS: the cluster's DNS server, which turns Service names into addresses.
  • Ingress and Gateway: a single front door that routes outside traffic to Services by domain name or path.

Go deeper