Headless & Discovery

A Headless Service is a Service with clusterIP: None, which means it doesn’t get a virtual IP address. Instead of routing traffic through a Service IP, Headless Services return individual pod IPs directly via DNS. This enables direct pod-to-pod communication and custom service discovery patterns, making it essential for StatefulSets and applications that need to know about individual pod instances.

What is a Headless Service?

A Headless Service is created by setting clusterIP: None in the Service spec. Unlike regular Services that provide a single IP for load balancing, Headless Services return all pod IPs that match the selector, allowing clients to connect directly to specific pods.

graph TB subgraph regular[Regular Service] A[Client] --> B[Service IP: 10.96.0.1] B --> C[Load Balances to Pods] end subgraph headless[Headless Service] D[Client] --> E[Headless Service<br/>clusterIP: None] E --> F[Returns All Pod IPs] F --> G[Client Chooses Pod] G --> H[Direct Connection to Pod] end style B fill:#e8f5e9 style E fill:#fff4e1 style H fill:#fff4e1

How Headless Services Work

When you create a Headless Service:

  1. No ClusterIP allocated - The Service doesn’t get a virtual IP
  2. DNS returns pod IPs - DNS queries return individual pod IP addresses
  3. Direct pod access - Clients can connect directly to specific pods
  4. StatefulSet integration - Works seamlessly with StatefulSets for stable pod identities
graph LR A[Headless Service Created] --> B[clusterIP: None] B --> C[Selector Evaluates Pods] C --> D[DNS Records Created for Each Pod] D --> E[Client Queries DNS] E --> F[Receives All Pod IPs] F --> G[Client Connects Directly] style A fill:#e1f5ff style D fill:#e8f5e9 style G fill:#fff4e1

Headless Service Example

Here’s a basic Headless Service:

apiVersion: v1
kind: Service
metadata:
  name: web-headless
spec:
  clusterIP: None  # This makes it headless
  selector:
    app: web
  ports:
  - port: 80
    targetPort: 8080

Key difference: clusterIP: None instead of type: ClusterIP (or omitting type).

DNS Behavior

The DNS behavior differs significantly between regular and Headless Services:

Regular Service DNS

# DNS query returns Service IP
$ nslookup web-service.default.svc.cluster.local
Name:    web-service.default.svc.cluster.local
Address: 10.96.0.1  # Single Service IP

Headless Service DNS

# DNS query returns all pod IPs
$ nslookup web-headless.default.svc.cluster.local
Name:    web-headless.default.svc.cluster.local
Address: 10.244.1.5  # Pod 1 IP
Address: 10.244.1.6  # Pod 2 IP
Address: 10.244.2.3  # Pod 3 IP
graph TB A[DNS Query: web-headless] --> B[CoreDNS] B --> C[Returns Pod 1 IP: 10.244.1.5] B --> D[Returns Pod 2 IP: 10.244.1.6] B --> E[Returns Pod 3 IP: 10.244.2.3] F[Client] --> G[Receives All IPs] G --> H[Chooses Pod to Connect] style A fill:#e1f5ff style B fill:#fff4e1 style H fill:#e8f5e9

StatefulSet Integration

Headless Services are essential for StatefulSets, which provide stable network identities for pods:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: web-headless  # References Headless Service
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: web
        image: nginx:1.21
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: web-headless
spec:
  clusterIP: None
  selector:
    app: web
  ports:
  - port: 80
    targetPort: 80

With StatefulSets, each pod gets a stable DNS name:

  • web-0.web-headless.default.svc.cluster.local → Pod 0
  • web-1.web-headless.default.svc.cluster.local → Pod 1
  • web-2.web-headless.default.svc.cluster.local → Pod 2
graph TB A[StatefulSet: web] --> B[Pod: web-0] A --> C[Pod: web-1] A --> D[Pod: web-2] E[Headless Service: web-headless] --> B E --> C E --> D F[Client] --> G[web-0.web-headless...] F --> H[web-1.web-headless...] F --> I[web-2.web-headless...] G --> B H --> C I --> D style E fill:#e8f5e9 style B fill:#fff4e1 style C fill:#fff4e1 style D fill:#fff4e1

Direct Pod Access

Headless Services enable direct access to specific pods, which is useful for:

Database Replicas

apiVersion: v1
kind: Service
metadata:
  name: postgres-headless
spec:
  clusterIP: None
  selector:
    app: postgres
  ports:
  - port: 5432
    targetPort: 5432

Clients can connect to specific database replicas:

  • Primary: postgres-0.postgres-headless.default.svc.cluster.local:5432
  • Replica: postgres-1.postgres-headless.default.svc.cluster.local:5432

Leader Election

Applications can use Headless Services for leader election patterns:

apiVersion: v1
kind: Service
metadata:
  name: app-headless
spec:
  clusterIP: None
  selector:
    app: my-app
  ports:
  - port: 8080
    targetPort: 8080

Each pod instance can discover all other instances via DNS and implement leader election logic.

SRV Records

Headless Services also create SRV (Service) records that provide port information:

# Query SRV records
$ dig SRV app-headless.default.svc.cluster.local

# Returns:
app-headless.default.svc.cluster.local. 30 IN SRV 10 50 8080 web-0.app-headless.default.svc.cluster.local.
app-headless.default.svc.cluster.local. 30 IN SRV 10 50 8080 web-1.app-headless.default.svc.cluster.local.
app-headless.default.svc.cluster.local. 30 IN SRV 10 50 8080 web-2.app-headless.default.svc.cluster.local.

SRV records include:

  • Priority (10)
  • Weight (50)
  • Port (8080)
  • Target (pod DNS name)
