15. Services, DNS, and the networking model
15.1 The Kubernetes network model (the rules every CNI implements)
- Every pod gets its own IP.
- Every pod can reach every other pod without NAT, across nodes.
- Agents on a node can reach all pods on that node.
The CNI plugin implements this. Common ones: Cilium (eBPF, network
policy, observability; the default choice for new clusters), Calico,
cloud-native CNIs like the AWS VPC CNI (pods get real VPC IPs), and
kind's simple kindnet.
Pod IPs are ephemeral. Services give a stable virtual IP + DNS name in front of a changing set of pods selected by label.
15.2 Service types
apiVersion: v1
kind: Service
metadata:
name: catalog
spec:
type: ClusterIP # default
selector: { app.kubernetes.io/name: catalog }
ports:
- name: http
port: 80 # the Service's port
targetPort: http # the container port NAME (rename-safe)
| Type | Reachable from | Use |
|---|---|---|
ClusterIP |
inside the cluster | service-to-service (99% of Services) |
headless (clusterIP: None) |
DNS returns pod IPs directly | StatefulSets, client-side load balancing, gRPC |
NodePort |
<any-node-ip>:30000-32767 |
rarely directly; building block for LBs |
LoadBalancer |
external cloud LB | exposing a gateway/ingress controller (one LB, not one per app) |
ExternalName |
DNS CNAME | aliasing an external DB host |
15.3 DNS
CoreDNS gives every Service a name:
catalog # same namespace
catalog.orbit # other namespace
catalog.orbit.svc.cluster.local # fully qualified
redis-0.redis.orbit.svc.cluster.local # one StatefulSet pod via a headless Service
kubectl run -it --rm dnsutils --image=registry.k8s.io/e2e-test-images/agnhost:2.39 --restart=Never -- \
nslookup catalog.orbit
War story: pod
/etc/resolv.confhasndots:5, so the nameapi.stripe.com(2 dots) is first tried asapi.stripe.com.orbit.svc.cluster.local,...svc.cluster.local,...cluster.local, plus the cloud's search domains, before the real name. That's 4–6 wasted queries per lookup (×2 for A and AAAA). At scale this overloaded CoreDNS and added latency to every external call. Fixes: use FQDNs with a trailing dot (api.stripe.com.), setdnsConfig.options: [{name: ndots, value: "2"}]on the pod, and run NodeLocal DNSCache.
15.4 How a Service actually routes
The EndpointSlice controller keeps a list of ready pod IPs for each Service. kube-proxy (iptables/IPVS/nftables) or an eBPF dataplane (Cilium) on every node rewrites packets addressed to the Service VIP to one of those pod IPs. There is no proxy process in the data path. It is connection-level load balancing.
Consequence: long-lived connections don't rebalance. HTTP/2 and gRPC
clients open one connection and pin to one pod. Scale from 3 to 10 pods
and the new 7 get no traffic from existing clients. Fixes: client-side
balancing with a headless Service, a service mesh / L7 proxy, or a max
connection age on the server (MaxConnectionAge in gRPC).
kubectl get endpointslices -l kubernetes.io/service-name=catalog -o wide
# If this is empty, your Service selects nothing or no pod is Ready (see §33).