Headless Services and ExternalName
SummaryCovers Headless Services (clusterIP: None) with DNS returning...
Covers Headless Services (clusterIP: None) with DNS returning...
Covers Headless Services (clusterIP: None) with DNS returning Pod IPs directly, their role in StatefulSets, ExternalName Services as CNAME aliases, Services without selectors with manually managed Endpoints, EndpointSlices in Kubernetes 1.21+, and a structured Service debugging checklist.
Headless Services and ExternalName
Not every Service needs a virtual IP. Sometimes you want DNS to return the actual Pod IPs, or you want to alias an external database inside your cluster’s DNS namespace. Kubernetes provides two specialized Service types for these cases: Headless Services and ExternalName Services.
Headless Services: clusterIP: None
A Headless Service is a regular Service with one key difference: clusterIP is set to None. When this field is None, Kubernetes does not allocate a virtual IP. Instead, DNS queries for the Service name return A records pointing directly to the IPs of the backing Pods.
apiVersion: v1
kind: Service
metadata:
name: db-headless
spec:
clusterIP: None
selector:
app: database
ports:
- port: 5432
targetPort: 5432
When a Pod resolves db-headless.default.svc.cluster.local, the response is not a single ClusterIP — it is a list of individual Pod IPs:
kubectl run dnstest --image=busybox:1.36 --rm -it --restart=Never -- nslookup db-headless
Expected output:
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: db-headless.default.svc.cluster.local
Address: 10.244.1.8
Address: 10.244.2.12
Address: 10.244.1.9
Each address is a Pod that matches the selector app: database. The client receives all IPs and can choose how to distribute connections — round-robin, random, or sticky. There is no kube-proxy load balancing.
Why Headless? The StatefulSet Connection
StatefulSets create Pods with stable, predictable identities: db-0, db-1, db-2. Each Pod needs a stable DNS name so that other components can address a specific instance — for example, the primary database replica vs. a read replica.
A Headless Service combined with a StatefulSet generates individual DNS records for each Pod:
db-0.db-headless.default.svc.cluster.local → 10.244.1.8
db-1.db-headless.default.svc.cluster.local → 10.244.2.12
db-2.db-headless.default.svc.cluster.local → 10.244.1.9
The pattern is <pod-name>.<headless-service>.<namespace>.svc.cluster.local. This gives every StatefulSet Pod a resolvable, stable DNS identity — even if the Pod is rescheduled to a different node with a different IP. The DNS name stays constant; only the A record’s target IP updates.
Without a Headless Service, StatefulSet Pods get IPs but no individual DNS names. For databases, message queues, and distributed systems that require peer discovery, this DNS-based identity is essential.
Creating a Headless Service with a StatefulSet
apiVersion: v1
kind: Service
metadata:
name: redis-headless
spec:
clusterIP: None
selector:
app: redis
ports:
- port: 6379
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis
spec:
serviceName: redis-headless
replicas: 3
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7
ports:
- containerPort: 6379
The serviceName field in the StatefulSet spec links it to the Headless Service. This linkage is what enables per-Pod DNS records. After applying:
kubectl apply -f redis-statefulset.yaml
Verify individual Pod DNS:
kubectl run dnstest --image=busybox:1.36 --rm -it --restart=Never -- nslookup redis-0.redis-headless
Expected output:
Name: redis-0.redis-headless.default.svc.cluster.local
Address: 10.244.1.15
ExternalName: CNAME to External Services
ExternalName Services do not proxy traffic. They create a DNS CNAME record that maps a cluster-internal name to an external DNS name.
apiVersion: v1
kind: Service
metadata:
name: prod-database
namespace: default
spec:
type: ExternalName
externalName: db.prod.example.com
When a Pod resolves prod-database.default.svc.cluster.local, CoreDNS returns a CNAME record pointing to db.prod.example.com. The Pod then resolves that external name through standard DNS. No Service proxy, no iptables rules, no port translation.
Use Case: Gradual Migration
ExternalName is valuable during migration from an external database to an in-cluster one. Your application connects to prod-database (the Service name). Initially, this resolves to the external RDS/Cloud SQL instance. When the in-cluster database is ready, you delete the ExternalName Service and create a ClusterIP Service with the same name pointing to the new in-cluster Pods. The application code does not change — only the Service definition.
# Phase 1: External database
kubectl get svc prod-database
# TYPE: ExternalName, EXTERNAL-IP: db.prod.example.com
# Phase 2: Switch to in-cluster database
kubectl delete svc prod-database
kubectl expose deployment postgres --name=prod-database --port=5432
# TYPE: ClusterIP, CLUSTER-IP: 10.96.55.100
Limitations
ExternalName Services have constraints:
- No selectors, no Endpoints — they are DNS-only.
- No port remapping. The Pod must connect to the port the external service listens on.
- HTTPS with TLS verification may fail because the certificate’s Subject Alternative Name (SAN) matches
db.prod.example.com, notprod-database.default.svc.cluster.local.
Services Without Selectors
A Service does not require a selector. When you omit the selector, Kubernetes does not create Endpoints automatically — you manage them manually. This is useful for:
- Routing to external IP addresses (not DNS names — that is ExternalName)
- Integrating legacy systems that sit outside the cluster
- Load balancing across a set of known IPs
apiVersion: v1
kind: Service
metadata:
name: external-api
spec:
ports:
- port: 443
targetPort: 443
---
apiVersion: v1
kind: Endpoints
metadata:
name: external-api # Must match the Service name
subsets:
- addresses:
- ip: 203.0.113.10
- ip: 203.0.113.11
ports:
- port: 443
The Service and Endpoints must share the same name. Traffic to external-api:443 is load-balanced across 203.0.113.10 and 203.0.113.11. You maintain the Endpoints object manually — if an IP changes or a host goes down, you update the Endpoints.
EndpointSlices
Kubernetes 1.21 introduced EndpointSlices as the successor to Endpoints objects. The motivation: a single Endpoints object for a Service with thousands of Pods becomes a large resource that triggers frequent, expensive updates across the cluster.
EndpointSlices split endpoint information into smaller chunks (default: 100 endpoints per slice). The kube-proxy watches EndpointSlices instead of Endpoints, reducing the bandwidth and processing cost of updates.
For CKAD purposes, you primarily interact with Endpoints (kubectl get ep). EndpointSlices are managed automatically and are visible via:
kubectl get endpointslices -l kubernetes.io/service-name=nginx
Expected output:
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
nginx-abc12 IPv4 80 10.244.1.3,10.244.1.4,10.244.2.5 5m
You do not create EndpointSlices manually for the exam. Understanding that they exist and why they replaced the older Endpoints mechanism is sufficient.
Debugging Services: A Systematic Checklist
When a Service is unreachable, work through these checks in order. Each step builds on the previous, narrowing the problem from broad to specific.
1. Do the Labels Match?
The most common Service failure: the selector does not match any Pod labels.
# Check the Service selector
kubectl describe svc my-service | grep Selector
# Check Pod labels
kubectl get pods --show-labels
# Verify the intersection
kubectl get pods -l app=my-app
If the kubectl get pods -l command returns no Pods, the selector is wrong.
2. Are Endpoints Populated?
kubectl get endpoints my-service
If the ENDPOINTS column shows <none>, no Pods match the selector. If it shows IPs, the Service is connected to Pods — the problem is elsewhere.
3. Are the Ports Correct?
The Service targetPort must match the port the container listens on:
# Service port configuration
kubectl get svc my-service -o yaml | grep -A 5 ports
# Container port
kubectl get pods -l app=my-app -o jsonpath='{.items[0].spec.containers[0].ports[*].containerPort}'
A mismatch means traffic reaches the Pod but lands on a port that nothing is listening on — the connection is refused.
4. Does DNS Resolve?
kubectl run dnstest --image=busybox:1.36 --rm -it --restart=Never -- nslookup my-service
If DNS does not resolve, check that the Service exists in the expected namespace and that CoreDNS Pods are running:
kubectl get pods -n kube-system -l k8s-app=kube-dns
5. Can You Reach the Pod Directly?
Bypass the Service entirely to verify the Pod is functional:
# Get a Pod IP from Endpoints
kubectl get ep my-service
# Curl it directly
kubectl run curltest --image=curlimages/curl --rm -it --restart=Never -- curl http://10.244.1.3:80
If direct access works but the Service does not, the issue is in Service routing (labels, ports, or kube-proxy). If direct access also fails, the problem is in the Pod itself (application crash, wrong port, image issue).
6. Complete Diagnostic Command Set
# Overview
kubectl describe svc my-service
# Endpoints
kubectl get ep my-service
# Pod readiness
kubectl get pods -l app=my-app -o wide
# Events (may show selector mismatches)
kubectl get events --field-selector involvedObject.name=my-service
Work through this checklist systematically on the exam rather than guessing. It covers the cause of nearly every Service connectivity problem you will encounter.
Practical Debugging Walkthrough
To solidify the debugging process, walk through a concrete scenario. Suppose you have a Deployment named api and a Service named api-svc, but clients report the Service is unreachable.
Step 1: Check Service and Endpoints
kubectl get svc api-svc
kubectl get ep api-svc
If Endpoints shows <none>, the selector is wrong. Compare:
kubectl describe svc api-svc | grep Selector
Output: Selector: app=api-server
kubectl get pods --show-labels | grep api
Output shows Pods with label app=api — the selector says api-server but the Pods have api. Fix the Service selector.
Step 2: Verify Port Configuration
If Endpoints are populated but connections fail:
kubectl get svc api-svc -o jsonpath='{.spec.ports[0].targetPort}'
Output: 8080
kubectl exec -it api-pod -- ss -tlnp
Output shows the process listening on port 3000, not 8080. Fix targetPort to 3000.
Step 3: Test End-to-End
After fixing both issues:
kubectl run curltest --image=curlimages/curl --rm -it --restart=Never -- curl -s http://api-svc:80
A successful response confirms the Service is working. This three-step pattern — check endpoints, verify ports, test connectivity — resolves the vast majority of Service issues in under a minute.
Service Mesh vs. Kubernetes Services
You may encounter references to service meshes (Istio, Linkerd) in Kubernetes documentation. Service meshes operate at a layer above Kubernetes Services, injecting sidecar proxies into every Pod to add mutual TLS, retry logic, circuit breaking, and fine-grained traffic routing. The CKAD exam does not test service mesh concepts. Kubernetes Services remain the foundational networking primitive — service meshes build on top of them, not replace them.
Summary Table: Service Types
| Type | ClusterIP | NodePort | LoadBalancer | Headless | ExternalName |
|---|---|---|---|---|---|
| Virtual IP | Yes | Yes | Yes | No | No |
| Internal access | Yes | Yes | Yes | Yes (Pod IPs) | CNAME |
| External access | No | Yes (node port) | Yes (LB IP) | No | No |
| DNS returns | ClusterIP | ClusterIP | ClusterIP | Pod IPs | CNAME |
| Use case | Pod-to-Pod | Dev/testing | Production | StatefulSets | External DB |