graph LR A[SRV Query] --> B[CoreDNS] B --> C[Returns Pod Records] C --> D[Pod 0: Priority 10, Weight 50, Port 8080] C --> E[Pod 1: Priority 10, Weight 50, Port 8080] C --> F[Pod 2: Priority 10, Weight 50, Port 8080] style A fill:#e1f5ff style B fill:#fff4e1 style D fill:#e8f5e9 style E fill:#e8f5e9 style F fill:#e8f5e9

Use Cases

1. StatefulSets

Headless Services are required for StatefulSets to provide stable network identities:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  serviceName: mysql-headless  # Must be Headless
  replicas: 3
  # ... rest of spec

2. Database Clusters

Database clusters (MongoDB, Cassandra, etc.) need to know about all members:

apiVersion: v1
kind: Service
metadata:
  name: cassandra-headless
spec:
  clusterIP: None
  selector:
    app: cassandra
  ports:
  - port: 9042
    targetPort: 9042

Each Cassandra node can discover all other nodes via DNS.

3. Service Mesh

Service meshes often use Headless Services for direct pod-to-pod communication:

apiVersion: v1
kind: Service
metadata:
  name: app-headless
spec:
  clusterIP: None
  selector:
    app: my-app

4. Custom Service Discovery

Applications implementing custom service discovery can query Headless Service DNS to get all pod IPs:

import socket

# Get all pod IPs
pod_ips = socket.gethostbyname_ex('app-headless.default.svc.cluster.local')[2]
# Returns: ['10.244.1.5', '10.244.1.6', '10.244.2.3']

Headless vs Regular Service

graph TB subgraph regular[Regular Service] A[Single Service IP] B[Load Balancing] C[Client → Service IP → Pod] D[Pod Selection: Automatic] end subgraph headless[Headless Service] E[No Service IP] F[Direct Pod Access] G[Client → Pod IP] H[Pod Selection: Client Choice] end style regular fill:#fff4e1 style headless fill:#e8f5e9

Regular Service:

  • ✅ Automatic load balancing
  • ✅ Single stable endpoint
  • ✅ Simpler for most use cases
  • ❌ Can’t access specific pods
  • ❌ No pod-level DNS names

Headless Service:

  • ✅ Direct pod access
  • ✅ Pod-level DNS names (with StatefulSets)
  • ✅ Custom service discovery
  • ✅ Required for StatefulSets
  • ❌ No automatic load balancing
  • ❌ Client must handle pod selection

Complete Example: StatefulSet with Headless Service

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: web-headless
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: web
        image: nginx:1.21
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: web-headless
spec:
  clusterIP: None
  selector:
    app: web
  ports:
  - port: 80
    targetPort: 80

DNS names created:

  • web-0.web-headless.default.svc.cluster.local
  • web-1.web-headless.default.svc.cluster.local
  • web-2.web-headless.default.svc.cluster.local
  • web-headless.default.svc.cluster.local (returns all pod IPs)

When to Use Headless Services

Use Headless Services when:

StatefulSets - Required for StatefulSet stable network identities
Direct pod access - Need to connect to specific pod instances
Database clusters - Database replicas that need to discover each other
Custom discovery - Implementing custom service discovery logic
Leader election - Applications that need to know about all instances
Service mesh - Direct pod-to-pod communication in service meshes

Don’t use Headless Services when:

  • You need automatic load balancing (use regular Service)
  • Simple service-to-service communication (use ClusterIP)
  • You don’t need pod-level access
  • Standard microservices patterns work fine

Best Practices

  1. Use with StatefulSets - Headless Services are designed for StatefulSets
  2. Document pod selection - If clients choose pods, document the selection logic
  3. Handle pod failures - Clients must handle cases where specific pods are unavailable
  4. Use SRV records - Leverage SRV records for port information
  5. Monitor pod health - Since there’s no load balancing, monitor individual pods
  6. Consider readiness - Use readiness probes to ensure pods are ready before DNS inclusion
  7. Test DNS resolution - Verify DNS returns expected pod IPs
  8. Use stable pod names - With StatefulSets, pod names are stable and predictable

Troubleshooting

DNS Not Returning Pod IPs

  1. Check Service selector: kubectl get service <service-name> -o yaml | grep selector
  2. Verify pods match selector: kubectl get pods -l app=my-app
  3. Check clusterIP is None: kubectl get service <service-name> -o jsonpath='{.spec.clusterIP}'
  4. Test DNS resolution: kubectl run -it --rm debug --image=busybox --restart=Never -- nslookup <service-name>
  5. Check CoreDNS: kubectl get pods -n kube-system -l k8s-app=kube-dns

Pod-Specific DNS Not Working

  1. Verify StatefulSet: Pod-specific DNS only works with StatefulSets
  2. Check pod name format: Should be <statefulset-name>-<ordinal>
  3. Verify serviceName: StatefulSet serviceName must match Headless Service name
  4. Test FQDN: Use full FQDN: <pod-name>.<service-name>.<namespace>.svc.cluster.local

Cannot Connect to Specific Pod

  1. Verify pod is running: kubectl get pods
  2. Check pod IP: kubectl get pods -o wide
  3. Test connectivity: kubectl exec <pod-name> -- wget -O- http://<target-pod-ip>:<port>
  4. Check Network Policies: Verify Network Policies allow pod-to-pod communication
  5. Verify port: Ensure target port is correct

SRV Records Not Available

  1. Check DNS server: Ensure CoreDNS supports SRV records
  2. Use dig or nslookup: Some tools don’t query SRV records by default
  3. Verify port in Service: SRV records require port specification
  4. Test with dig: kubectl run -it --rm debug --image=busybox --restart=Never -- dig SRV <service-name>

See Also