π§ͺ
π― Goal
By the end, you will clearly see:
- Why ClusterIP hides pod identity
- Why Headless Service exposes pod identity
- How StatefulSet + Headless Service gives stable DNS per pod
- Why databases need this
π§ Mental model (keep this in mind)
| Setup | DNS result |
|---|---|
| Deployment + ClusterIP | One virtual IP |
| Deployment + Headless | Multiple pod IPs |
| StatefulSet + Headless | Stable pod DNS names (this is the magic) |
π§© Project structure
headless-demo/
βββ mysql-headless.yaml
βββ mysql-statefulset.yaml
βββ dns-test.yaml
1οΈβ£ Headless Service (NO Cluster IP)
π mysql-headless.yaml
apiVersion: v1
kind: Service
metadata:
name: mysql-headless
spec:
clusterIP: None # π THIS MAKES IT HEADLESS
selector:
app: mysql
ports:
- port: 3306
π Important:
- No virtual IP
- DNS will return pod IPs
2οΈβ£ StatefulSet (stable pod identity)
π mysql-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql-headless # π REQUIRED
replicas: 3
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
env:
- name: MYSQL_ROOT_PASSWORD
value: root
ports:
- containerPort: 3306
π What StatefulSet guarantees:
- Pods named:
mysql-0
mysql-1
mysql-2
- Names NEVER change
- Identity is stable
3οΈβ£ DNS test pod (to observe behavior)
π dns-test.yaml
apiVersion: v1
kind: Pod
metadata:
name: dns-test
spec:
containers:
- name: dns
image: busybox:1.28
command: ["sleep", "3600"]
4οΈβ£ Apply everything (ORDER MATTERS)
kubectl apply -f mysql-headless.yaml
kubectl apply -f mysql-statefulset.yaml
kubectl apply -f dns-test.yaml
Wait:
kubectl get pods
You should see:
mysql-0 Running
mysql-1 Running
mysql-2 Running
dns-test Running
5οΈβ£ π₯ THE MOST IMPORTANT PART β DNS BEHAVIOR
Enter the test pod
kubectl exec -it dns-test -- sh
π DNS lookup of the HEADLESS service
nslookup mysql-headless
β
You will see MULTIPLE IPs (one per pod):
Address: 10.244.0.12
Address: 10.244.0.13
Address: 10.244.0.14
π This is DNS round-robin.
π₯ DNS lookup of INDIVIDUAL PODS (THIS IS THE KEY)
nslookup mysql-0.mysql-headless
nslookup mysql-1.mysql-headless
nslookup mysql-2.mysql-headless
β Each one resolves to a specific pod IP.
This is what ClusterIP can NEVER do.
6οΈβ£ Why databases NEED this
Imagine:
| Role | DNS |
|---|---|
| Primary DB | mysql-0.mysql-headless |
| Replica 1 | mysql-1.mysql-headless |
| Replica 2 | mysql-2.mysql-headless |
Now:
- Writes β
mysql-0.mysql-headless - Reads β replicas
- Replication β stable target
π‘ This is impossible with Deployment + ClusterIP.
7οΈβ£ Visual intuition (whatβs happening)
8οΈβ£ One-line interview answer (remember this)
βA headless service removes the virtual IP and exposes pod identities via DNS. Combined with StatefulSets, it enables stable per-pod DNS, which is required for databases and leader-follower architectures.β
π₯ SAME APP, TWO SERVICES
π¦ CASE 1 β ClusterIP Service (DEFAULT BEHAVIOR)
YAML (normal service)
apiVersion: v1
kind: Service
metadata:
name: mysql-clusterip
spec:
selector:
app: mysql
ports:
- port: 3306
What Kubernetes does
- Creates ONE virtual IP
- Hides all pod IPs
- kube-proxy load balances traffic
DNS behavior
Inside the cluster:
nslookup mysql-clusterip
Result:
Name: mysql-clusterip
Address: 10.96.120.15 <-- ONE IP
That IP is NOT a pod.
Itβs a virtual service IP.
π What happens to traffic
todo-app
|
v
mysql-clusterip (10.96.120.15)
|
+--> mysql-0
+--> mysql-1
+--> mysql-2
Kubernetes decides where each request goes.
β Why this breaks databases
Letβs say:
- Signup request β mysql-2 (write happens there)
- Login request β mysql-0 (no data)
- β login fails
Because:
- write and read hit different pods
- pod identity is hidden
- you cannot say βalways go to mysql-0β
ClusterIP is stateless-friendly, stateful-hostile.
π¨ CASE 2 β Headless Service (NO CLUSTER IP)
Now we change only one line.
YAML (headless service)
apiVersion: v1
kind: Service
metadata:
name: mysql-headless
spec:
clusterIP: None # π THIS IS EVERYTHING
selector:
app: mysql
ports:
- port: 3306
π DNS behavior (this is the key difference)
Inside a pod:
nslookup mysql-headless
Result:
Address: 10.244.0.10 (mysql-0)
Address: 10.244.0.11 (mysql-1)
Address: 10.244.0.12 (mysql-2)
β οΈ No virtual IP
β οΈ DNS returns pod IPs directly
This is DNS round-robin, not kube-proxy load balancing.
π§ Still not enough alone (important)
If you stop here and use:
mysql-headless
Your app may still hit different pods, because DNS answers rotate.
So headless alone is not the full solution.
π© ENTER STATEFULSET (THIS IS THE MISSING PIECE)
StatefulSet guarantees stable pod names:
mysql-0
mysql-1
mysql-2
And Kubernetes automatically creates DNS records like:
mysql-0.mysql-headless
mysql-1.mysql-headless
mysql-2.mysql-headless
π₯ THIS IS THE MOMENT IT CLICKS
Now you can do this:
Write traffic
mysql-0.mysql-headless
Read traffic
mysql-1.mysql-headless
mysql-2.mysql-headless
No guessing. No randomness.
π SIDE-BY-SIDE SUMMARY (MEMORIZE THIS)
| Feature | ClusterIP | Headless |
|---|---|---|
| Has virtual IP | β | β |
| Hides pod identity | β | β |
| DNS returns | 1 IP | Multiple pod IPs |
| Pod-specific DNS | β | β |
| Good for web apps | β | β |
| Good for databases | β | β (with StatefulSet) |
π§ͺ Why we used dns-test pod
You cannot βsee DNSβ from your laptop.
So we create a pod just to ask:
nslookup <service-name>
Thatβs how SREs debug real prod issues.
π― ONE-LINE INTERVIEW ANSWER (IMPORTANT)
βClusterIP services hide pod identity and load balance traffic, which is unsuitable for databases. Headless services expose pod IPs via DNS, and when combined with StatefulSets, provide stable per-pod DNS required for stateful workloads.β



Top comments (0)