π― Level 30 Debrief: Headless Services & StatefulSet DNS¶
Congratulations! You've completed World 3: Networking & Services by mastering headless services and StatefulSet DNS! This is one of the most important patterns for running stateful applications in Kubernetes, and understanding it will enable you to deploy databases, message queues, and distributed systems with confidence.
π What You Just Fixed¶
The Problem¶
Your StatefulSet pods couldn't communicate with each other using predictable DNS names, causing: - Pods unable to discover peers in the cluster - Database replication failing (can't find master/slave nodes) - Distributed systems broken (peers can't coordinate) - Random pod assignment instead of specific pod targeting
The Root Cause¶
# β BROKEN: Regular ClusterIP service
apiVersion: v1
kind: Service
metadata:
name: web-cluster
spec:
clusterIP: 10.96.100.50 # β Has virtual IP (not headless)
selector:
app: web-cluster
ports:
- port: 80
Why this fails for StatefulSets: 1. Regular ClusterIP service gets a virtual IP (10.96.100.50) 2. DNS resolves service name to this virtual IP 3. Traffic is load-balanced to random pods 4. Per-pod DNS names don't work (web-0.web-cluster returns NXDOMAIN) 5. StatefulSet pods can't find each other by name 6. Applications expecting stable pod identities fail
The Solution¶
# β
FIXED: Headless service
apiVersion: v1
kind: Service
metadata:
name: web-cluster
spec:
clusterIP: None # β
No virtual IP (headless)
selector:
app: web-cluster
ports:
- port: 80
How this works:
1. Service has clusterIP: None (headless)
2. No virtual IP is assigned
3. DNS returns pod IPs directly
4. Per-pod DNS names work! (web-0.web-cluster β web-0's IP)
5. Each pod has a stable network identity
6. Pods can find each other by predictable names
DNS Resolution:
# Headless service DNS:
web-0.web-cluster.k8squest.svc.cluster.local β 10.244.1.5 (web-0 pod)
web-1.web-cluster.k8squest.svc.cluster.local β 10.244.1.6 (web-1 pod)
web-2.web-cluster.k8squest.svc.cluster.local β 10.244.1.7 (web-2 pod)
# Service DNS (returns all pod IPs):
web-cluster.k8squest.svc.cluster.local β [10.244.1.5, 10.244.1.6, 10.244.1.7]
π Deep Dive: Headless Services¶
What is a Headless Service?¶
A headless service is a Kubernetes Service without a ClusterIP (virtual IP address). Instead of load balancing traffic to pods, it provides direct DNS entries for each pod.
Definition:
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
clusterIP: None # β This makes it "headless"
selector:
app: my-app
ports:
- port: 80
Key Characteristic: clusterIP: None
Regular Service vs Headless Service¶
Regular ClusterIP Service¶
apiVersion: v1
kind: Service
metadata:
name: web-service
spec:
clusterIP: 10.96.50.100 # Virtual IP assigned
selector:
app: web
ports:
- port: 80
targetPort: 8080
Behavior:
ββββββββββββββββββββββββββββββββββββββββββββββββββ
β Client queries: β
β web-service.default.svc.cluster.local β
β β β
β DNS returns: 10.96.50.100 (ClusterIP) β
β β β
β kube-proxy load balances to pods: β
β ββββββββββββ¬βββββββββββ¬βββββββββββ β
β β Pod A β Pod B β Pod C β β
β β10.1.1.5 β10.1.1.6 β10.1.1.7 β β
β ββββββββββββ΄βββββββββββ΄βββββββββββ β
β β
β Features: β
β β
Single virtual IP β
β β
Load balancing β
β β
Service discovery β
β β No per-pod DNS β
β β Cannot target specific pod β
ββββββββββββββββββββββββββββββββββββββββββββββββββ
Headless Service¶
apiVersion: v1
kind: Service
metadata:
name: web-service
spec:
clusterIP: None # β Headless!
selector:
app: web
ports:
- port: 80
targetPort: 8080
Behavior:
ββββββββββββββββββββββββββββββββββββββββββββββββββ
β Client queries: β
β web-service.default.svc.cluster.local β
β β β
β DNS returns ALL pod IPs (A records): β
β [10.1.1.5, 10.1.1.6, 10.1.1.7] β
β (no load balancing, client chooses) β
β β
β Per-pod DNS also works: β
β pod-0.web-service β 10.1.1.5 β
β pod-1.web-service β 10.1.1.6 β
β pod-2.web-service β 10.1.1.7 β
β β β
β ββββββββββββ¬βββββββββββ¬βββββββββββ β
β β Pod 0 β Pod 1 β Pod 2 β β
β β10.1.1.5 β10.1.1.6 β10.1.1.7 β β
β ββββββββββββ΄βββββββββββ΄βββββββββββ β
β β
β Features: β
β β No virtual IP β
β β No load balancing β
β β
Service discovery β
β β
Per-pod DNS names β
β β
Direct pod targeting β
ββββββββββββββββββββββββββββββββββββββββββββββββββ
Comparison Table¶
| Feature | Regular Service | Headless Service |
|---|---|---|
| ClusterIP value | IP address (e.g., 10.96.50.100) | None |
| Virtual IP | β Yes | β No |
| Load balancing | β Yes (kube-proxy) | β No |
| Service DNS | Returns ClusterIP | Returns all pod IPs |
| Per-pod DNS | β No | β Yes |
| kube-proxy rules | β Created | β Not created |
| Best for | Deployments (stateless) | StatefulSets (stateful) |
| Use case | Web servers, APIs | Databases, clusters |
π― StatefulSet DNS Convention¶
DNS Naming Format¶
For a StatefulSet with headless service:
Example:
apiVersion: v1
kind: Service
metadata:
name: mysql-service
namespace: production
spec:
clusterIP: None
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
namespace: production
spec:
serviceName: mysql-service
replicas: 3
DNS Names:
# Full DNS names:
mysql-0.mysql-service.production.svc.cluster.local
mysql-1.mysql-service.production.svc.cluster.local
mysql-2.mysql-service.production.svc.cluster.local
# Short names (within same namespace):
mysql-0.mysql-service
mysql-1.mysql-service
mysql-2.mysql-service
# Service DNS (returns all pod IPs):
mysql-service.production.svc.cluster.local
How DNS Resolution Works¶
- Pod gets name from StatefulSet:
- StatefulSet
mysqlwith 3 replicas -
Pods:
mysql-0,mysql-1,mysql-2 -
Service provides domain:
- Service name:
mysql-service -
Domain:
mysql-service.production.svc.cluster.local -
Kubernetes DNS creates per-pod A records:
-
Service DNS returns all pods:
Testing DNS Resolution¶
# Deploy a debug pod
kubectl run -it --rm debug --image=busybox:1.37 -- sh
# Test per-pod DNS
nslookup mysql-0.mysql-service.production.svc.cluster.local
# Returns: 10.244.1.5
# Test service DNS
nslookup mysql-service.production.svc.cluster.local
# Returns: 10.244.1.5, 10.244.1.6, 10.244.1.7
# Test from application
ping mysql-0.mysql-service
# Works! (short name within same namespace)
π Real-World Horror Story: The $3.2M MongoDB Migration Disaster¶
Company: DataFlow Inc. (Analytics platform) Date: August 2023 Duration: 8 hours of downtime + 3 days of data inconsistency Impact: $3.2M in lost revenue + major customer exodus
The Setup¶
DataFlow was migrating their MongoDB cluster to Kubernetes: - 3-node MongoDB replica set (1 primary, 2 secondaries) - 500GB of customer analytics data - $40M annual revenue depending on this database - Black Friday preparation (migration scheduled 2 months before)
The Architecture (Attempted)¶
# β BROKEN: Regular ClusterIP service (not headless!)
apiVersion: v1
kind: Service
metadata:
name: mongodb
spec:
# β MISSING: clusterIP: None
selector:
app: mongodb
ports:
- port: 27017
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mongodb
spec:
serviceName: mongodb
replicas: 3
template:
spec:
containers:
- name: mongodb
image: mongo:5.0
command:
- mongod
- --replSet=rs0
- --bind_ip_all
The Fatal Mistake: No clusterIP: None in the service!
The Incident Timeline¶
Friday, 2:00 AM - Migration Begins - Team deploys MongoDB StatefulSet - Pods start: mongodb-0, mongodb-1, mongodb-2 - All pods healthy
2:15 AM - Initialize Replica Set
// Engineer connects to mongodb-0
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "mongodb-0.mongodb:27017" },
{ _id: 1, host: "mongodb-1.mongodb:27017" },
{ _id: 2, host: "mongodb-2.mongodb:27017" }
]
})
Output:
β Per-pod DNS doesn't work! Service is not headless!
2:20 AM - Workaround Attempt #1: Use Pod IPs
// Engineer tries with pod IPs directly
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "10.244.1.5:27017" }, // mongodb-0 IP
{ _id: 1, host: "10.244.1.6:27017" }, // mongodb-1 IP
{ _id: 2, host: "10.244.1.7:27017" } // mongodb-2 IP
]
})
// Success: { "ok": 1 }
β Replica set initialized! Team celebrates.
2:30 AM - Data Migration Starts - Begin copying 500GB from old database - Estimated time: 4 hours
6:45 AM - Migration 95% Complete - 475GB transferred - Only 25GB remaining - Team takes coffee break
6:52 AM - Kubernetes Reschedules Pod - Node running mongodb-1 gets drained for maintenance - Pod mongodb-1 terminated - New mongodb-1 pod starts on different node - New IP: 10.244.2.8 (was 10.244.1.6)
6:53 AM - Replica Set Breaks
# MongoDB log on mongodb-0 (primary):
2023-08-11T06:53:12.345 E REPL [replication] cannot connect to
10.244.1.6:27017 (mongodb-1): Connection refused
2023-08-11T06:53:12.456 W REPL [replication] member 10.244.1.6:27017
is down
The problem: - Replica set configured with OLD IP: 10.244.1.6 - New pod has NEW IP: 10.244.2.8 - Primary can't reach secondary - Replica set loses quorum!
6:55 AM - Write Failures Begin
- Primary demoted itself (lost majority)
- No new primary elected (only 1 of 3 members reachable)
- All writes failing!
- Customers see error pages
7:00 AM - Panic Sets In - On-call engineer woken up - Production database down - Black Friday traffic starting in 3 months - Management escalates to VP level
7:15 AM - Realization Senior engineer joins war room:
# Check service
kubectl get service mongodb
# Output:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
mongodb ClusterIP 10.96.100.50 <none> 27017/TCP 5h
# CLUSTER-IP should be "None"!
Root cause discovered: Service is not headless!
7:20 AM - Emergency Fix Applied
# Patch service to be headless
kubectl patch service mongodb -p '{"spec":{"clusterIP":"None"}}'
# ERROR: Cannot change ClusterIP on existing service!
Can't patch! Must delete and recreate!
7:25 AM - The Risky Fix
# Delete service (risky!)
kubectl delete service mongodb
# Create headless service
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
name: mongodb
spec:
clusterIP: None # β
HEADLESS!
selector:
app: mongodb
ports:
- port: 27017
EOF
7:30 AM - Reconfigure Replica Set
// Connect to mongodb-0
use admin
// Get current config
cfg = rs.conf()
// Update to use DNS names instead of IPs
cfg.members[0].host = "mongodb-0.mongodb:27017"
cfg.members[1].host = "mongodb-1.mongodb:27017"
cfg.members[2].host = "mongodb-2.mongodb:27017"
// Reconfigure
rs.reconfig(cfg)
7:35 AM - Replica Set Recovers
# New replica set status:
rs0:PRIMARY> rs.status()
{
members: [
{ name: "mongodb-0.mongodb:27017", stateStr: "PRIMARY" },
{ name: "mongodb-1.mongodb:27017", stateStr: "SECONDARY" },
{ name: "mongodb-2.mongodb:27017", stateStr: "SECONDARY" }
]
}
β Replica set healthy!
7:40 AM - Data Inconsistency Discovered
# Check data on each replica
db.analytics.count() # mongodb-0: 12,450,382
db.analytics.count() # mongodb-1: 12,449,201 (1,181 docs behind)
db.analytics.count() # mongodb-2: 12,448,905 (1,477 docs behind)
Problem: During the 45-minute outage, some writes succeeded on primary but weren't replicated!
7:45 AM - Rollback Decision - Data inconsistency unacceptable - Must restore from backup - Lose 5 hours of data
8:00 AM - Restore from Backup
# Restore from 1:30 AM backup (before migration)
# Lose all data from 1:30 AM - 7:45 AM (6 hours 15 minutes)
10:00 AM - Service Restored - Database back online - Lost 6+ hours of analytics data - Customers notified of data loss
The Damage¶
Financial: - $3.2M in lost revenue (8 hours downtime during peak hours) - $400K in customer refunds (analytics data lost) - $150K emergency contractor fees (data recovery specialists) - Total: $3.75M
Operational: - 8 hours of complete downtime - 6 hours of data lost (analytics for 15,000 customers) - 250 support tickets filed - 45 enterprise customers lost trust
Reputational: - 3 major customers cancelled contracts (total $2M ARR) - Press coverage of the outage - Stock price dropped 8% on news
Root Cause Analysis¶
Immediate Cause:
- Service not configured as headless (clusterIP: None)
- MongoDB replica set used pod IPs instead of DNS names
- Pod rescheduling changed IP, breaking replica set
Contributing Factors:
- Insufficient Testing:
- Never tested pod rescheduling in staging
- Didn't validate per-pod DNS before production
-
No chaos engineering (pod deletion tests)
-
Lack of Knowledge:
- Team didn't know about headless services
- Used pod IPs thinking it was "more reliable"
-
Didn't read StatefulSet documentation thoroughly
-
No Validation:
- No automated check that service was headless
- No DNS tests before declaring migration complete
-
Manual configuration prone to errors
-
Poor Runbook:
- No procedure for StatefulSet deployments
- No checklist for headless service requirements
- Engineers learning on the fly
What Should Have Been Done¶
Correct Configuration:
# β
CORRECT: Headless service
apiVersion: v1
kind: Service
metadata:
name: mongodb
spec:
clusterIP: None # β CRITICAL for StatefulSets!
selector:
app: mongodb
ports:
- port: 27017
- port: 27018 # Optional: direct pod access
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mongodb
spec:
serviceName: mongodb # Must match headless service name
replicas: 3
selector:
matchLabels:
app: mongodb
template:
metadata:
labels:
app: mongodb
spec:
containers:
- name: mongodb
image: mongo:5.0
command:
- mongod
- --replSet=rs0
- --bind_ip_all
env:
# Use DNS names, not IPs!
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
Initialization Script:
// β
Use DNS names (survive pod rescheduling)
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "mongodb-0.mongodb:27017" }, // β
DNS name
{ _id: 1, host: "mongodb-1.mongodb:27017" }, // β
DNS name
{ _id: 2, host: "mongodb-2.mongodb:27017" } // β
DNS name
]
})
// β NEVER use pod IPs!
// They change when pods reschedule!
Validation Tests:
# Test 1: Verify service is headless
kubectl get service mongodb -o jsonpath='{.spec.clusterIP}'
# Must output: None
# Test 2: Verify per-pod DNS works
kubectl run -it --rm debug --image=busybox:1.37 -- \
nslookup mongodb-0.mongodb
# Must resolve to pod IP
# Test 3: Test pod rescheduling (chaos engineering)
kubectl delete pod mongodb-1
# Wait for pod to reschedule
# Verify replica set still healthy
# Verify new pod IP doesn't break anything
Lessons Learned¶
-
Always use headless services for StatefulSets
-
Never use pod IPs in application configuration
- Pod IPs change on rescheduling
- Always use DNS names
-
DNS names are stable
-
Test pod rescheduling before production
-
Automate validation
-
Document StatefulSet requirements
- Checklist for headless services
- Runbook for common issues
- Training for all engineers
Post-Incident Improvements¶
1. Pre-flight Checks:
#!/bin/bash
# validate-statefulset.sh
echo "Validating StatefulSet deployment..."
# Check 1: Service is headless
SVC_NAME="$1"
CLUSTER_IP=$(kubectl get svc "$SVC_NAME" -o jsonpath='{.spec.clusterIP}')
if [[ "$CLUSTER_IP" != "None" ]]; then
echo "β FAIL: Service $SVC_NAME is not headless (clusterIP: $CLUSTER_IP)"
exit 1
fi
echo "β
Service is headless"
# Check 2: StatefulSet references correct service
SS_NAME="$2"
SVC_NAME_IN_SS=$(kubectl get statefulset "$SS_NAME" -o jsonpath='{.spec.serviceName}')
if [[ "$SVC_NAME_IN_SS" != "$SVC_NAME" ]]; then
echo "β FAIL: StatefulSet serviceName mismatch"
exit 1
fi
echo "β
StatefulSet references correct service"
# Check 3: Per-pod DNS works
for i in $(seq 0 2); do
POD_NAME="${SS_NAME}-${i}"
DNS_NAME="${POD_NAME}.${SVC_NAME}"
kubectl run dns-test-$i --image=busybox:1.37 --rm -it --restart=Never -- \
nslookup "$DNS_NAME" &>/dev/null
if [[ $? -ne 0 ]]; then
echo "β FAIL: DNS resolution failed for $DNS_NAME"
exit 1
fi
echo "β
DNS works for $DNS_NAME"
done
echo "β
All checks passed!"
2. Chaos Engineering:
# Test pod rescheduling regularly
while true; do
# Delete random replica
POD=$(kubectl get pods -l app=mongodb -o name | shuf -n1)
kubectl delete "$POD"
# Wait for recovery
kubectl wait --for=condition=Ready "$POD" --timeout=60s
# Verify replica set health
kubectl exec mongodb-0 -- mongosh --eval "rs.status()"
sleep 300 # 5 minutes
done
3. Policy Enforcement:
# OPA policy: Require headless service for StatefulSets
package kubernetes.admission
deny[msg] {
input.request.kind.kind == "StatefulSet"
svc_name := input.request.object.spec.serviceName
svc := data.kubernetes.services[svc_name]
svc.spec.clusterIP != "None"
msg := sprintf("StatefulSet %s requires headless service (clusterIP: None), but service %s has clusterIP: %s",
[input.request.object.metadata.name, svc_name, svc.spec.clusterIP])
}
π Common Use Cases for Headless Services¶
1. Database Clusters¶
MongoDB Replica Set:
apiVersion: v1
kind: Service
metadata:
name: mongo
spec:
clusterIP: None
selector:
app: mongo
ports:
- port: 27017
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mongo
spec:
serviceName: mongo
replicas: 3
template:
spec:
containers:
- name: mongo
image: mongo:5.0
command:
- mongod
- --replSet=rs0
Initialize:
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "mongo-0.mongo:27017" },
{ _id: 1, host: "mongo-1.mongo:27017" },
{ _id: 2, host: "mongo-2.mongo:27017" }
]
})
MySQL Master-Slave:
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
clusterIP: None
selector:
app: mysql
ports:
- port: 3306
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql
replicas: 3
template:
spec:
containers:
- name: mysql
image: mysql:8.0
env:
- name: MYSQL_MASTER_HOST
value: mysql-0.mysql # β
Stable DNS for master
2. Message Queues¶
Kafka Cluster:
apiVersion: v1
kind: Service
metadata:
name: kafka
spec:
clusterIP: None
selector:
app: kafka
ports:
- port: 9092
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: kafka
spec:
serviceName: kafka
replicas: 3
template:
spec:
containers:
- name: kafka
image: confluentinc/cp-kafka:latest
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: KAFKA_ADVERTISED_LISTENERS
value: PLAINTEXT://$(POD_NAME).kafka:9092
- name: KAFKA_ZOOKEEPER_CONNECT
value: zk-0.zk:2181,zk-1.zk:2181,zk-2.zk:2181
RabbitMQ Cluster:
apiVersion: v1
kind: Service
metadata:
name: rabbitmq
spec:
clusterIP: None
selector:
app: rabbitmq
ports:
- name: amqp
port: 5672
- name: management
port: 15672
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: rabbitmq
spec:
serviceName: rabbitmq
replicas: 3
template:
spec:
containers:
- name: rabbitmq
image: rabbitmq:3.9-management
env:
- name: RABBITMQ_DEFAULT_USER
value: admin
- name: RABBITMQ_ERLANG_COOKIE
value: secret-cookie
3. Distributed Systems¶
etcd Cluster:
apiVersion: v1
kind: Service
metadata:
name: etcd
spec:
clusterIP: None
selector:
app: etcd
ports:
- name: client
port: 2379
- name: peer
port: 2380
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: etcd
spec:
serviceName: etcd
replicas: 3
template:
spec:
containers:
- name: etcd
image: quay.io/coreos/etcd:v3.5
command:
- etcd
- --name=$(POD_NAME)
- --initial-advertise-peer-urls=http://$(POD_NAME).etcd:2380
- --advertise-client-urls=http://$(POD_NAME).etcd:2379
- --initial-cluster=etcd-0=http://etcd-0.etcd:2380,etcd-1=http://etcd-1.etcd:2380,etcd-2=http://etcd-2.etcd:2380
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
Zookeeper Ensemble:
apiVersion: v1
kind: Service
metadata:
name: zk
spec:
clusterIP: None
selector:
app: zk
ports:
- name: client
port: 2181
- name: follower
port: 2888
- name: election
port: 3888
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: zk
spec:
serviceName: zk
replicas: 3
template:
spec:
containers:
- name: zk
image: zookeeper:3.7
env:
- name: ZOO_SERVERS
value: server.1=zk-0.zk:2888:3888;2181 server.2=zk-1.zk:2888:3888;2181 server.3=zk-2.zk:2888:3888;2181
π― Best Practices¶
1. Always Use Headless Services for StatefulSets¶
# β
DO THIS:
apiVersion: v1
kind: Service
metadata:
name: my-statefulset-svc
spec:
clusterIP: None # Always for StatefulSets!
selector:
app: my-app
# β NOT THIS:
spec:
# clusterIP: 10.96.100.50 # Wrong for StatefulSets!
2. Use DNS Names, Never Pod IPs¶
// β
DO THIS:
rs.initiate({
members: [
{ host: "mongo-0.mongo:27017" }, // DNS name
{ host: "mongo-1.mongo:27017" },
{ host: "mongo-2.mongo:27017" }
]
})
// β NOT THIS:
rs.initiate({
members: [
{ host: "10.244.1.5:27017" }, // IP changes on reschedule!
{ host: "10.244.1.6:27017" },
{ host: "10.244.1.7:27017" }
]
})
3. Match serviceName in StatefulSet¶
apiVersion: v1
kind: Service
metadata:
name: database # β Service name
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: db
spec:
serviceName: database # β Must match service name!
4. Test Per-Pod DNS Before Production¶
# Validation script
for i in 0 1 2; do
kubectl run dns-test-$i --rm -it --image=busybox:1.37 -- \
nslookup my-app-$i.my-service
done
5. Combine Headless + Regular Services¶
# Headless service for StatefulSet (per-pod DNS)
apiVersion: v1
kind: Service
metadata:
name: mysql-headless
spec:
clusterIP: None
selector:
app: mysql
---
# Regular service for read traffic (load balanced)
apiVersion: v1
kind: Service
metadata:
name: mysql-read
spec:
selector:
app: mysql
ports:
- port: 3306
# Usage:
# Write to master: mysql-0.mysql-headless:3306
# Read from any replica: mysql-read:3306 (load balanced)
6. Use Init Containers for DNS Verification¶
apiVersion: apps/v1
kind: StatefulSet
spec:
template:
spec:
initContainers:
- name: verify-dns
image: busybox:1.37
command:
- sh
- -c
- |
# Wait for DNS to be ready
until nslookup $(hostname).my-service; do
echo "Waiting for DNS..."
sleep 2
done
echo "DNS is ready!"
7. Monitor DNS Resolution¶
# Prometheus ServiceMonitor for DNS health
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: dns-health
spec:
selector:
matchLabels:
app: my-statefulset
endpoints:
- port: metrics
path: /metrics
interval: 30s
π οΈ Troubleshooting Guide¶
Problem 1: "Cannot resolve pod DNS name"¶
Symptom:
$ nslookup pod-0.my-service
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
nslookup: can't resolve 'pod-0.my-service': Name or service not known
Diagnosis:
Solution:
# Service must have clusterIP: None
kubectl delete service my-service
kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
clusterIP: None # β Fix: Make it headless
selector:
app: my-app
EOF
Problem 2: "DNS returns wrong pod IP"¶
Symptom:
Diagnosis:
# Check StatefulSet serviceName
kubectl get statefulset my-app -o yaml | grep serviceName
# Check if names match
kubectl get service my-service
Solution:
# StatefulSet serviceName must match Service name exactly
apiVersion: apps/v1
kind: StatefulSet
spec:
serviceName: my-service # β Must match Service metadata.name
Problem 3: "Service has ClusterIP, can't change to None"¶
Symptom:
$ kubectl patch service my-service -p '{"spec":{"clusterIP":"None"}}'
The Service "my-service" is invalid: spec.clusterIP: Invalid value: "None": field is immutable
Solution:
# Must delete and recreate (risky in production!)
kubectl delete service my-service
kubectl apply -f headless-service.yaml
# Or use blue-green deployment:
# 1. Create new headless service with different name
# 2. Update StatefulSet to use new service
# 3. Delete old service
Problem 4: "Pods can't find each other after reschedule"¶
Symptom:
# MongoDB replica set member unreachable after pod reschedule
MongoError: no primary found in replica set
Diagnosis:
Solution:
// Reconfigure to use DNS names
cfg = rs.conf()
cfg.members[0].host = "mongo-0.mongo:27017" // DNS name
cfg.members[1].host = "mongo-1.mongo:27017"
cfg.members[2].host = "mongo-2.mongo:27017"
rs.reconfig(cfg)
π Key Takeaways¶
Must Remember¶
- Headless services have
clusterIP: None- No virtual IP, DNS returns pod IPs directly - StatefulSets require headless services - For stable per-pod DNS names
- DNS format:
pod-name.service-name.namespace.svc.cluster.local - Never use pod IPs in config - They change on reschedule, use DNS names
- Regular service returns random pod - Headless service allows targeting specific pods
StatefulSet + Headless Service Checklist¶
Before deploying to production:
- Service has
clusterIP: None - StatefulSet
serviceNamematches Service name - Application uses DNS names (not pod IPs)
- Tested per-pod DNS resolution
- Tested pod rescheduling (delete pod, verify app still works)
- Init containers verify DNS before app starts
- Monitoring on DNS resolution
- Runbook for common issues
Common Mistakes to Avoid¶
β Forgetting clusterIP: None - Service won't provide per-pod DNS
β Using pod IPs - IPs change on reschedule, breaking apps
β Mismatched service names - StatefulSet serviceName must match Service name
β Testing only once - Test pod rescheduling, deletion, failures
β No validation - Automate checks for headless service
β Assuming it works - Verify DNS resolution before declaring success
π Achievement Unlocked!¶
StatefulSet Master - You can now: - β Configure headless services for StatefulSets - β Understand per-pod DNS naming conventions - β Deploy stateful applications (databases, message queues) - β Avoid the $3.2M MongoDB migration mistake - β Troubleshoot DNS and service issues
World 3 Complete! You've mastered Kubernetes networking and services: - Service selectors and labels - NodePort for external access - DNS resolution and naming - Ingress routing - NetworkPolicy for traffic control - Session affinity for stateful apps - Cross-namespace communication - Readiness probes and endpoints - LoadBalancer vs NodePort - Headless services for StatefulSets
π― What's Next?¶
You've completed World 3: Networking & Services (2,300 XP)!
Continue Your Journey:¶
- World 4: Storage & Persistence (Coming soon)
- World 5: Configuration & Secrets (Coming soon)
- World 6: Security & RBAC (Coming soon)
Further Learning:¶
- Kubernetes Documentation: Headless Services
- StatefulSets: StatefulSet Basics
- DNS for Services: DNS for Services and Pods
- Real Examples: Running MongoDB on Kubernetes
"Headless services are not missing a headβthey're giving each pod its own identity." - Kubernetes Wisdom
Remember: For StatefulSets, always use clusterIP: None. Your database cluster will thank you!