Sign in
← Kubernetes: debugging
6 min · hard · Lesson 8 of 12

DNS doesn't resolve: when CoreDNS is down

How to tell a general DNS outage from a single Service's problem, and the chain to follow to find the cause.

The incident shows up as "everything is broken". Apps can't find the database, the queue can't find the broker, health checks fail on half the services. Meanwhile the Pods are alive, the network between nodes works, and kubectl answers normally.

When the list of affected services is too wide, there's usually one cause: the cluster's DNS infrastructure. In this lesson you'll learn to tell a cluster-wide DNS outage apart from a single Service problem, and walk the check chain that shows exactly where it broke.

Concepts

How DNS works inside the cluster

CoreDNS runs as an ordinary Deployment. In clusters created by kubeadm (and in kind, which the course labs run on), it has two replicas by default; in k3s, one. In front of the Pods sits Service kube-dns with a fixed ClusterIP. kubelet writes this address into every new Pod's /etc/resolv.conf. Here's what the file looks like in a Pod from namespace default in kind:

search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5

nameserver is Service kube-dns's ClusterIP. In kind and kubeadm it's 10.96.0.10, in k3s it's 10.43.0.10. search lists the suffixes the resolver appends to incomplete names. ndots:5 means: if a name has fewer than five dots, the resolver first tries it with the suffixes appended, and only then asks for the name as is.

When curl webapp runs inside a Pod, here's what happens:

  1. the resolver appends a suffix: webapp.default.svc.cluster.local;
  2. it sends a UDP request to 10.96.0.10:53;
  3. rules that kube-proxy wrote into the node's kernel ahead of time route the packet to one of the CoreDNS Pods;
  4. CoreDNS replies with Service webapp's ClusterIP. It doesn't look up the Service in the API on every request: the kubernetes plugin keeps a copy of Services and EndpointSlices in memory and updates it through a watch.

If any link in this chain breaks, resolution goes down for the whole cluster at once.

Diagnosis: nslookup from a Pod

Command number one. Bring up a temporary Pod and ask DNS for a name that exists in any cluster, Service kubernetes in namespace default:

kubectl run dns-probe --image=busybox:1.36.1 --restart=Never -- sleep 3600
kubectl exec dns-probe -- nslookup kubernetes.default.svc.cluster.local

A healthy answer in kind:

Server:		10.96.0.10
Address:	10.96.0.10:53

Name:	kubernetes.default.svc.cluster.local
Address: 10.96.0.1

Instructions often give the short form, nslookup kubernetes.default. Busybox sends that name both with the search suffixes and as is. The "as is" query gets answered by an external DNS server, and on some machines it returns something of its own. Busybox prints that answer, and the output is confusing. The full name doesn't leave that ambiguity.

What you get instead of a healthy answer:

Symptom in busybox nslookup outputWhat it means
nslookup: write to '10.96.0.10': Connection refused and ;; connection timed out; no servers could be reachedService kube-dns has no ready addresses at all: there are no CoreDNS Pods, they aren't Ready, or the Service doesn't select them
;; connection timed out; no servers could be reached without Connection refusedkube-dns has ready addresses, but CoreDNS doesn't respond: the Pods just crashed, are overloaded, or a NetworkPolicy is dropping packets
** server can't find <name>: NXDOMAIN for every suffix, while kubernetes.default.svc.cluster.local resolves fineDNS works, that name just doesn't exist: a typo in the Service name, or the wrong namespace

The first two lines point to infrastructure, the last one to a specific name. These are different incidents. An empty address list on a Service (say, a selector that doesn't match the Pod's labels) has no effect on NXDOMAIN: a ClusterIP Service resolves even without Pods behind it.

Where CoreDNS lives

kubectl get deploy coredns -n kube-system
NAME      READY   UP-TO-DATE   AVAILABLE   AGE
coredns   2/2     2            2           30d

Pod status:

kubectl get pods -n kube-system -l k8s-app=kube-dns
NAME                       READY   STATUS    RESTARTS   AGE
coredns-7d764666f9-mmp4x   1/1     Running   0          30d
coredns-7d764666f9-nfg59   1/1     Running   0          30d

READY 0/1, CrashLoopBackOff, or no Pods at all mean DNS is down. Scaling to zero looks the same. Service kube-dns stays in place with its ClusterIP, but there's nothing behind it.

Check the address list behind the Service through EndpointSlice:

kubectl get endpointslice -n kube-system -l kubernetes.io/service-name=kube-dns
NAME             ADDRESSTYPE   PORTS     ENDPOINTS   AGE
kube-dns-bll6p   IPv4          <unset>   <unset>     79s

