1. Why Services Exist
Every rolling update in Week 8 replaced Pods with brand-new ones, each getting a new internal IP the moment it starts. If your frontend hardcoded an IP for the API Pod it talks to, that IP would be wrong within minutes of any deploy.
ReplicaSet manages 3 Pods for "api":
api-7d9f8-abc12 10.244.1.5
api-7d9f8-def34 10.244.2.8
api-7d9f8-ghi56 10.244.1.9
# A rollout replaces all three. New IPs, every time:
api-9c2e1-jkl78 10.244.2.14
api-9c2e1-mno90 10.244.1.21
api-9c2e1-pqr12 10.244.3.3
# Anything that hardcoded the old IPs is now broken.
A Service gives that constantly-shifting set of Pods one stable address — a fixed virtual IP and a DNS name — and load-balances traffic across whichever Pods currently match its selector, however many there are, wherever they're running.
Docker's user-defined network DNS solved "find a container by name, not IP" on one machine. A Kubernetes Service solves the same problem at cluster scale, across many Pods and nodes — same underlying need, a more powerful mechanism to match a more dynamic environment.
2. ClusterIP, NodePort & LoadBalancer
Three Service types cover almost every scenario, differing only in how far the Service is reachable from:
apiVersion: v1
kind: Service
metadata:
name: api
spec:
type: ClusterIP # default — reachable only from INSIDE the cluster
selector:
app: api
ports:
- port: 80 # the Service's own port
targetPort: 3000 # the port the container actually listens on
ClusterIP # internal only. Default. Right choice for a database, an internal
# API, anything another Pod calls but the outside world shouldn't.
NodePort # opens a static port (30000-32767) on EVERY node's own IP. Mostly
# a building block for LoadBalancer, or for bare-metal clusters
# without a cloud load balancer available.
LoadBalancer # provisions an actual cloud load balancer (AWS ELB, GCP LB, etc.)
# with a public IP. The standard way to expose a Service to the
# public internet on a managed cloud cluster.
In practice: internal-only services (databases, internal APIs) are ClusterIP, and public-facing entry points are usually fronted by an Ingress (Week 12) sitting behind a single LoadBalancer, rather than giving every public service its own cloud load balancer.
port is what OTHER things use to call this Service. targetPort is what the CONTAINER actually listens on. Mixing these up is one of the most common causes of a Service that "exists" but returns nothing — always double-check targetPort matches your app's actual listening port.
3. Selectors & Endpoints
A Service finds its Pods the exact same way a ReplicaSet does: label selectors, not
names. Kubernetes maintains an Endpoints (or EndpointSlice)
object per Service, continuously updated with the IPs of every currently-matching,
ready Pod.
kubectl get service api
kubectl get endpoints api
# NAME ENDPOINTS
# api 10.244.1.5:3000,10.244.2.8:3000,10.244.1.9:3000
kubectl describe service api
# Selector: app=api <- must match your Pods' labels EXACTLY
If kubectl get endpoints shows <none>, that's almost
always a label mismatch — the Service's selector doesn't match any
Pod's labels, character for character. It's the single most common
Service debugging scenario you'll hit.
A Pod that's Running but failing its readiness probe (Week 16) is automatically excluded from a Service's Endpoints — traffic never routes to a Pod that isn't ready to handle it, even though the Pod is technically alive. This is a core piece of how Kubernetes avoids sending traffic to broken instances.
4. Cluster DNS in Practice
Every Service automatically gets a DNS name, resolved by CoreDNS (the same
kube-system Pod you may have spotted in Week 7's exercise):
<service-name>.<namespace>.svc.cluster.local
# From a Pod in the SAME namespace, the short form just works:
curl http://api/users
# From a Pod in a DIFFERENT namespace, include the namespace:
curl http://api.backend.svc.cluster.local/users
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
spec:
replicas: 2
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
image: myrepo/frontend:1.0.0
env:
- name: API_URL
value: "http://api" # resolves via cluster DNS, no IP anywhere
This is the payoff for the whole week: your application code never needs to know or care about Pod IPs, node placement, or how many replicas exist — it calls a stable name, and the Service + Endpoints + cluster DNS machinery handles the rest.
The ConfigMaps and Secrets you'll meet in Week 10 are the real-world way to set values like API_URL without hardcoding them directly into every Deployment manifest — same idea shown here, made reusable across environments.
5. Hands-on Exercise
Expose your Week 8 Deployment with a Service, then wire up a second one that calls it by name
Turn last week's standalone Deployment into a properly service-discoverable component, then prove name-based calling works from a completely separate Deployment.
Part 1 — Expose it:
- Write a ClusterIP Service for your Week 8 Deployment, matching its Pod labels via selector.
kubectl apply -fit, then confirmkubectl get endpointsshows all your Pods' IPs — if it shows none, fix the label mismatch before continuing.- From inside a throwaway debug Pod (
kubectl run debug --image=busybox -it --rm -- sh), curl your Service by name and confirm you get a response.
busybox doesn't have curl, but it does have wget -O- http://api — or use an image like nicolaka/netshoot which ships a fuller networking toolkit if you'd rather have curl, dig and friends available.
Part 2 — Chain a second service to it by name:
- Deploy a second, simple app (any small container that can make an outbound HTTP call, or reuse another one of your own images) as its own Deployment.
- Set an environment variable on it pointing at your Week 8 Service by its cluster-DNS name, and confirm it can successfully call it.
- Delete and recreate all the Pods behind your Week 8 Service (
kubectl delete pods -l app=<your-label>) to force new IPs, and immediately re-test the call from your second app — confirm it still works without any config change, proving the Service absorbed the IP churn. - Run
kubectl get endpointsagain right after the Pod deletion and note how quickly the Endpoints list updates to the new Pod IPs.
If the second app's call fails right after the Pod deletion, it's likely a timing issue — give new Pods a few seconds to reach Running and pass their default readiness state before retrying, since a Service only routes to Pods it considers ready.
6. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why shouldn't application code ever hardcode a Pod's IP address?
Why shouldn't application code ever hardcode a Pod's IP address?
Pod IPs are not stable — a rollout, a crash and restart, or rescheduling to another node all assign a brand-new IP. Any hardcoded reference breaks the moment the Pod is replaced, which is why Services (with a stable virtual IP and DNS name) exist as the correct abstraction to call instead.
Q2
Which Service type is the right default for an internal database that only other Pods should reach?
Which Service type is the right default for an internal database that only other Pods should reach?
ClusterIP — the default type, reachable only from inside the cluster. NodePort and LoadBalancer both expose the Service beyond the cluster, which is unnecessary and increases exposure for something that should only ever be called by other internal Pods.
Q3
You created a Service but kubectl get endpoints shows none. What's the most likely cause?
You created a Service but kubectl get endpoints shows none. What's the most likely cause?
The Service's label selector doesn't match any Pod's actual labels, character for character — this is the single most common Service misconfiguration. It's also possible matching Pods exist but none are passing their readiness probe, since only Ready Pods appear as Endpoints.
Q4
What's the difference between a Service's port and targetPort fields?
What's the difference between a Service's port and targetPort fields?
port is the port other things use when calling the Service. targetPort is the port the actual container is listening on inside the Pod. They can differ intentionally (e.g. Service port 80 forwarding to container port 3000) — mixing them up is a common cause of a Service that exists but returns nothing.