Service: A Stable Address and Load Balancing
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 --urlto 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.