Skip to content

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

TypeAllocatesReachable from
ClusterIPA virtual IP inside the clusterInside the cluster only
NodePortA ClusterIP plus the same port on every nodeAnything that can reach a node
LoadBalancerA NodePort plus an external load balancerThe 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.