<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Zain Ulabidin</title>
    <description>The latest articles on DEV Community by Zain Ulabidin (@zain_ulabidin_5a2cea24457).</description>
    <link>https://dev.to/zain_ulabidin_5a2cea24457</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4235138%2F64f09452-03e9-4954-ac5e-e0be78ccc825.png</url>
      <title>DEV Community: Zain Ulabidin</title>
      <link>https://dev.to/zain_ulabidin_5a2cea24457</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zain_ulabidin_5a2cea24457"/>
    <language>en</language>
    <item>
      <title>Breaking Kubernetes DNS on Purpose: A Hands-On CoreDNS Tutorial</title>
      <dc:creator>Zain Ulabidin</dc:creator>
      <pubDate>Mon, 09 Nov 2026 12:55:23 +0000</pubDate>
      <link>https://dev.to/zain_ulabidin_5a2cea24457/breaking-kubernetes-dns-on-purpose-a-hands-on-coredns-tutorial-4m1a</link>
      <guid>https://dev.to/zain_ulabidin_5a2cea24457/breaking-kubernetes-dns-on-purpose-a-hands-on-coredns-tutorial-4m1a</guid>
      <description>&lt;p&gt;Every pod in a Kubernetes cluster can find every Service by name, with zero DNS configuration from you. Type &lt;code&gt;db&lt;/code&gt; in your app's connection string and it just works. This post takes that magic apart, one concept at a time, on a live cluster — because DNS is the thing everyone blames during an outage, and the thing surprisingly few people can actually debug.&lt;/p&gt;

&lt;p&gt;Everything below was run on a real cluster on my machine. The outputs are real. You should run it too — total time is about an hour, and the cluster is disposable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why kind?
&lt;/h2&gt;

