Services — stable endpoints for unstable pods
Pods come and go. A Service stays put. It gives you one permanent IP and DNS name that keeps answering even while the pods behind it are dying and being replaced.
The problem, one more time
Say you're running three replicas of a database pod, sitting at 10.244.2.4, 10.244.2.5, and 10.244.2.6. Any one of them can die and come back at a completely different IP. So if your web app has an IP — or a name that resolves straight to a single pod — baked into it, that connection snaps the instant the pod restarts.
What a Service does
A Service is a stable virtual endpoint — an IP plus a DNS name — that spreads traffic across a group of pods it picks out by label. As pods appear and disappear, Kubernetes keeps the Service's backend list up to date behind the scenes, and the front IP never budges.
The web pod just talks to db-svc. Kubernetes' internal DNS turns that name into the Service's stable IP, and then kube-proxy on the node hands the connection off to one of the healthy pods sitting behind it.
Service types
| Type | Reachable from | Typical use |
|---|---|---|
ClusterIP (default) | Only inside the cluster | Internal services — DB, cache, other microservices. |
NodePort | Any node's IP on a specific high port (30000-32767) | Ad-hoc external access, dev clusters, MetalLB base. |
LoadBalancer | A cloud-provisioned public IP | Production external services on cloud (AWS ELB, GCP LB). |
ExternalName | Resolves to an external DNS name | Aliasing an in-cluster name to an external one. |
Headless (clusterIP: None) | DNS returns the raw pod IPs | StatefulSets — direct pod addressing, custom load balancing. |
Internal DNS — how one pod finds another
Every Service you create automatically picks up an in-cluster DNS entry, courtesy of CoreDNS:
<service>.<namespace>.svc.cluster.local
# From a pod in the same namespace, this shortens to:
db-svc
# Or the fully qualified form:
db-svc.default.svc.cluster.local
Pod IPs are here-today-gone-tomorrow, and even a Service IP is only dependable inside the cluster. So reach for the DNS name every single time — it'll always point at whatever the right target is right now, and you never have to think about it again.
A minimal Service manifest
apiVersion: v1
kind: Service
metadata:
name: db-svc
spec:
selector:
app: db # picks all pods with this label
ports:
- name: postgres
port: 5432 # port on the Service
targetPort: 5432 # port on the pod
type: ClusterIP
Under the hood — how kube-proxy delivers
kube-proxy runs on every node, watching Services and rewriting the node's iptables (or IPVS, or eBPF) rules so that any packet aimed at a Service IP gets DNAT'd to one of the backing pod IPs. Notice what's missing: there's no separate load-balancer process sitting in the traffic path. It all happens down in the kernel.
Readiness — why healthy backends matter
A Service will only send traffic to pods that pass their readiness probe. A pod that's up but still booting sits on the bench until its readiness check goes green. That's what makes deploys safe: kube-proxy quietly adds new pods to the pool and pulls old ones out as their readiness flips back and forth.
Before you go digging into the database itself, check what the app is actually pointing at. If someone wired it up with a pod IP, it'll work right up until that pod restarts and then quietly break. The rule to hand back: talk to services by their DNS name — db-svc — never by a pod IP. And if the DNS name resolves but connections still get refused, it's usually the Service's readiness probe holding pods out of the pool because they haven't finished booting — not a networking fault at all.
Key takeaways
- A Service is a stable virtual IP + DNS name that fronts a moving set of pods.
- Types:
ClusterIP(internal),NodePort,LoadBalancer,ExternalName, headless. - Pods find each other by Service DNS name —
db-svc.default.svc.cluster.local. kube-proxyprograms the kernel to load-balance to healthy backends.- Readiness probes decide which pods a Service will route to.