Section 4 · Kubernetes Core
Services: Stable Addresses for Moving Pods
20 min3 min readSpot the bug
Lesson at a glance
Give notes-api and its database stable names and addresses with Services, wire port, targetPort and containerPort correctly, and choose between ClusterIP, NodePort and LoadBalancer.
- 1A Service for notes-api
- 2Three ports, three meanings
- 3Finding Services by name
- 4Service types
- 5Reaching it from your laptop
No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.
Run kubectl get pods -o wide twice, with a rollout in between, and every IP is different. Pods are replaced constantly: new versions, rescheduling, scaling. Anything that remembered a Pod’s IP is now pointing at nothing. A Service is the stable thing you point at instead.
A Service for notes-api
apiVersion: v1
kind: Service
metadata:
name: notes-api
labels:
app.kubernetes.io/name: notes-api
spec:
selector:
app.kubernetes.io/name: notes-api
ports:
- name: http
port: 80
targetPort: httpThis creates a virtual IP and a DNS name, notes-api, that last as long as the Service exists. Connections to it are spread across the Pods its selector matches, the same label query the Deployment uses. Kubernetes tracks the current set of matching ready Pods in EndpointSlices and updates them as Pods come and go. Clients never notice.
A ClusterIP isn’t a machine and no process listens on it. Each node’s networking layer (kube-proxy, or an eBPF-based replacement in many clusters) programs rules that rewrite packets sent to the Service’s IP and port so they go to one of the ready Pods. That’s why you can’t ping a Service IP and shouldn’t worry when it doesn’t answer: only the declared ports exist. It’s also why a Service costs almost nothing, so create one for every workload that receives traffic.
Three ports, three meanings
This is where most Service bugs live.
containerPortin the Pod spec is the port your process listens on: 3000. It’s informational, likeEXPOSE, but naming it (name: http) lets other objects refer to it.targetPorton the Service is where traffic is delivered on the Pod. It must match where the app actually listens. Here it uses the namehttp, which resolves to 3000.porton the Service is what clients connect to: 80.
Finding Services by name
Cluster DNS gives every Service a name of the form <service>.<namespace>.svc.cluster.local. From a Pod in the same namespace, the short name notes-api is enough; from another namespace, use notes-api.notes or the full name.
notes-api needs a database, so give it one the same way. For practice in kind, run Postgres as a single-replica Deployment with a ClusterIP Service:
apiVersion: apps/v1
kind: Deployment
metadata:
name: notes-db
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: notes-db
template:
metadata:
labels:
app.kubernetes.io/name: notes-db
spec:
containers:
- name: postgres
image: postgres:17
env:
- name: POSTGRES_USER
value: notes
- name: POSTGRES_PASSWORD
value: notes # moved to a Secret in the next lesson
- name: POSTGRES_DB
value: notes
ports:
- name: postgres
containerPort: 5432
---
apiVersion: v1
kind: Service
metadata:
name: notes-db
spec:
selector:
app.kubernetes.io/name: notes-db
ports:
- name: postgres
port: 5432
targetPort: postgresnotes-api’s DATABASE_URL host is now notes-db, exactly as db was in Compose. The --- separates two objects in one file.
Service types
| Type | Reachable from | Use it for |
|---|---|---|
ClusterIP (default) |
Inside the cluster | Service-to-service traffic, like notes-api → notes-db |
NodePort |
Every node’s IP on a port in 30000–32767 | Quick tests, or as a building block for external load balancers |
LoadBalancer |
A cloud load balancer’s address | Exposing one service publicly on a managed cluster |
There’s also the headless Service, with clusterIP: None. It gets no virtual IP; instead its DNS name returns the IPs of the individual Pods. Databases and other clustered systems use it when each member needs to be addressed directly. You won’t need it for notes-api.
LoadBalancer builds on the other two: the cloud provider creates a load balancer that typically forwards to node ports, which forward to the Service (some controllers send traffic straight to Pod IPs instead). One per public service gets expensive and repetitive when you have many HTTP services; lesson 5.4 covers Gateway API, which puts many routes behind one entry point.
Reaching it from your laptop
kubectl apply -f k8s/
kubectl get svc
kubectl get endpointslices -l kubernetes.io/service-name=notes-api
kubectl port-forward svc/notes-api 8080:80
curl localhost:8080/readyzport-forward tunnels a local port to the Service through the API server. It’s for debugging, not for real traffic. If the EndpointSlice lists no addresses, the Service matches no ready Pods: compare the selector with kubectl get pods --show-labels, then check readiness.
Two problems are now visible in the YAML: a password sits in plain text, and configuration is mixed into the Deployment. The next lesson moves both out.
Key takeaways
- A Service gives a changing set of Pods one stable virtual IP and DNS name, and spreads connections across the ready Pods its selector matches.
portis what clients connect to on the Service;targetPortis where traffic goes on the Pod and must match the container’s listening port.- Inside the cluster, Pods reach a Service by name, such as
notes-dbin the same namespace ornotes-db.notes.svc.cluster.localfrom anywhere. - Use ClusterIP for internal traffic, LoadBalancer to expose one service through a cloud load balancer, and NodePort mainly for testing.
- An empty EndpointSlice means the Service matches no ready Pods; check the selector and readiness first.