&lt;p&gt;Any local Kubernetes works for this — kind, k3d, minikube, microk8s — because &lt;strong&gt;every one of them ships CoreDNS as the cluster DNS&lt;/strong&gt; (it's been the default since Kubernetes 1.13, replacing the older kube-dns). But they're not equally good for learning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;kind&lt;/strong&gt; (Kubernetes in Docker) uses a vanilla kubeadm-style deployment, so its CoreDNS setup matches the official docs exactly. What you learn transfers 1:1 to any conformant cluster. Runs entirely in Docker — no VM, cluster up in about a minute, and you can destroy and recreate it freely (which matters, because we're going to break DNS on purpose).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;k3d/k3s&lt;/strong&gt; works too, but k3s manages CoreDNS through its own bundled manifests with quirks — direct edits to the CoreDNS ConfigMap get reverted by its manifest reconciler unless you use its customization hooks (the &lt;code&gt;coredns-custom&lt;/code&gt; ConfigMap, &lt;code&gt;NodeHosts&lt;/code&gt;). Worth knowing if you run k3s, but it's noise while learning fundamentals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;microk8s&lt;/strong&gt; on a Mac needs a Multipass VM and treats DNS as an addon (&lt;code&gt;microk8s enable dns&lt;/code&gt;) — more friction, less transferable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;kind kubectl     &lt;span class="c"&gt;# or your platform's equivalent&lt;/span&gt;
kind create cluster
kubectl cluster-info &lt;span class="nt"&gt;--context&lt;/span&gt; kind-kind
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Kubernetes control plane is running at https://127.0.0.1:55842
CoreDNS is running at https://127.0.0.1:55842/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CoreDNS is already there. Let's go find it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Concept 1: CoreDNS is just a normal app running &lt;em&gt;in&lt;/em&gt; the cluster
&lt;/h2&gt;

&lt;p&gt;DNS isn't baked into Kubernetes itself. It's an ordinary Deployment, scheduled like any other workload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system get deploy,pods,svc &lt;span class="nt"&gt;-l&lt;/span&gt; k8s-app&lt;span class="o"&gt;=&lt;/span&gt;kube-dns &lt;span class="nt"&gt;-o&lt;/span&gt; wide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME                      READY   UP-TO-DATE   AVAILABLE   CONTAINERS   IMAGES
deployment.apps/coredns   2/2     2            2           coredns      registry.k8s.io/coredns/coredns:v1.14.2

NAME                           READY   STATUS    IP           NODE
pod/coredns-589f44dc88-jn9fw   1/1     Running   10.244.0.2   kind-control-plane
pod/coredns-589f44dc88-nl8ff   1/1     Running   10.244.0.3   kind-control-plane

NAME               TYPE        CLUSTER-IP   PORT(S)
service/kube-dns   ClusterIP   10.96.0.10   53/UDP,53/TCP,9153/TCP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three objects, all in &lt;code&gt;kube-system&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A Deployment (&lt;code&gt;coredns&lt;/code&gt;, 2 replicas).&lt;/strong&gt; Two replicas for availability — if one pod dies, DNS keeps working while it restarts. Each pod gets a regular pod IP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A ClusterIP Service (&lt;code&gt;kube-dns&lt;/code&gt;) at &lt;code&gt;10.96.0.10&lt;/code&gt;.&lt;/strong&gt; The single stable IP every pod sends DNS queries to. Note the name quirk: the &lt;em&gt;Service&lt;/em&gt; is called &lt;code&gt;kube-dns&lt;/code&gt; even though the software is CoreDNS — kept for compatibility with the DNS server it replaced.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The ports tell you the roles:&lt;/strong&gt; &lt;code&gt;53/UDP&lt;/code&gt; and &lt;code&gt;53/TCP&lt;/code&gt; are DNS itself (UDP is the normal path, TCP the fallback for large responses). &lt;code&gt;9153/TCP&lt;/code&gt; is not DNS at all — it's Prometheus metrics. CoreDNS reports on itself via a plain HTTP &lt;code&gt;/metrics&lt;/code&gt; endpoint: query counts, response codes, cache hits/misses. Nothing scrapes it by default; in production you point Prometheus at it and alert on things like SERVFAIL spikes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The mental model: &lt;strong&gt;pod → asks &lt;code&gt;10.96.0.10:53&lt;/code&gt; → Service load-balances to one of the CoreDNS pods → CoreDNS answers.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Concept 2: How every pod gets wired to CoreDNS
&lt;/h2&gt;

&lt;p&gt;How does a random pod you deploy know to ask &lt;code&gt;10.96.0.10&lt;/code&gt;? Nobody configures your app pods manually. Look inside one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl run dnstest &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;busybox:1.36 &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Never &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;cat&lt;/span&gt; /etc/resolv.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;search&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;.&lt;span class="n"&gt;svc&lt;/span&gt;.&lt;span class="n"&gt;cluster&lt;/span&gt;.&lt;span class="n"&gt;local&lt;/span&gt; &lt;span class="n"&gt;svc&lt;/span&gt;.&lt;span class="n"&gt;cluster&lt;/span&gt;.&lt;span class="n"&gt;local&lt;/span&gt; &lt;span class="n"&gt;cluster&lt;/span&gt;.&lt;span class="n"&gt;local&lt;/span&gt;
&lt;span class="n"&gt;nameserver&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;.&lt;span class="m"&gt;96&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;10&lt;/span&gt;
&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="n"&gt;ndots&lt;/span&gt;:&lt;span class="m"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These three lines are injected by the &lt;strong&gt;kubelet&lt;/strong&gt; into every pod it starts (for the default &lt;code&gt;dnsPolicy: ClusterFirst&lt;/code&gt;). Nothing inside the container image put them there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;nameserver 10.96.0.10&lt;/code&gt;&lt;/strong&gt; — exactly the &lt;code&gt;kube-dns&lt;/code&gt; Service ClusterIP. The kubelet is started with that IP as a flag (&lt;code&gt;--cluster-dns&lt;/code&gt;) and stamps it into every pod. That one line is the entire wiring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;search&lt;/code&gt; + &lt;code&gt;ndots:5&lt;/code&gt;&lt;/strong&gt; — these two work together and cause ~80% of real-world Kubernetes DNS confusion, so here's the mechanic precisely:&lt;/p&gt;

&lt;p&gt;When a program looks up a name, the resolver counts the dots in it. If the name has &lt;strong&gt;fewer than 5 dots&lt;/strong&gt; (&lt;code&gt;ndots:5&lt;/code&gt;) and doesn't end in a literal &lt;code&gt;.&lt;/code&gt;, it is &lt;em&gt;not&lt;/em&gt; tried as-is first — instead the resolver walks the search domains, appending each:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You look up &lt;code&gt;db&lt;/code&gt; → it tries &lt;code&gt;db.default.svc.cluster.local&lt;/code&gt;, then &lt;code&gt;db.svc.cluster.local&lt;/code&gt;, then &lt;code&gt;db.cluster.local&lt;/code&gt;, then bare &lt;code&gt;db&lt;/code&gt;. The first search domain is namespace-specific — that's what lets your app connect to just &lt;code&gt;db&lt;/code&gt; and reach the &lt;code&gt;db&lt;/code&gt; Service &lt;strong&gt;in its own namespace&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;You look up &lt;code&gt;db.payments&lt;/code&gt; → the second search domain expands it to &lt;code&gt;db.payments.svc.cluster.local&lt;/code&gt; — cross-namespace shorthand.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The dark side: &lt;code&gt;github.com&lt;/code&gt; has only &lt;strong&gt;1 dot&lt;/strong&gt; — fewer than 5. So a pod resolving it first tries &lt;code&gt;github.com.default.svc.cluster.local&lt;/code&gt; (NXDOMAIN), &lt;code&gt;github.com.svc.cluster.local&lt;/code&gt; (NXDOMAIN), &lt;code&gt;github.com.cluster.local&lt;/code&gt; (NXDOMAIN), and only &lt;em&gt;then&lt;/em&gt; &lt;code&gt;github.com&lt;/code&gt;. Every external lookup from a pod costs up to four round trips. Remember this for the NodeLocal DNSCache discussion at the end.&lt;/p&gt;

&lt;p&gt;Watch the walk happen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl run dnstest &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;busybox:1.36 &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Never &lt;span class="nt"&gt;--&lt;/span&gt; nslookup kubernetes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Server:     10.96.0.10
Address:    10.96.0.10:53

** server can't find kubernetes.svc.cluster.local: NXDOMAIN
** server can't find kubernetes.cluster.local: NXDOMAIN

Name:    kubernetes.default.svc.cluster.local
Address: 10.96.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bare &lt;code&gt;kubernetes&lt;/code&gt; (0 dots) → expanded via the first search domain → answered with the API server's Service IP. (Busybox's &lt;code&gt;nslookup&lt;/code&gt; fires the queries for all search domains in parallel instead of stopping at the first hit, which is why the NXDOMAINs print in jumbled order — and each name is looked up twice, once for A/IPv4 and once for AAAA/IPv6.)&lt;/p&gt;




&lt;h2&gt;
  
  
  Concept 3: The Corefile — CoreDNS is a plugin chain
&lt;/h2&gt;

&lt;p&gt;Now for how CoreDNS &lt;em&gt;knew&lt;/em&gt; that answer. Its entire configuration lives in one ConfigMap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system get configmap coredns &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.data.Corefile}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;.:&lt;span class="m"&gt;53&lt;/span&gt; {
    &lt;span class="n"&gt;errors&lt;/span&gt;
    &lt;span class="n"&gt;health&lt;/span&gt; {
       &lt;span class="n"&gt;lameduck&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;
    }
    &lt;span class="n"&gt;ready&lt;/span&gt;
    &lt;span class="n"&gt;kubernetes&lt;/span&gt; &lt;span class="n"&gt;cluster&lt;/span&gt;.&lt;span class="n"&gt;local&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt;-&lt;span class="n"&gt;addr&lt;/span&gt;.&lt;span class="n"&gt;arpa&lt;/span&gt; &lt;span class="n"&gt;ip6&lt;/span&gt;.&lt;span class="n"&gt;arpa&lt;/span&gt; {
       &lt;span class="n"&gt;pods&lt;/span&gt; &lt;span class="n"&gt;insecure&lt;/span&gt;
       &lt;span class="n"&gt;fallthrough&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt;-&lt;span class="n"&gt;addr&lt;/span&gt;.&lt;span class="n"&gt;arpa&lt;/span&gt; &lt;span class="n"&gt;ip6&lt;/span&gt;.&lt;span class="n"&gt;arpa&lt;/span&gt;
       &lt;span class="n"&gt;ttl&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
    }
    &lt;span class="n"&gt;prometheus&lt;/span&gt; :&lt;span class="m"&gt;9153&lt;/span&gt;
    &lt;span class="n"&gt;forward&lt;/span&gt; . /&lt;span class="n"&gt;etc&lt;/span&gt;/&lt;span class="n"&gt;resolv&lt;/span&gt;.&lt;span class="n"&gt;conf&lt;/span&gt; {
       &lt;span class="n"&gt;max_concurrent&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;
    }
    &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt; {
        &lt;span class="n"&gt;disable&lt;/span&gt; &lt;span class="n"&gt;success&lt;/span&gt; &lt;span class="n"&gt;cluster&lt;/span&gt;.&lt;span class="n"&gt;local&lt;/span&gt;
        &lt;span class="n"&gt;disable&lt;/span&gt; &lt;span class="n"&gt;denial&lt;/span&gt; &lt;span class="n"&gt;cluster&lt;/span&gt;.&lt;span class="n"&gt;local&lt;/span&gt;
    }
    &lt;span class="n"&gt;loop&lt;/span&gt;
    &lt;span class="n"&gt;reload&lt;/span&gt;
    &lt;span class="n"&gt;loadbalance&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole DNS config for the cluster — about 20 lines.&lt;/p&gt;

&lt;p&gt;The first line, &lt;code&gt;.:53 { ... }&lt;/code&gt;, is a &lt;strong&gt;server block&lt;/strong&gt;: "listen on port 53 for queries in zone &lt;code&gt;.&lt;/code&gt;" — and &lt;code&gt;.&lt;/code&gt; is the DNS root, so every domain matches. Inside the braces is a list of &lt;strong&gt;plugins&lt;/strong&gt;. That's the core idea of CoreDNS: it's not a monolithic DNS server, it's a chain of small plugins, and each query passes through the relevant ones until one produces an answer.&lt;/p&gt;

&lt;p&gt;The two plugins doing the real work:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;kubernetes cluster.local in-addr.arpa ip6.arpa&lt;/code&gt;&lt;/strong&gt; — the plugin that answered the &lt;code&gt;kubernetes.default.svc.cluster.local&lt;/code&gt; query. It declares "I am authoritative for &lt;code&gt;cluster.local&lt;/code&gt;." It doesn't read zone files — it &lt;strong&gt;watches the Kubernetes API&lt;/strong&gt; for Services and EndpointSlices and synthesizes DNS answers from them in memory. Create a Service and it's resolvable within seconds, no config change ever. The sub-options: &lt;code&gt;ttl 30&lt;/code&gt; stamps answers valid for 30 seconds; &lt;code&gt;pods insecure&lt;/code&gt; enables legacy pod-IP names like &lt;code&gt;10-244-0-2.default.pod.cluster.local&lt;/code&gt;; &lt;code&gt;fallthrough&lt;/code&gt; lets reverse-DNS queries it can't answer continue to the next plugin instead of dying as NXDOMAIN.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;forward . /etc/resolv.conf&lt;/code&gt;&lt;/strong&gt; — the escape hatch for everything else. Queries that don't match &lt;code&gt;cluster.local&lt;/code&gt; (like &lt;code&gt;github.com&lt;/code&gt;) get forwarded to whatever nameservers are in the &lt;em&gt;CoreDNS pod's own&lt;/em&gt; &lt;code&gt;/etc/resolv.conf&lt;/code&gt; — which is the &lt;strong&gt;node's&lt;/strong&gt; resolver (in kind, Docker's DNS). Cluster names answered from the API watch, everything else proxied upstream.&lt;/p&gt;

&lt;p&gt;The supporting cast:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plugin&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;errors&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Log errors to stdout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;health&lt;/code&gt; / &lt;code&gt;ready&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;HTTP endpoints for liveness/readiness probes; &lt;code&gt;lameduck 5s&lt;/code&gt; keeps serving 5s during shutdown so in-flight queries finish&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;prometheus :9153&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The metrics endpoint from Concept 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cache 30&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cache upstream answers up to 30s — but note the &lt;code&gt;disable&lt;/code&gt; lines: caching is &lt;em&gt;off&lt;/em&gt; for &lt;code&gt;cluster.local&lt;/code&gt;, because the &lt;code&gt;kubernetes&lt;/code&gt; plugin's in-memory data is already instant and always fresh; caching it would only delay seeing Service changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;loop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Detect forwarding loops and crash on purpose (better than melting down) — a common cause of CrashLoopBackOff on hosts whose &lt;code&gt;/etc/resolv.conf&lt;/code&gt; points at &lt;code&gt;127.0.0.1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reload&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Watch this very ConfigMap and hot-reload on change — no pod restart needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;loadbalance&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shuffle the order of A records in answers — poor man's round-robin&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Concept 4: Reading the query log
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;log&lt;/code&gt; plugin makes CoreDNS print every query it receives — the single most useful debugging tool you have. Enable it by adding one word to the Corefile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system edit configmap coredns
&lt;span class="c"&gt;# add a line containing just:  log  inside the .:53 { } block&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thanks to the &lt;code&gt;reload&lt;/code&gt; plugin, the logs soon show the config being hot-swapped — no restart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[INFO] Reloading
[INFO] plugin/reload: Running configuration SHA512 = 2dd49c56...
[INFO] Reloading complete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now tail the logs (note &lt;code&gt;-l k8s-app=kube-dns&lt;/code&gt; grabs &lt;strong&gt;both&lt;/strong&gt; replicas — the Service load-balances queries between them, so tailing just one pod misses half the traffic):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system logs &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; k8s-app&lt;span class="o"&gt;=&lt;/span&gt;kube-dns
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...and in another terminal, generate some traffic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl run dnsgen &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;busybox:1.36 &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Never &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'nslookup kubernetes; nslookup example.com; nslookup example.com'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what came back, and it contains everything from Concepts 1–3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[INFO] 10.244.0.9:55719 - 60987 "A IN kubernetes.cluster.local. udp 42 false 512" NXDOMAIN qr,aa,rd 135 0.000981587s
[INFO] 10.244.0.9:55719 - 61125 "A IN kubernetes.svc.cluster.local. udp 46 false 512" NXDOMAIN qr,aa,rd 139 0.000780419s
[INFO] 10.244.0.9:55719 - 65472 "A IN kubernetes.default.svc.cluster.local. udp 54 false 512" NOERROR qr,aa,rd 106 0.001500255s
[INFO] 10.244.0.9:34990 - 21746 "A IN example.com. udp 29 false 512" NOERROR qr,rd,ra 83 0.027218625s
[INFO] 10.244.0.9:55840 - 22203 "A IN example.com. udp 29 false 512" NOERROR qr,aa,rd,ra 83 0.000005625s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anatomy of a line, reading left to right: &lt;code&gt;10.244.0.9:55719&lt;/code&gt; is the client pod and port, &lt;code&gt;60987&lt;/code&gt; the query ID, &lt;code&gt;A&lt;/code&gt; the record type (A = IPv4, AAAA = IPv6), &lt;code&gt;kubernetes.cluster.local.&lt;/code&gt; the queried name, &lt;code&gt;udp&lt;/code&gt; the transport (TCP is the fallback for large answers), &lt;code&gt;NXDOMAIN&lt;/code&gt; the result code, &lt;code&gt;qr,aa,rd&lt;/code&gt; the DNS flags, &lt;code&gt;135&lt;/code&gt; the answer size in bytes, and the trailing number the duration.&lt;/p&gt;

&lt;p&gt;Three stories hide in those five lines:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The search walk, server-side (lines 1–3).&lt;/strong&gt; The same &lt;code&gt;nslookup kubernetes&lt;/code&gt; from Concept 2, seen from CoreDNS's perspective: two NXDOMAINs, then NOERROR. Note the &lt;code&gt;aa&lt;/code&gt; flag on all three — &lt;em&gt;authoritative answer&lt;/em&gt;. The &lt;code&gt;kubernetes&lt;/code&gt; plugin owns &lt;code&gt;cluster.local&lt;/code&gt; and answers from its API-watch data. That's also why they're fast: about 1 millisecond.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A forwarded query (line 4).&lt;/strong&gt; First &lt;code&gt;example.com&lt;/code&gt; lookup: &lt;strong&gt;no &lt;code&gt;aa&lt;/code&gt; flag&lt;/strong&gt;, and &lt;code&gt;ra&lt;/code&gt; (recursion available) instead — not answered locally, it went through &lt;code&gt;forward&lt;/code&gt; to the upstream resolver. Cost: &lt;strong&gt;27 milliseconds&lt;/strong&gt;, ~27× slower than a cluster answer, because it left the building.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cache earning its keep (line 5).&lt;/strong&gt; Second &lt;code&gt;example.com&lt;/code&gt; lookup, seconds later: same answer, duration &lt;strong&gt;15 microseconds&lt;/strong&gt; — the &lt;code&gt;cache 30&lt;/code&gt; plugin serving from memory, roughly 1,800× faster than the forwarded query.&lt;/p&gt;

&lt;p&gt;One grep shows you who asked, what they asked, which plugin answered (the &lt;code&gt;aa&lt;/code&gt; flag is the tell), and what it cost.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Production note:&lt;/strong&gt; &lt;code&gt;log&lt;/code&gt; prints &lt;em&gt;every query in the cluster&lt;/em&gt; — fine for a lab, noisy and disk-hungry at scale. Turn it on for debugging, off after.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Concept 5: Service discovery — normal vs headless
&lt;/h2&gt;

&lt;p&gt;Time to close the loop on the &lt;code&gt;kubernetes&lt;/code&gt; plugin: create workloads and watch DNS records appear out of thin air.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create deployment web &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nginx:1.27 &lt;span class="nt"&gt;--replicas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3
kubectl expose deployment web &lt;span class="nt"&gt;--port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;80
kubectl run dnstest &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;busybox:1.36 &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Never &lt;span class="nt"&gt;--&lt;/span&gt; nslookup web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Name:    web.default.svc.cluster.local
Address: 10.96.24.66
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero DNS config written, yet it resolves. Two things to notice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The answer is &lt;strong&gt;one A record — the Service's ClusterIP&lt;/strong&gt;, not any of the 3 pod IPs. Load-balancing to pods happens later, in the network layer (kube-proxy), invisible to DNS.&lt;/li&gt;
&lt;li&gt;In the query log, this answer carries the &lt;code&gt;aa&lt;/code&gt; flag — the &lt;code&gt;kubernetes&lt;/code&gt; plugin saw the Service appear via its API watch and started answering instantly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now the interesting variant. Same pods, second Service, one crucial difference — &lt;code&gt;clusterIP: None&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create service clusterip web-headless &lt;span class="nt"&gt;--clusterip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;None &lt;span class="nt"&gt;--tcp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;80
kubectl &lt;span class="nb"&gt;set &lt;/span&gt;selector service web-headless &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;web
kubectl scale deployment web &lt;span class="nt"&gt;--replicas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5
kubectl run dnstest &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;busybox:1.36 &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Never &lt;span class="nt"&gt;--&lt;/span&gt; nslookup web-headless
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Name:    web-headless.default.svc.cluster.local
Address: 10.244.0.20
Name:    web-headless.default.svc.cluster.local
Address: 10.244.0.19
Name:    web-headless.default.svc.cluster.local
Address: 10.244.0.12
Name:    web-headless.default.svc.cluster.local
Address: 10.244.0.18
Name:    web-headless.default.svc.cluster.local
Address: 10.244.0.17
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No ClusterIP exists, so DNS can't hand out a single VIP — instead you get &lt;strong&gt;one A record per pod, the actual pod IPs&lt;/strong&gt;. The answer is a &lt;em&gt;live view&lt;/em&gt; of the EndpointSlice: scale the Deployment and the record set follows within seconds. The client sees every backend and picks one itself — this is how client-side load balancers (gRPC and friends) discover all endpoints, and the foundation of how StatefulSets give each database replica an addressable name.&lt;/p&gt;

&lt;p&gt;Two closing details:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-pod names.&lt;/strong&gt; With a headless Service, each backend also gets its own DNS name — the pod IP with dashes, prefixed to the service name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nslookup 10-244-0-20.web-headless.default.svc.cluster.local   &lt;span class="c"&gt;# → 10.244.0.20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;StatefulSets improve on this: their pods get &lt;em&gt;stable&lt;/em&gt; names like &lt;code&gt;db-0.web-headless.default.svc.cluster.local&lt;/code&gt; instead of IP-based ones — that's the actual mechanism behind "connect to the primary at &lt;code&gt;db-0&lt;/code&gt;".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The TTL.&lt;/strong&gt; Busybox's &lt;code&gt;nslookup&lt;/code&gt; hides it, but every answer carried &lt;code&gt;ttl 30&lt;/code&gt; from the Corefile — a client may cache the pod list for up to 30 seconds. That's the staleness window: scale down, and a client that cached the answer can keep trying a dead pod IP for up to 30s. It's a tradeoff between DNS load and freshness — now you know where the knob is.&lt;/p&gt;




&lt;h2&gt;
  
  
  Concept 6: Breaking DNS on purpose
&lt;/h2&gt;

&lt;p&gt;The skill you'll actually use at 2am. Two failures, two very different presentations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure A — CoreDNS is down (total outage)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system scale deployment coredns &lt;span class="nt"&gt;--replicas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
kubectl run dnstest &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;busybox:1.36 &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Never &lt;span class="nt"&gt;--&lt;/span&gt; nslookup web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;;; connection timed out; no servers could be reached
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not an NXDOMAIN, not an error answer — a &lt;strong&gt;hang, then timeout&lt;/strong&gt;. Nothing is listening behind &lt;code&gt;10.96.0.10&lt;/code&gt;. Note how &lt;em&gt;slow&lt;/em&gt; failure is compared to a crisp NXDOMAIN — this is why DNS outages make everything look "slow" before anyone realizes it's DNS. Also: &lt;code&gt;kubectl&lt;/code&gt; itself keeps working (it doesn't use cluster DNS), and running pods with established connections keep working — only &lt;em&gt;new&lt;/em&gt; name resolutions die.&lt;/p&gt;

&lt;p&gt;Restore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system scale deployment coredns &lt;span class="nt"&gt;--replicas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Failure B — CoreDNS is up but misconfigured (the subtle one)
&lt;/h3&gt;

&lt;p&gt;Edit the ConfigMap and change &lt;code&gt;forward . /etc/resolv.conf&lt;/code&gt; to &lt;code&gt;forward . 10.99.99.99&lt;/code&gt; (a black-hole IP). Wait for the reload, then test one cluster name and one external name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl run dnstest &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;busybox:1.36 &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Never &lt;span class="nt"&gt;--&lt;/span&gt; nslookup web
&lt;span class="c"&gt;# Name: web.default.svc.cluster.local  Address: 10.96.24.66     ← WORKS&lt;/span&gt;

kubectl run dnstest &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;busybox:1.36 &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Never &lt;span class="nt"&gt;--&lt;/span&gt; nslookup example.com
&lt;span class="c"&gt;# ;; connection timed out; no servers could be reached          ← TIMES OUT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That asymmetry &lt;strong&gt;is&lt;/strong&gt; the diagnosis. Memorize the pattern:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Cluster names resolve, external names don't → CoreDNS itself is fine; the problem is upstream/forwarding.&lt;/strong&gt; The &lt;code&gt;kubernetes&lt;/code&gt; plugin answers &lt;code&gt;cluster.local&lt;/code&gt; from memory — the broken &lt;code&gt;forward&lt;/code&gt; never gets involved. &lt;strong&gt;Both fail → the problem is CoreDNS itself or the network path to it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two lookups, and you've localized the fault.&lt;/p&gt;

&lt;p&gt;One more lesson hides in the recovery: after fixing the ConfigMap back, the &lt;em&gt;first&lt;/em&gt; retry still timed out; the second succeeded. That's config propagation delay — the kubelet syncs ConfigMap volumes into pods periodically (up to ~1 minute), and the &lt;code&gt;reload&lt;/code&gt; plugin checks for changes every ~30s on top of that. &lt;strong&gt;A Corefile fix can take up to ~90 seconds to bite.&lt;/strong&gt; Knowing that saves you from "my fix didn't work!" panic-reverts.&lt;/p&gt;




&lt;h2&gt;
  
  
  The DNS debugging checklist
&lt;/h2&gt;

&lt;p&gt;Covers ~95% of real incidents:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Characterize from inside a pod — one cluster name, one external name:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl run dbg &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;busybox:1.36 &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Never &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'nslookup kubernetes.default; nslookup example.com'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both fail → step 2. Only external fails → check the &lt;code&gt;forward&lt;/code&gt; target and the upstream resolver. One &lt;em&gt;specific&lt;/em&gt; service fails → step 4.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Is CoreDNS alive?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system get pods &lt;span class="nt"&gt;-l&lt;/span&gt; k8s-app&lt;span class="o"&gt;=&lt;/span&gt;kube-dns
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CrashLoopBackOff or frequent restarts? Classic cause: the &lt;code&gt;loop&lt;/code&gt; plugin detecting a forwarding loop and killing the process on purpose — common on hosts whose &lt;code&gt;/etc/resolv.conf&lt;/code&gt; points to &lt;code&gt;127.0.0.1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Read the logs:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system logs &lt;span class="nt"&gt;-l&lt;/span&gt; k8s-app&lt;span class="o"&gt;=&lt;/span&gt;kube-dns &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload errors mean a broken Corefile. With &lt;code&gt;log&lt;/code&gt; enabled you can see whether queries even arrive, and their rcodes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. For one failing service — does it have endpoints?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get svc web
kubectl get endpointslices &lt;span class="nt"&gt;-l&lt;/span&gt; kubernetes.io/service-name&lt;span class="o"&gt;=&lt;/span&gt;web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A selector typo gives you a Service with zero endpoints. Gotcha: the &lt;em&gt;normal&lt;/em&gt; Service name still resolves (DNS hands out the ClusterIP regardless — connections just go nowhere), but a &lt;em&gt;headless&lt;/em&gt; Service with no endpoints returns NXDOMAIN. Same root cause, two different symptoms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Bypass the Service layer&lt;/strong&gt; — query a CoreDNS pod IP directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system get pods &lt;span class="nt"&gt;-l&lt;/span&gt; k8s-app&lt;span class="o"&gt;=&lt;/span&gt;kube-dns &lt;span class="nt"&gt;-o&lt;/span&gt; wide   &lt;span class="c"&gt;# grab a pod IP&lt;/span&gt;
kubectl run dbg &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;busybox:1.36 &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Never &lt;span class="nt"&gt;--&lt;/span&gt; nslookup web 10.244.0.X
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Works against the pod IP but not against &lt;code&gt;10.96.0.10&lt;/code&gt; → CoreDNS is innocent; your problem is kube-proxy / the Service VIP plumbing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cleanup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl delete deployment web
kubectl delete service web web-headless
&lt;span class="c"&gt;# and remove the `log` line from the coredns ConfigMap, or just:&lt;/span&gt;
kind delete cluster
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compare distros:&lt;/strong&gt; if you have a k3s/k3d cluster, diff its Corefile against kind's (&lt;code&gt;kubectl -n kube-system get cm coredns -o yaml&lt;/code&gt; on each) and look up k3s's &lt;code&gt;coredns-custom&lt;/code&gt; ConfigMap pattern — k3s's manifest reconciler reverts direct Corefile edits, so customization goes through dedicated hooks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customization:&lt;/strong&gt; add a stub domain (forward &lt;code&gt;corp.example.com&lt;/code&gt; to an internal resolver), &lt;code&gt;rewrite&lt;/code&gt; rules, or static records via the &lt;code&gt;hosts&lt;/code&gt; plugin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NodeLocal DNSCache:&lt;/strong&gt; a per-node caching DNS agent that intercepts pod queries before they cross the network — the standard fix at scale for the &lt;code&gt;ndots:5&lt;/code&gt; query-storm problem you saw in Concept 2.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reference:&lt;/strong&gt; the &lt;a href="https://coredns.io/plugins/" rel="noopener noreferrer"&gt;CoreDNS plugin list&lt;/a&gt; — the Corefile is small precisely because each plugin does one thing; skimming the list tells you what else the chain can do.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Tested on kind with Kubernetes v1.36.1 / CoreDNS v1.14.2.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>dns</category>
      <category>coredns</category>
      <category>networking</category>
    </item>
  </channel>
</rss>
