Services
A pod IP is an unstable identifier. Pods are rescheduled, replaced on deploy, and scaled in and out. Anything that hard-codes a pod IP breaks on the first restart.
A Service is a stable name and virtual IP in front of a changing set of pods.
How the selector works
A Service does not reference pods directly. It declares a label selector, and a
controller keeps a matching EndpointSlice up to date:
apiVersion: v1
kind: Service
metadata:
name: payments
spec:
selector:
app: payments
ports:
- port: 80
targetPort: 8080
Any pod carrying app: payments and passing its readiness probe is added to the
endpoint list. Any pod that fails readiness is removed. This is the reconciliation
model again: the Service is a declaration, the endpoint list is reality, and a
controller closes the gap.
The three types
| Type | Allocates | Reachable from |
|---|---|---|
ClusterIP | A virtual IP inside the cluster | Inside the cluster only |
NodePort | A ClusterIP plus the same port on every node | Anything that can reach a node |
LoadBalancer | A NodePort plus an external load balancer | The internet, via the cloud provider |
The types are cumulative, not alternatives. A LoadBalancer Service still has a
ClusterIP and still has a node port — the cloud controller simply provisions an
external balancer that points at those node ports.
What ClusterIP really is
The ClusterIP is not assigned to any interface. Nothing answers ARP for it. It
exists only as a set of rules in each node's kernel — iptables or IPVS entries
installed by kube-proxy — that rewrite the destination address to a real pod IP
as the packet leaves.
That has a practical consequence worth internalising:
Load balancing granularity
kube-proxy picks an endpoint per connection, not per request. For HTTP/1.1
with connection reuse, and especially for HTTP/2 and gRPC where a single long-lived
connection carries every request, this means traffic pins to one pod.
This is why gRPC services behind a plain ClusterIP often show badly skewed load. The fix is client-side load balancing, a proxy that understands HTTP/2, or a service mesh — not a different Service type.
Headless Services
Setting clusterIP: None disables the virtual IP entirely. DNS then returns the
pod IPs directly, one A record per ready endpoint:
spec:
clusterIP: None
selector:
app: cassandra
This is what StatefulSets use. When each replica is individually addressable and identity matters — database members, brokers, anything with a quorum — you want the caller to see the real topology rather than a single virtual IP hiding it.