Frontend Labs
⋯

15. Services, DNS, and the networking model

15.1 The Kubernetes network model (the rules every CNI implements)

  1. Every pod gets its own IP.
  2. Every pod can reach every other pod without NAT, across nodes.
  3. 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.conf has ndots:5, so the name api.stripe.com (2 dots) is first tried as api.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.), set dnsConfig.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).

Generated from real-life-k8s-guide.md · 8 Oct 2026 16:36