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.
Try it: put a Service in front of your pods
Start a cluster, and run the three-copy hello Deployment from Lesson 5:
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:
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:
kubectl apply -f service.yaml
kubectl get service hello
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:
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:
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:
| Part | What it does |
|---|---|
kubectl run tmp | Start a pod named tmp |
--image=busybox:1.36 | Use BusyBox, a tiny Linux image with basic tools like wget and nslookup |
--restart=Never | Make it a one-off pod: when the command ends, don't restart it |
-it | Show the output live in your terminal, as if you ran it on your laptop |
--rm | Delete 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://hello | Fetch 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:
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:
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:
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:
| Type | Who can reach it | Typical use |
|---|---|---|
| ClusterIP | Only things inside the cluster | Apps talking to each other: an API calling its database or cache |
| NodePort | Anyone who can reach a node, on a high port (30000 to 32767) opened on every node | Quick tests, or setups with their own load balancer in front |
| LoadBalancer | The internet. The cloud provider creates a real load balancer with a public address | The 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:
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:
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:
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:
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
kind delete cluster --name learn
Check yourself
Why shouldn't one app call another by pod IP address?
How does a Service decide which pods to send traffic to?
A pod in the shop namespace needs a Service called db in the data namespace. Which name works?
Which Service type is only reachable from inside the cluster?
Your pods are running, but requests to the Service fail. kubectl describe service shows Endpoints: none. What's the likely cause?
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
- Service (opens in a new tab) in the official docs covers every type and option.
- DNS for Services and Pods (opens in a new tab) explains the naming rules in full.