Service: A Stable Address and Load Balancing

K
Kai··5 min read

Article 4 left a real problem: a Deployment keeps 3 pods alive, but each pod has its own internal IP, and every time a pod is rebuilt (self-healing, rolling update) the IP changes. If service A wants to call service B, it can't memorize the IP of each B pod — those IPs are quicksand. Service is the answer: a stable address standing in front of a group of pods, with free load balancing.

The problem: ephemeral pods, unpredictable IPs

   Without a Service:                 With a Service:
   client ──► pod 10.244.0.15 ?       client ──► Service "web" (fixed IP)
              pod 10.244.0.16 ?                       │ load-balances itself
              pod 10.244.0.17 ?                        ├─► pod 10.244.0.15
              (which IP? changes a lot)                ├─► pod 10.244.0.16
                                                       └─► pod 10.244.0.17

A Service gives you a virtual IP (ClusterIP) and a DNS name that never changes for its entire lifetime. The client only needs to know "web"; the Service handles finding which pods are alive and splitting traffic among them.

How does a Service find pods? Label selector

The key that makes everything fit together: a Service is not hard-wired to any pod. It uses a label selector (like the Deployment in Article 4) to dynamically select pods matching a label — any pod carrying the right label automatically sits behind the Service, even one created a second ago.

apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  type: ClusterIP
  selector:
    app: web          # select every pod with label app=web
  ports:
    - port: 80        # the Service's port
      targetPort: 80  # the port on the pod
kubectl apply -f service-clusterip.yaml
kubectl get svc web
NAME   TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
web    ClusterIP   10.106.68.120   <none>        80/TCP    0s

The Service is assigned the virtual IP 10.106.68.120 — stable until deleted. Which pods does it find? Look at the endpoints (the list of real pod IPs the Service points to):

kubectl describe svc web | grep -iE "Selector|Endpoints"
Selector:    app=web
Endpoints:   10.244.0.15:80,10.244.0.16:80,10.244.0.17:80

Exactly the 3 IPs of the 3 web pods. This is the dynamic part: a pod dies and a new pod is born, and Kubernetes updates this endpoint list automatically — the Service always points to the set of live pods. You don't have to do anything.

Internal DNS: call a service by name

Remember coredns from Article 1? Thanks to it, each Service gets a DNS name inside the cluster. Another pod just calls http://web to reach it. Try from a throwaway pod:

kubectl run tmp --image=busybox:1.36 --rm -it -- sh
# inside:
wget -qO- http://web | grep title
<title>Welcome to nginx!</title>

Called by name web, no need to know the IP. The full DNS name has the form <service>.<namespace>.svc.cluster.local:

nslookup web.default.svc.cluster.local
Name:    web.default.svc.cluster.local
Address: 10.106.68.120

Resolves to the right ClusterIP. Within the same namespace you only need the short name web; across namespaces, add web.<namespace>. This is the mechanism that lets microservices find each other in Kubernetes — no manual service discovery, DNS handles it all.

Three Service types: ClusterIP, NodePort, LoadBalancer

They differ in "who can call this Service".

   ClusterIP   ──  only callable INSIDE the cluster (default). For internal communication.
   NodePort    ──  opens a port (30000-32767) on EVERY node → callable from outside the cluster.
   LoadBalancer──  asks the cloud to provision a load balancer + public IP. For production on cloud.

ClusterIP (default) is the one we just created — only reachable from inside the cluster, good for services talking to each other (database, backend API...).

NodePort opens a fixed port on the node so the outside can get in:

spec:
  type: NodePort
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 80
      nodePort: 30080      # port on the node (range 30000-32767)
kubectl get svc web-nodeport
NAME           TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
web-nodeport   NodePort   10.107.188.99   <none>        80:30080/TCP   0s

80:30080 means port 30080 on the node maps to port 80 of the Service. Access from inside the minikube node:

minikube ssh "curl -s http://localhost:30080 | grep title"
<title>Welcome to nginx!</title>

With the Docker driver, NodePort isn't exposed straight to the host machine. minikube has the handy command minikube service web-nodeport --url to open a tunnel and give you a URL to call from your real machine.

LoadBalancer is the type used in production on cloud: Kubernetes asks the provider (AWS/GCP/Azure) to create a real load balancer with a public IP. On minikube there's no cloud, so EXTERNAL-IP will be stuck at <pending> — unless you run minikube tunnel to simulate one. In real production, NodePort is rarely used directly; people put an Ingress (Article 9) in front to route HTTP more cleanly.

kube-proxy: the one doing the load balancing

Back to Article 1: kube-proxy, running on every node, is the component that turns a Service into reality. When you create a Service, kube-proxy installs network rules (iptables/IPVS) on the node so that traffic sent to the ClusterIP is redirected to one of the pod endpoints, rotating to spread the load. This happens at the kernel level, very lightweight — a Service isn't a proxy running in the middle slowing down requests, just routing rules. That's why a Service has almost no performance cost.

Wrap-up

A Service gives you a stable address (ClusterIP + DNS name) in front of a group of pods selected dynamically via a label selector, and load-balances among them — solving exactly the "ephemeral pods, constantly changing IPs" problem. Thanks to coredns, pods call each other by name (web or web.<namespace>). Three types: ClusterIP (internal, default), NodePort (opens a port on the node to the outside), LoadBalancer (public IP via cloud). Behind the scenes is kube-proxy, installing network rules at the kernel level — light and fast.

The cluster now has some order, but everything sits in one place. Article 6: Namespaces, Labels and Selectors — how to partition and tag resources, the very mechanism Deployment and Service just used to find pods.