62.5 ExternalName and headless Services: DNS-only Services and StatefulSet addresses
Why: the problem
Not every Service needs a virtual IP. Two special kinds of Service exist only in DNS, and both show up in real projects. The first is ExternalName. Suppose Zomato's order service talks to a payments database on Amazon RDS. The RDS hostname is long and differs per environment. If you hard-code it in every app, moving to a new database means changing every Deployment. An ExternalName Service named payments-db returns a DNS CNAME pointing to the RDS hostname. Apps always connect to payments-db, and only one object changes when the database moves. There is no proxying and no ClusterIP; it is just a DNS alias. The second is a headless Service, with clusterIP: None. Normal Services hide pods behind one IP. Sometimes you want the opposite. A Kafka or PostgreSQL cluster in a StatefulSet needs each member to be reachable by a stable name like pg-0.pg.data.svc.cluster.local, and clients like a gRPC library may want the full list of pod IPs to balance on their own. A headless Service returns the pod IPs directly from DNS, and with a StatefulSet each pod gets its own DNS record. In this lesson we create an ExternalName alias, prove it is a CNAME, build a headless Service for a 3-replica StatefulSet, resolve each pod by name, and see the common mistakes, such as expecting ports or TLS names to change.
What: every term in plain English
- ExternalName — A Service whose DNS name is a CNAME to an outside hostname.
- CNAME — A DNS record that says 'this name is an alias for that name'.
- Headless Service — A Service with clusterIP: None; DNS returns pod IPs.
- A record — A DNS record mapping a name to an IPv4 address.
- StatefulSet — A controller giving pods stable names like pg-0, pg-1.
- serviceName — StatefulSet field naming its governing headless Service.
- Pod DNS name — pod.service.namespace.svc.cluster.local, for StatefulSet pods.
- SRV record — DNS record carrying port numbers for named ports.
- publishNotReadyAddresses — Lists pods in DNS even before they are Ready.
- Client-side load balancing — The client picks among pod IPs itself.
How: step by step
- Create an ExternalName Service for a database host. payments-db.
- Resolve it from a pod. See the CNAME.
- Create a headless Service and a StatefulSet. 3 replicas.
- Resolve the Service name. Many A records.
- Resolve each pod name. pg-0, pg-1, pg-2.
- Compare with a normal Service. One ClusterIP.
- Clean up. Keep the net namespace.
Example 1: Alias an outside database with ExternalName
Goal. Alias an outside database with ExternalName.
payments-db.yaml
apiVersion: v1
kind: Service
metadata:
name: payments-db
namespace: net
spec:
type: ExternalName
externalName: payments.c9xk2abcd.ap-south-1.rds.amazonaws.com
What each line does
type: ExternalName— No selector, no ClusterIP, no endpoints.externalName— The real hostname, without a scheme or port.DNS answer— payments-db.net.svc.cluster.local CNAME to the RDS name.Ports— Clients still use the real port, for example 5432.Moving the DB— Change one field; apps keep their config.
Green output
$ kubectl apply -f payments-db.yaml
$ kubectl get svc -n net payments-db
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
payments-db ExternalName <none> payments.c9xk2abcd.ap-south-1.rds.amazonaws.com <none>
$ kubectl exec -n net curl -- nslookup -type=cname payments-db.net.svc.cluster.local
payments-db.net.svc.cluster.local canonical name = payments.c9xk2abcd.ap-south-1.rds.amazonaws.com
Expected result. DNS returns a CNAME. On kind the RDS name does not exist, so the final lookup fails, but the alias itself is visible. On EKS it would resolve to the database.
Example 2: A headless Service with a StatefulSet gives each pod a stable name
Goal. A headless Service with a StatefulSet gives each pod a stable name.
pg.yaml
apiVersion: v1
kind: Service
metadata:
name: pg
namespace: net
spec:
clusterIP: None
selector:
app: pg
ports:
- name: tcp
port: 8080
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: pg
namespace: net
spec:
serviceName: pg
replicas: 3
selector:
matchLabels:
app: pg
template:
metadata:
labels:
app: pg
spec:
containers:
- name: echo
image: hashicorp/http-echo:1.0
args: ["-listen=:8080", "-text=member"]
ports:
- name: tcp
containerPort: 8080
What each line does
clusterIP: None— Makes the Service headless.serviceName: pg— Links the StatefulSet to the headless Service so pods get DNS names.http-echo— Stands in for real database pods to keep the lab light.pg-0.pg— Short form inside the namespace.Order— Pods start one by one: pg-0, then pg-1, then pg-2.
Green output
$ kubectl apply -f pg.yaml
$ kubectl rollout status sts/pg -n net
partitioned roll out complete: 3 new pods have been updated...
$ kubectl get svc -n net pg
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
pg ClusterIP None <none> 8080/TCP
$ kubectl exec -n net curl -- nslookup pg-1.pg.net.svc.cluster.local
Name: pg-1.pg.net.svc.cluster.local
Address: 10.244.40.71
Expected result. pg-1 has its own DNS name. If pg-1 is recreated with a new IP, the name stays the same and DNS updates.
Example 3: Compare DNS answers: headless vs normal Service
Goal. Compare DNS answers: headless vs normal Service.
compare.sh
kubectl expose sts pg -n net --name pg-vip --port 8080
kubectl exec -n net curl -- nslookup pg.net.svc.cluster.local
kubectl exec -n net curl -- nslookup pg-vip.net.svc.cluster.local
kubectl exec -n net curl -- nslookup -type=srv _tcp._tcp.pg.net.svc.cluster.local
What each line does
pg— Headless: three A records, one per pod.pg-vip— Normal: one A record, the ClusterIP.SRV— Named port tcp gives port and pod name.Who balances— Headless: the client. Normal: kube-proxy.Use case— Headless for databases, gRPC client balancing, peer discovery.
Green output
$ bash compare.sh
service/pg-vip exposed
Name: pg.net.svc.cluster.local
Address: 10.244.40.70
Address: 10.244.40.71
Address: 10.244.21.12
Name: pg-vip.net.svc.cluster.local
Address: 10.96.77.140
_tcp._tcp.pg.net.svc.cluster.local service = 0 33 8080 pg-0.pg.net.svc.cluster.local.
_tcp._tcp.pg.net.svc.cluster.local service = 0 33 8080 pg-1.pg.net.svc.cluster.local.
_tcp._tcp.pg.net.svc.cluster.local service = 0 33 8080 pg-2.pg.net.svc.cluster.local.
Expected result. The headless name lists every pod; the normal Service returns one virtual IP. SRV records even carry the port and the pod names.
Every command, step by step (kind lab with kubectl)
-
Create the alias
kubectl apply -f payments-db.yamlWhat it does: ExternalName.
service/payments-db created -
Check no endpoints
kubectl get endpointslices -n net -l kubernetes.io/service-name=payments-dbWhat it does: None exist.
No resources found in net namespace. -
Resolve the CNAME
kubectl exec -n net curl -- nslookup -type=cname payments-dbWhat it does: Alias visible.
payments-db.net.svc.cluster.local canonical name = payments.c9xk2abcd.ap-south-1.rds.amazonaws.com -
Point it elsewhere
kubectl patch svc payments-db -n net -p '{"spec":{"externalName":"example.com"}}'What it does: Move without touching apps.
service/payments-db patched -
Curl through the alias
kubectl exec -n net curl -- curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: example.com' http://payments-dbWhat it does: Host header must match the real site.
200 -
Create StatefulSet
kubectl apply -f pg.yamlWhat it does: Headless plus sts.
statefulset.apps/pg created -
Watch order
kubectl get pods -n net -l app=pg -wWhat it does: pg-0 first.
pg-0 1/1 Running pg-1 0/1 ContainerCreating -
Resolve all
kubectl exec -n net curl -- nslookup pgWhat it does: Three IPs.
Address: 10.244.40.70 Address: 10.244.40.71 Address: 10.244.21.12 -
Call one member
kubectl exec -n net curl -- curl -s pg-2.pg:8080What it does: Direct pod.
member -
Delete and keep the name
kubectl delete pod -n net pg-2 kubectl exec -n net curl -- nslookup pg-2.pgWhat it does: New IP, same name.
Name: pg-2.pg.net.svc.cluster.local Address: 10.244.21.19 -
Clean up
kubectl delete -f pg.yaml kubectl delete svc -n net pg-vip payments-dbWhat it does: Done.
service "pg" deleted
When it is red 1
curl: (6) Could not resolve host: payments-db
Cause. The outside name does not exist or the node cannot reach external DNS.
Fix, step by step
- Check externalName for typos and test nslookup of the real host from a pod.
When it is red 2
SSL: no alternative certificate subject name matches target host name 'payments-db'
Cause. TLS cert is for the real host, not the alias.
Fix, step by step
- Connect with the real hostname for TLS, or use sslmode that checks the real name.
When it is red 3
nslookup pg-0.pg: NXDOMAIN
Cause. serviceName in the StatefulSet does not match the headless Service.
Fix, step by step
- Set serviceName to the headless Service name.
When it is red 4
ExternalName with an IP address does not work
Cause. externalName must be a DNS name.
Fix, step by step
- Use a Service without selector plus an EndpointSlice for raw IPs.
When it is red 5
Pod missing from headless DNS while starting
Cause. Not Ready pods are hidden.
Fix, step by step
- Set publishNotReadyAddresses: true for peer discovery.
Practice tasks
Easy Task 1: Alias
Why: Create.
What to do: Create payments-db.
- ExternalName.
Solution
kubectl get svc -n net payments-dbEasy Task 2: CNAME
Why: Resolve.
What to do: See the CNAME.
- nslookup -type=cname.
Solution
kubectl exec -n net curl -- nslookup -type=cname payments-dbEasy Task 3: Headless
Why: Create.
What to do: Create pg.
- clusterIP: None.
Solution
kubectl get svc -n net pgMedium Task 4: Members
Why: Resolve.
What to do: Resolve each pod.
- pg-N.pg.
Solution
kubectl exec -n net curl -- nslookup pg-0.pgMedium Task 5: Compare
Why: DNS.
What to do: Compare headless and ClusterIP.
- compare.sh.
Solution
bash compare.shMedium Task 6: SRV
Why: Ports.
What to do: Read SRV records.
- _tcp._tcp.
Solution
kubectl exec -n net curl -- nslookup -type=srv _tcp._tcp.pgMedium Task 7: Stable
Why: Recreate.
What to do: Delete pg-2 and resolve again.
- Same name.
Solution
kubectl delete pod -n net pg-2Medium Task 8: NotReady
Why: Discover.
What to do: Use publishNotReadyAddresses.
- Peer discovery.
Solution
kubectl explain service.spec.publishNotReadyAddressesHard Task 9: Ledger
Why: Main task.
What to do: Build ledger with peer names.
- StatefulSet.
Solution
kubectl get pods -n net -l app=ledgerHard Task 10: Explain
Why: Interview.
What to do: Explain headless for databases.
- Stable identity.
Solution
kubectl explain statefulset.spec.serviceNameHomework: try at home
For Zomato, create payments-db as an ExternalName to the RDS host in each environment, and a 3-member headless StatefulSet 'ledger' where each member can call the others by pg-style DNS names. Show the SRV records.
Recap
- ExternalName is a DNS CNAME alias to an outside host, with no proxy.
- Headless Services (clusterIP: None) return pod IPs from DNS.
- With a StatefulSet each pod gets a stable name like pg-0.pg.
- SRV records carry named ports.
- Next: CoreDNS, the server answering all these lookups.
FAQ
Q. Is ExternalName handled by kube-proxy?
No. It is purely DNS, answered by CoreDNS. kube-proxy writes no rules for it.
Q. Can a headless Service have no selector?
Yes. You then manage EndpointSlices yourself and DNS returns those addresses.
Q. Why use pod names instead of pod IPs in configs?
Pod IPs change on every restart. StatefulSet DNS names stay the same, so cluster member lists in config files stay valid.
Q. How long does DNS take to update after a pod restarts?
CoreDNS sees the EndpointSlice change within a second or two, but clients may cache the old answer for the record TTL, 30 seconds by default.
Q. Should I use ExternalName for every outside API?
Only where an alias really helps, such as databases or internal services that move between environments. For public HTTPS APIs, keep the real hostname so TLS checks and Host headers keep working.
Q. How does a Kafka client use the headless names?
Each broker advertises its own name, like kafka-1.kafka.data.svc.cluster.local, and clients connect straight to the broker that owns a partition. A single ClusterIP would send them to the wrong broker.
Q. What happens to DNS when a StatefulSet scales down?
The removed pod's record disappears, while the remaining names stay the same. Clients that cached the old IP may need to reconnect.
Q. Does ExternalName change the port?
No. Clients still connect to the real port.
Q. Does it work with HTTP Host headers?
The client sends Host: payments-db, which many sites reject. Fine for databases, tricky for HTTPS.
Q. Can I use headless with a Deployment?
Yes, for the list of IPs, but pods do not get stable names.
Q. Is a headless Service load balanced?
Only by the client, or by DNS answer order.
Q. Why do StatefulSets need it?
Members of clustered software find each other by stable names.
Q. Can I map an outside IP to a Service name?
Yes: a Service with no selector plus a manual EndpointSlice.
Interview questions
-
What is an ExternalName Service?
Model answer: A Service that returns a CNAME to an outside DNS name, without proxying.
-
What is a headless Service?
Model answer: A Service with clusterIP: None whose DNS returns pod IPs.
-
Why do StatefulSets use headless Services?
Model answer: To give each pod a stable DNS name for peer discovery.
-
What is a limitation of ExternalName?
Model answer: TLS and HTTP Host names do not match the alias; no IPs allowed.
-
How do you expose an outside IP inside the cluster?
Model answer: Service without selector plus an EndpointSlice with that IP.
-
How would you migrate apps from one database host to another with minimal change?
Model answer: Point an ExternalName Service at the old host, switch apps to use the Service name, then change externalName to the new host during the cutover window, keeping TLS hostname checks in mind.
Quick quiz
Q1. Does ExternalName have a ClusterIP?
No.
Q2. What does a headless Service return?
Pod IPs.
Q3. What links a StatefulSet to its Service?
serviceName.
Q4. Who balances with headless?
The client.
Q5. Can externalName be an IP?
No, it must be a hostname.
Samajla ka?