7.2 Cluster DNS & Service Discovery with CoreDNS
Key Takeaways
CoreDNS is the CNCF graduated project that serves cluster DNS, running as a Deployment in kube-system and exposed through the kube-dns Service.
Every Service gets an A/AAAA record at <service>.<namespace>.svc.cluster.local resolving to the Service ClusterIP.
The kubelet writes a search list into each Pod's /etc/resolv.conf so a bare Service name resolves within the Pod's own namespace, while cross-namespace calls need at least <service>.<namespace>.
A headless Service (clusterIP: None) returns the individual Pod IPs instead of a single virtual IP, which is what gives StatefulSet Pods stable per-Pod DNS names.
Environment-variable service discovery exists but only covers Services created before the Pod, so DNS is the correct mechanism in every modern cluster.
7.2 Cluster DNS & Service Discovery with CoreDNS
Quick Answer: Kubernetes ships an in-cluster DNS server — CoreDNS, a CNCF graduated project — running as a Deployment in
kube-systemand fronted by a Service conventionally namedkube-dns. Every Service receives an A/AAAA record at<service>.<namespace>.svc.cluster.localpointing at its ClusterIP. The kubelet writes a search list into every Pod's/etc/resolv.conf, which is whycurl http://paymentsworks inside the same namespace but a cross-namespace call needspayments.finance.
Section 7.1 explained that a Service provides a stable virtual IP. This section explains how a Pod ever learns that IP in the first place — and why "it's always DNS" is the oldest joke in platform engineering.
1. Why Discovery Must Be Dynamic
Pod IPs change on every reschedule. Hard-coding one is meaningless. Kubernetes therefore layers two stable abstractions on top:
Pod ──asks──► CoreDNS ──returns──► Service ClusterIP
│
kube-proxy rules
▼
one of the healthy backing Pod IPs
The application only ever needs to know a name. CoreDNS turns the name into a ClusterIP; kube-proxy turns the ClusterIP into a live endpoint.
2. CoreDNS
CoreDNS replaced the older kube-dns add-on as the cluster DNS default in Kubernetes 1.13. It is a single Go binary built from chained plugins, configured through a Corefile stored in a ConfigMap:
.:53 {
errors
health
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153 # exposes CoreDNS metrics for scraping
forward . /etc/resolv.conf # anything not cluster.local goes upstream
cache 30
loop
reload
loadbalance
}
Key facts KCNA expects:
- CoreDNS runs as a Deployment (typically two replicas) in
kube-system, not a DaemonSet. - It watches the Kubernetes API for Services and EndpointSlices and answers from that live view — it is not a zone file that needs reloading.
- The
forwardplugin sends non-cluster names (github.com) to the node's upstream resolvers. - The
prometheusplugin makes CoreDNS observable, which matters because DNS latency shows up as application latency everywhere at once. - NodeLocal DNSCache is an optional DaemonSet that puts a caching resolver on every node, cutting cross-node DNS traffic and conntrack pressure on large clusters.
3. The DNS Name Format
Services
<service-name>.<namespace>.svc.cluster.local
| Record type | Applies to | Returns |
|---|---|---|
| A / AAAA | Normal Service | The Service's ClusterIP |
| A / AAAA | Headless Service | The IP of every ready backing Pod |
| SRV | Named Service ports | _<port-name>._<protocol>.<service>.<namespace>.svc.cluster.local → port and target |
| CNAME | ExternalName Service | The external DNS name, with no proxying at all |
The Search List
The kubelet writes something like this into each Pod:
nameserver 10.96.0.10
search production.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
So, from a Pod in namespace production:
| What the app requests | What resolves | Works? |
|---|---|---|
payments | payments.production.svc.cluster.local | Yes, if payments is in production |
payments.finance | payments.finance.svc.cluster.local | Yes — cross-namespace |
payments.finance.svc.cluster.local | itself (FQDN) | Yes — most explicit |
payments for a Service in finance | payments.production… → NXDOMAIN | No |
The
ndots:5gotcha. Any name containing fewer than five dots is tried against every search-domain suffix first. Looking upapi.github.com(two dots) therefore generates three failing cluster queries before the correct one. On busy clusters this is a measurable source of DNS load and tail latency; the fix is a trailing dot (api.github.com.) or a per-PoddnsConfigloweringndots.
Pods
Pods also get records, with dots in the IP replaced by dashes: 10-244-1-15.production.pod.cluster.local. These are rarely used directly — except by StatefulSets.
4. Headless Services and StatefulSet Identity
Setting clusterIP: None creates a headless Service: no virtual IP is allocated and kube-proxy programs no rules. CoreDNS instead returns the A records of all ready backing Pods.
apiVersion: v1
kind: Service
metadata:
name: redis-service
spec:
clusterIP: None # headless
selector:
app: redis
ports:
- port: 6379
Combined with a StatefulSet's serviceName, this produces the stable per-Pod names that clustered databases require:
redis-cluster-0.redis-service.production.svc.cluster.local
redis-cluster-1.redis-service.production.svc.cluster.local
redis-cluster-2.redis-service.production.svc.cluster.local
A Cassandra or Kafka node needs to address a specific peer, not "whichever replica the load balancer picks" — headless DNS is what makes that possible.
5. DNS Policies
spec.dnsPolicy controls what the kubelet writes into resolv.conf:
| Policy | Behaviour |
|---|---|
ClusterFirst (default) | Cluster DNS first; anything outside the cluster suffix is forwarded upstream |
Default | Inherit the node's resolver configuration; the Pod cannot resolve cluster names |
ClusterFirstWithHostNet | Required for Pods using hostNetwork: true that still need cluster DNS |
None | Ignore everything and use the explicit dnsConfig block instead |
Trap: a Pod with
hostNetwork: trueand the defaultClusterFirstsilently gets the node's resolver and cannot resolve Service names.ClusterFirstWithHostNetis the fix.
6. Environment-Variable Discovery
Kubernetes also injects Docker-link-style variables for every Service that existed before the Pod started:
PAYMENTS_SERVICE_HOST=10.96.31.7
PAYMENTS_SERVICE_PORT=80
This mechanism is legacy and has a fatal ordering flaw: a Service created after the Pod produces no variables, so it silently fails to the application. DNS is the correct answer for service discovery in any modern cluster; environment variables exist for backwards compatibility only.
7. Triaging DNS Failures
kubectl get pods -n kube-system -l k8s-app=kube-dns # are CoreDNS Pods Ready?
kubectl logs -n kube-system -l k8s-app=kube-dns # SERVFAIL? loop detected?
kubectl run tmp --rm -it --image=busybox:1.36 --restart=Never -- \
nslookup payments.finance.svc.cluster.local # resolve from inside
kubectl exec <pod> -- cat /etc/resolv.conf # right nameserver and search list?
kubectl get endpointslices -l kubernetes.io/service-name=payments
| Symptom | Usual cause |
|---|---|
| Nothing in the cluster resolves | CoreDNS Pods not Ready, or the CNI plugin is broken |
| One Service does not resolve | Name or namespace typo, or the Service selector matches no Pods |
| Resolves but connections fail | DNS is fine — check the Service port, readiness probes, or a NetworkPolicy |
| Intermittent timeouts under load | ndots amplification, conntrack exhaustion; deploy NodeLocal DNSCache |
| Works in one namespace only | A bare name was used across namespaces — qualify it |
A Pod in the production namespace runs curl http://payments but the payments Service lives in the finance namespace. What happens?
It resolves correctly because cluster DNS searches every namespace
It succeeds only if a NetworkPolicy explicitly allows cross-namespace traffic
It resolves to the node's external DNS server and reaches the internet
It fails, because the Pod's search list expands a bare name inside its own namespace first — the call must use payments.finance
What does cluster DNS return for a headless Service (clusterIP: None)?
The A records of every ready backing Pod, with no virtual IP allocated
A single virtual IP that kube-proxy load balances across the backing Pods
A CNAME pointing to an external DNS name
Nothing — headless Services are not registered in DNS
How does CoreDNS learn about newly created Services?
An administrator edits the Corefile and reloads the DNS zone
Service records are written directly into etcd by kube-proxy
The kubelet pushes zone files to CoreDNS over gRPC on each node
CoreDNS watches the Kubernetes API server for Services and EndpointSlices and answers from that live view
Sections you finish are checked off in the contents.