DEV Community

Aisalkyn Aidarova
Aisalkyn Aidarova

Posted on

MINI PROJECT: Headless Service with StatefulSet (MySQL-style behavior)

πŸ§ͺ

🎯 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

πŸ“Œ 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
Enter fullscreen mode Exit fullscreen mode

πŸ“Œ What StatefulSet guarantees:

  • Pods named:
  mysql-0
  mysql-1
  mysql-2
Enter fullscreen mode Exit fullscreen mode
  • 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"]
Enter fullscreen mode Exit fullscreen mode

4️⃣ Apply everything (ORDER MATTERS)

kubectl apply -f mysql-headless.yaml
kubectl apply -f mysql-statefulset.yaml
kubectl apply -f dns-test.yaml
Enter fullscreen mode Exit fullscreen mode

Wait:

kubectl get pods
Enter fullscreen mode Exit fullscreen mode

You should see:

mysql-0   Running
mysql-1   Running
mysql-2   Running
dns-test  Running
Enter fullscreen mode Exit fullscreen mode

5️⃣ πŸ”₯ THE MOST IMPORTANT PART β€” DNS BEHAVIOR

Enter the test pod

kubectl exec -it dns-test -- sh
Enter fullscreen mode Exit fullscreen mode

πŸ” DNS lookup of the HEADLESS service

nslookup mysql-headless
Enter fullscreen mode Exit fullscreen mode

βœ… You will see MULTIPLE IPs (one per pod):

Address: 10.244.0.12
Address: 10.244.0.13
Address: 10.244.0.14
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ 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
Enter fullscreen mode Exit fullscreen mode

βœ… 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)

Image

Image


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
Enter fullscreen mode Exit fullscreen mode

What Kubernetes does

  • Creates ONE virtual IP
  • Hides all pod IPs
  • kube-proxy load balances traffic

DNS behavior

Inside the cluster:

nslookup mysql-clusterip
Enter fullscreen mode Exit fullscreen mode

Result:

Name: mysql-clusterip
Address: 10.96.120.15   <-- ONE IP
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Kubernetes decides where each request goes.


❌ Why this breaks databases

Let’s say:

  1. Signup request β†’ mysql-2 (write happens there)
  2. Login request β†’ mysql-0 (no data)
  3. ❌ 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
Enter fullscreen mode Exit fullscreen mode

πŸ” DNS behavior (this is the key difference)

Inside a pod:

nslookup mysql-headless
Enter fullscreen mode Exit fullscreen mode

Result:

Address: 10.244.0.10   (mysql-0)
Address: 10.244.0.11   (mysql-1)
Address: 10.244.0.12   (mysql-2)
Enter fullscreen mode Exit fullscreen mode

⚠️ 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And Kubernetes automatically creates DNS records like:

mysql-0.mysql-headless
mysql-1.mysql-headless
mysql-2.mysql-headless
Enter fullscreen mode Exit fullscreen mode

πŸ”₯ THIS IS THE MOMENT IT CLICKS

Now you can do this:

Write traffic

mysql-0.mysql-headless
Enter fullscreen mode Exit fullscreen mode

Read traffic

mysql-1.mysql-headless
mysql-2.mysql-headless
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.”


Image

Image

Top comments (0)