<unset> in the ENDPOINTS column means there are no addresses at all: either there are no Pods, or the Service doesn't select them. Keep in mind this column also shows not-ready addresses. If the CoreDNS Pods are crashing in CrashLoopBackOff, their IPs stay listed there, just marked ready: false, and traffic won't go to them (conditions in EndpointSlice). You can see each address's readiness like this:

kubectl get endpointslice -n kube-system -l kubernetes.io/service-name=kube-dns \
  -o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]} ready={.conditions.ready}{"\n"}{end}'

The older kubectl get endpoints kube-dns -n kube-system command shows only ready addresses, and prints <none> when there are none. But the v1 Endpoints object is deprecated as of Kubernetes 1.33, and kubectl prints a warning about it.

No ready addresses on kube-dns means traffic has nowhere to go. Next you need to figure out why. There are three possibilities: zero replicas, Pods exist but aren't Ready, or the Pods are Ready but the Service doesn't select them. Check the last case the same way as in the selector lesson: put the Service's selector next to the Pods' labels.

Inside the Corefile

CoreDNS's config lives in ConfigMap coredns in kube-system:

kubectl get cm coredns -n kube-system -o yaml

The key lines: kubernetes cluster.local answers names under *.cluster.local, forward . /etc/resolv.conf sends external domains on to the node's DNS server.

CoreDNS finds an error in the Corefile while parsing it. What happens next depends on when:

The second case is sneakier. The Corefile edit goes through "with no consequences", DNS still answers, everyone moves on. Then a day later the CoreDNS Pods get recreated (a node restart, an upgrade, an eviction), they read the broken file from scratch, and DNS goes down across the whole cluster. The cause is visible in the logs:

kubectl logs -n kube-system -l k8s-app=kube-dns

If the container just restarted and the current log is empty, add --previous: kubectl will then show the output of the previous, crashed run.

Check the Corefile when Pods exist but keep crashing or aren't Ready, or when replicas are Ready but external names don't resolve. If replicas are at zero, the config isn't the issue.

Fixes

ProblemWhat to do
zero replicaskubectl scale deploy coredns -n kube-system --replicas=2
Pod in CrashLoopBackOffread the cause: kubectl logs -n kube-system -l k8s-app=kube-dns --previous
broken Corefilekubectl edit cm coredns -n kube-system, fix it, then kubectl -n kube-system rollout restart deployment/coredns
Pod Ready, EndpointSlice emptycompare Service kube-dns's selector with the CoreDNS Pods' labels

After you fix the ConfigMap, Pods in CrashLoopBackOff will come back up on their own: kubelet updates the file inside the Pod, and the next restart reads the fixed config. But both paths come with a delay. The ConfigMap file takes about a minute to reach the Pod (docs), and the pause between restarts doubles after every crash, up to five minutes (docs). If the Pods had been crashing for a while, the wait will be long too. rollout restart recreates the Pods right away, and they read the fixed config immediately.

After the fix, check resolution the same way, with nslookup from a test Pod. You usually don't need to restart your apps: libc's resolver hits DNS on every new request. The exception is programs with their own DNS cache. nginx, for example, resolves the name from proxy_pass once, when it loads its config (docs), and if it started while DNS was down, you'll need to restart it.

Common mistakes

How to avoid this again

Keep CoreDNS at a minimum of two replicas on different nodes (podAntiAffinity or topologySpreadConstraints). Alert on kube-dns having empty endpoints, not on "cluster availability": empty DNS endpoints are caught in seconds, while app degradation spreads out over minutes. Ship Corefile edits through a GitOps repository, so a broken config has something to roll back to.

The check order as a single list

deploy coredns replicas exist pods k8s-app=kube-dns Pod Ready endpoints kube-dns IP in the list nslookup kubernetes.default
Wherever the condition fails, that's where the cause is.
# How many replicas does the Deployment want, and how many are ready?
kubectl get deploy coredns -n kube-system

# Do CoreDNS Pods exist, and are they Ready?
kubectl get pods -n kube-system -l k8s-app=kube-dns

# Does Service kube-dns have addresses?
kubectl get endpointslice -n kube-system -l kubernetes.io/service-name=kube-dns

# Does DNS respond from a Pod?
kubectl exec dns-probe -- nslookup kubernetes.default.svc.cluster.local

The chain: deploy → pods → endpoints → nslookup. Wherever it breaks, that's where the cause is.

In short

What you'll need for the tasks

Done reading?

Mark the lesson as read to move your track progress forward.

Go through it with an AI agent

The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.

Get access for $49

Tell the agent: use MCP izi, give me lesson k8s-debug-08-coredns-down