Service: one stable address for many Pods
Deployments keep Pods alive, but every replacement Pod has a new IP. A Service gives the group one name and one IP that never change, and spreads requests across whichever Pods are ready right now.
The problem
Your frontend needs to call posts. Which of the three Pods? And what happens when that one is replaced? Send requests, crash a Pod, scale up, and keep sending:
The client only ever uses posts-service. It never learns a Pod IP, so Pods can come and go freely.
How a Service finds Pods
Exactly like a ReplicaSet: with a label selector. The Service keeps a live list of the IPs of every ready Pod with matching labels, stored in an object called an EndpointSlice.
app: post vs app: posts). The Service exists, has an IP, and sends traffic nowhere. Check with kubectl get endpointslices -l kubernetes.io/service-name=posts-service; empty means no match.The YAML and its three ports
apiVersion: v1
kind: Service
metadata:
name: posts-service
spec:
type: ClusterIP
selector:
app: posts
ports:
- port: 80
targetPort: 4000
protocol: TCP
name
Becomes the DNS name. Other Pods call http://posts-service.
type
Who can reach it. ClusterIP (default) is inside the cluster only. See Service types.
selector
Which Pods get the traffic: every ready Pod labelled app=posts. Must match the labels in your Deployment's Pod template.
port: 80
The port the Service listens on. Clients call posts-service:80, or just http://posts-service since 80 is the HTTP default.
targetPort: 4000
The port on the Pod to forward to: where your Node app listens, the same number as containerPort. If you leave it out, it defaults to the same value as port.
protocol
TCP by default. UDP and SCTP also work.
client ──► posts-service:80 ──► Pod IP:4000 (targetPort) ──► app.listen(4000)
Calling it by name
Kubernetes runs a DNS server (CoreDNS). Every Service gets a name that any Pod can use:
| From | Use |
|---|---|
| A Pod in the same namespace | http://posts-service |
| A Pod in another namespace | http://posts-service.default |
| Fully written out | http://posts-service.default.svc.cluster.local |
So your comments service calls posts like this, with no IP anywhere:
// comments service
const res = await fetch('http://posts-service/posts');
Service types
Each type builds on the previous one, adding a way in from further away.
The default. A virtual IP inside the cluster only. Use it for service-to-service calls: posts, users, databases. Most of your Services will be this.
spec:
type: ClusterIP
selector: { app: posts }
ports: [{ port: 80, targetPort: 4000 }]
A ClusterIP plus the same port (30000–32767) opened on every node. http://<any-node-ip>:30080 reaches the Pods. Fine for local clusters and demos; in production you rarely expose node ports directly.
spec:
type: NodePort
selector: { app: posts }
ports: [{ port: 80, targetPort: 4000, nodePort: 30080 }]
A NodePort plus a load balancer from your cloud (AWS, GCP, Azure) with a public IP. The usual way to expose one app to the internet. Each LoadBalancer Service costs one cloud load balancer, so for many apps you put an Ingress in front instead (later chapter).
spec:
type: LoadBalancer
selector: { app: posts }
ports: [{ port: 80, targetPort: 4000 }]
No Pods, no proxying. The Service name becomes a DNS alias for a name outside the cluster, such as a managed database. Your code calls posts-db; switching providers means changing one line of YAML.
apiVersion: v1
kind: Service
metadata:
name: posts-db
spec:
type: ExternalName
externalName: posts.abc123.us-east-1.rds.amazonaws.com
The kubectl commands
$ kubectl apply -f service.yaml service/posts-service created $ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) kubernetes ClusterIP 10.96.0.1 <none> 443/TCP posts-service ClusterIP 10.96.40.12 <none> 80/TCPThe Service has its own stable IP. (
kubernetesis the API server's own Service; ignore it.)$ kubectl get endpointslices -l kubernetes.io/service-name=posts-service NAME ADDRESSTYPE PORTS ENDPOINTS posts-service-x7k2d IPv4 4000 10.244.1.5,10.244.2.9,10.244.1.7The Pods it currently sends to. Crash one and run this again: the list updates.
$ kubectl run tmp --rm -it --image=busybox:1.36 -- sh / # wget -qO- http://posts-service/posts [{"id":1,"title":"Hello Kubernetes"}] / # exitTest from inside the cluster with a throwaway Pod. This is exactly what your other services do.
$ kubectl port-forward service/posts-service 8080:80 Forwarding from 127.0.0.1:8080 -> 4000 # in another terminal: $ curl http://localhost:8080/postsReach a ClusterIP Service from your laptop while developing, without changing its type.
$ kubectl expose deployment posts --name=posts-service --port=80 --target-port=4000 service/posts-service exposedThe imperative shortcut: creates the same Service, copying the selector from the Deployment.
spec: selector: app: post # typo: Pods are labelled app=postsA one-letter mistake.
$ kubectl get svc posts-service NAME TYPE CLUSTER-IP PORT(S) posts-service ClusterIP 10.96.40.12 80/TCPLooks perfectly healthy…
/ # wget -qO- http://posts-service/posts wget: can't connect to remote host (10.96.40.12): Connection refused…but nothing answers.
$ kubectl get endpointslices -l kubernetes.io/service-name=posts-service NAME ADDRESSTYPE PORTS ENDPOINTS posts-service-x7k2d IPv4 <unset> <unset> $ kubectl get pods --show-labels NAME LABELS posts-7d9f8c6b5d-4kq2p app=posts,pod-template-hash=7d9f8c6b5dNo endpoints. Compare the selector with the Pod labels, fix
app: posts, apply, and the endpoints appear.
Recap
- A Service is a stable name and IP in front of a changing set of Pods.
- It finds Pods by label selector and only sends to ready ones.
portis the Service's port,targetPortthe Pod's.- Call it by DNS name:
http://posts-service. - ClusterIP for inside, NodePort and LoadBalancer for outside, ExternalName for an outside alias.
- Empty endpoints almost always means the selector does not match.
Quick check
port: 80 and no targetPort. Does http://posts-service work?Show answer
No. targetPort defaults to port, so traffic goes to Pod port 80, where nothing listens. Set targetPort: 4000.
Services: common questions
What is the difference between port and targetPort?
port is the port the Service listens on. targetPort is the port on the Pods it forwards to, where your app listens. targetPort defaults to port.