Module 08 — DNS in the Cloud & in Kubernetes
Updated 20 August 2026
Seven modules of DNS as a protocol. This is DNS as a DevOps engineer actually meets it: Route 53 alias records and routing policies, private hosted zones and split-horizon, then CoreDNS — where Module 06's ndots arithmetic becomes the notorious five-second Kubernetes timeout.
🧠 concept → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (answers hidden)
Prerequisite: Modules 01–06. You need CNAME-at-apex (02 C3), delegation (03), TTLs and caching (03 B), the ndots rule (06 B3), and getent versus dig (06 A).
Part A · Managed DNS: Route 53 as the worked example
A1 · Hosted zones, and what the provider is really selling
The floors end up equally clean. What you actually bought was not owning the equipment, the rota, or the cover when someone is off sick.
A hosted zone is the same trade: the records are identical, and what you have handed over is the servers, the copies, and the version numbers — along with the ability to inspect any of it.
A hosted zone is a zone file with an API in front of it. Everything from Module 02 still applies — RRsets, TTLs, the CNAME rules — and Module 05's operational burden is what you are paying to avoid: no named.conf, no serial numbers, no zone transfers, no secondaries to keep in sync.
And Module 05 explains what you gave up. Route 53's SOA serial is a constant 1 because it replicates internally rather than by AXFR. There are no secondaries to fall out of sync, so the entire class of failure in Module 05 C3 simply cannot happen — and neither can you inspect or control the replication.
A2 · Alias records — the apex problem, solved outside the protocol
From where you are standing nothing unusual happened — you asked a question and got a normal answer. The looking-up happened behind the counter.
Which explains the two catches: you cannot tell from outside that it happened, and if you change receptionist, the new one has to be taught all the same lookups by hand.
Module 02 C3 established the rule: a CNAME cannot sit at a zone apex, because the apex must carry SOA and NS. Every provider sells a way around it.
| Provider | Name for it |
|---|---|
| Route 53 | alias record |
| Cloudflare | CNAME flattening |
| DNSimple, easyDNS | ALIAS |
| DNS Made Easy and others | ANAME |
Three things follow:
- You cannot see an alias record from outside. dig shows a plain A. The only way to know is the provider's console or API — which is why an inherited estate hides them
- They do not survive a provider migration. Every one must be re-created by hand, and forgetting one is an apex outage
- Resolution happens from the provider's network position, not your users'. For a geo-distributed target that can hand users an address chosen for the wrong region
A3 · Routing policies — where DNS stops being a lookup
Usually sensible. But you might be calling from a work mobile registered in another city, or through a company switchboard hundreds of miles away — and the system will confidently send you to the wrong regional team.
DNS routing sees the resolver, not the user, and that is the same mistake with the same confident tone.
| Policy | What decides the answer |
|---|---|
| Simple | Nothing. One RRset, returned to everyone |
| Weighted | A weight per record — canary and blue/green deployments |
| Failover | A health check. Primary while healthy, secondary otherwise |
| Latency | Measured latency from the querying resolver to each AWS region |
| Geolocation / Geoproximity | Where the querying resolver appears to be |
| Multivalue answer | Up to eight health-checked records, returned together |
A user in Kuala Lumpur whose ISP uses a resolver in Singapore is, to Route 53, in Singapore. A user on 8.8.8.8 is wherever Google's nearest instance happens to be. EDNS Client Subnet partially fixes this by passing a truncated client prefix upstream, but it is optional, privacy-sensitive, and not universally supported.
And the second limitation is the one that catches teams during incidents: caching. Module 03 B established that a resolver holds an answer for the TTL and that TTL floors and application caches can extend it further. So DNS failover is not fast failover. It moves traffic eventually. Anything needing sub-second recovery must be handled by an anycast address, a load balancer, or the client — not by a DNS record change.
A4 · Private hosted zones and split-horizon
Same building, same question — "where do I go?" — and two completely different, equally correct answers depending on which door you are standing at.
Which is why "it works for me" proves nothing here. Before you believe any test result, ask which door the person was standing at.
Split-horizon DNS means serving different answers for the same name depending on who is asking. Internally api.example.com resolves to 10.0.4.12; externally it resolves to a public load balancer, or to nothing at all.
Implementations: private hosted zones in Route 53, attached to specific VPCs; views in BIND, selected by client address; separate resolvers entirely in a corporate network.
The failure that costs a whole afternoon: a name exists only in the private zone, someone tests from a laptop off the VPN, gets NXDOMAIN, and concludes the record was never created. Always ask from where a test was run, and reproduce from a host inside the same view.
And the leak to look for on an audit — Module 02 D5 found exactly this on seamless.se: corp.seamless.se publishing 10.0.48.20, an RFC 1918 address, in the public zone. That is a private record that escaped its horizon.
🎯 Interview questions — Managed DNS
Q. How do you point a root domain at a load balancer?
Not with a CNAME — the apex must hold SOA and NS, and a CNAME excludes all other types at the same name. In Route 53 you use an alias record; other providers call it ALIAS, ANAME or CNAME flattening.
The provider resolves the target internally and returns a genuine A or AAAA, so nothing non-standard goes on the wire.
Two consequences worth volunteering. These records are invisible from outside — dig shows a plain A — so on an inherited estate the only way to find them is the provider's API, and they must be re-created by hand during any provider migration. And the flattening happens from the provider's network position, which for a geo-distributed target can hand your users a badly chosen address.
Q. Can you use DNS for failover?
Yes, and it works — but not quickly, and the distinction matters more than the mechanism. Route 53 failover routing swaps the answer when a health check fails; weighted and multivalue policies do related jobs.
The limit is caching. Resolvers hold the old answer for the TTL, resolver-side min-cache-ttl floors can extend that, serve-stale can extend it further, and application-level caches ignore TTLs entirely — a JVM caches for the process lifetime by default.
So DNS moves traffic eventually, on the order of the TTL and often longer. Anything needing sub-second recovery belongs in an anycast address, a load balancer, or client-side retry logic. Designing a failover whose correctness depends on clients honouring a 30-second TTL is the mistake this question is testing for.
Q. What is split-horizon DNS and when would you use it?
Serving different answers for the same name depending on who asks — internal clients get private addresses, external clients get public ones or nothing. Route 53 private hosted zones scoped to a VPC, BIND views, or separate internal resolvers.
It is the right way to keep internal names off the public internet, because the names are absent from the public zone rather than merely obscured. NSEC3 in a signed public zone only raises the cost of enumeration; split-horizon removes the data.
The operational cost is the part worth naming: it makes "it works for me" meaningless. Every DNS test result now depends on where it was run, so a name that resolves inside a VPC and NXDOMAINs from a laptop is not a broken record — it is the design working. Reproducing from inside the correct view is the first step in any such investigation.
Part B · DNS inside Kubernetes
B1 · What a cluster gives every pod
You never fill in a form. The badge appears, and it follows a strict pattern: name, floor, building.
The one distinction to hold on to: some badges point at a department — always the same, someone will deal with you. Others point at a specific person, and that badge stops meaning anything the moment they leave.
Every Service in a cluster gets a DNS name, generated automatically, in a fixed shape:
<service>.<namespace>.svc.cluster.local
│ │ │ │
│ │ │ └── the cluster domain, configurable
│ │ └───────── always "svc" for Services
│ └──────────────── the namespace
└─────────────────────────── the Service name| Object | What DNS returns |
|---|---|
| ClusterIP Service | One A record: the virtual service IP. Not a pod address |
| Headless Service (clusterIP: None) | One A record per ready pod. The set changes as pods come and go |
| Named ports | SRV records at _port._proto.<service>.<ns>.svc.cluster.local — Module 02 B8, generated for you |
| ExternalName Service | A CNAME to a name outside the cluster |
| StatefulSet pods | Stable per-pod names: <pod>.<service>.<ns>.svc.cluster.local |
A ClusterIP name resolves to one stable virtual address, and kube-proxy load-balances behind it. DNS returns one answer, it rarely changes, and caching is harmless.
A headless name resolves to the actual pod addresses — so the RRset changes on every scale event, every rollout and every crash. That is what clients need when they must address individual instances: databases with a primary, Kafka brokers, anything doing its own partitioning.
And it inherits every caching problem in this track. A client that caches a headless lookup keeps talking to pods that no longer exist. This is where Module 03 B's TTL discussion and Module 06's application-cache warning stop being theory — Kubernetes serves a 30-second TTL by default, and a JVM ignoring it will hold dead pod addresses indefinitely.
B2 · The pod's resolv.conf, and where five seconds comes from
You dial a supplier. The phone quietly tries supplier-at-this-floor, supplier-at-this-building, supplier-at-head-office — gets "no such extension" three times — and only then dials the real number.
It works. It is four calls instead of one. And on the rare occasion one of those attempts goes astray, the phone sits silent for five seconds before trying again — which is exactly the delay everyone blames on the network.
Every pod gets a resolv.conf injected by the kubelet:
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5ndots:5 means any name with fewer than five dots is treated as relative and run through the search list first. api.example.com has two dots. www.google.com has two dots. Almost every external name you will ever query has fewer than five dots.
So a pod resolving api.example.com sends, in order:
1. api.example.com.default.svc.cluster.local -> NXDOMAIN
2. api.example.com.svc.cluster.local -> NXDOMAIN
3. api.example.com.cluster.local -> NXDOMAIN
4. api.example.com -> the answerFour queries instead of one — and because most clients query A and AAAA in parallel, eight packets instead of two. Every external lookup, from every pod, forever.
Why five seconds? Because resolv.conf's default timeout is 5. If any one of those packets is lost — and there is a well-known conntrack race in the Linux kernel that drops parallel UDP DNS packets — the client waits the full five-second timeout before retrying. That is the "five-second DNS delay in Kubernetes" every team eventually meets, and it is not a CoreDNS fault: it is ndots:5 multiplying the query count until a rare packet-loss race becomes routine.
| Fix | Effect |
|---|---|
| Trailing dot on external names — api.example.com. | Absolute, so ndots never applies. One query. Costs nothing, fixes it completely |
| dnsConfig with ndots: "2" per pod | Cluster names still work; external names skip the search list |
| NodeLocal DNSCache | A per-node caching resolver, talking to CoreDNS over TCP. Removes the UDP race and most of the latency |
| single-request-reopen in dnsConfig | Separates the A and AAAA queries, working around the conntrack race |
🧪 Exercise B2.1 — Count the queries a pod actually sends
kubectl run dnslab --rm -it --image=nicolaka/netshoot -- bash
# inside the pod:
cat /etc/resolv.conf
for n in api.example.com. api.example.com; do
printf '%-22s ' "$n"
dig +search "$n" +noall +stats | awk '/Query time/{print $4" ms"}'
done
getent hosts kubernetes.default.svc.cluster.local✅ Expected result — click to reveal
$ cat /etc/resolv.conf
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
api.example.com. 4 ms <- trailing dot: ONE query
api.example.com 31 ms <- no dot: four queries, three of them wasted
$ getent hosts kubernetes.default.svc.cluster.local
10.96.0.1 kubernetes.default.svc.cluster.localRun this in a real cluster — the exact numbers vary, the ratio does not.
One character changed the latency by roughly 8×, and it is the same trailing dot from Module 01 A3. Three NXDOMAIN round trips to CoreDNS were eliminated by making the name absolute.
Multiply it out. A service doing 1,000 external lookups a second sends 4,000 queries per second to CoreDNS instead of 1,000 — and 3,000 of them exist only to be told NXDOMAIN. At cluster scale this is the largest single source of CoreDNS load, and the reason teams scale CoreDNS up when the real problem is a search list.
It also ties three earlier modules together. Module 06 B3 gave you the ndots rule. Module 03 B2 explained that those NXDOMAINs are negatively cached, which is the only reason this is survivable at all. And Module 01 A3 gave you the fix.
B3 · CoreDNS and the Corefile
Internal extensions — handle here. Everything else — pass to the outside line. Keep a note of recent numbers. And if the outside line turns out to be our own switchboard, stop immediately rather than looping forever.
That last rule is not paranoia. It is the single most-searched CoreDNS error, and it happens when the "outside line" was accidentally configured as the switchboard's own extension.
CoreDNS is the cluster's resolver. Its configuration is a Corefile in a ConfigMap — a plugin chain rather than a config file in the BIND sense.
.:53 {
errors
health { lameduck 5s }
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf { max_concurrent 1000 }
cache 30
loop
reload
loadbalance
}| Plugin | Job |
|---|---|
| kubernetes | Answers *.cluster.local from the Kubernetes API. ttl 30 is the 30 seconds from B1 |
| forward | Everything else goes upstream, to the node's /etc/resolv.conf. Module 03 C3's forwarding |
| cache 30 | CoreDNS's own cache — Module 03 B, inside the cluster |
| loop | Detects a forwarding loop at startup and refuses to run. See below |
| reload | Picks up ConfigMap changes with no restart |
| loadbalance | Rotates A records in responses — the round-robin from Module 01 C3 |
loop detects it and exits with Loop ... detected for zone ".", which is far kinder than silent infinite recursion.
The fix is to give the kubelet the file with the real upstream servers — --resolv-conf=/run/systemd/resolve/resolv.conf — rather than the stub file. It is Module 06 C1's "127.0.0.53 does not tell you where queries go" causing a cluster-wide outage two layers away.
B4 · NodeLocal DNSCache
Most trips stop happening entirely. The ones that remain go by a more reliable route.
But be honest about the cause first: if people are walking to the shop four times for every one drink they need, a fridge hides that rather than fixing it.
It fixes three things at once, and each is something you have already met:
- Cache hits never leave the node — the ndots query amplification from B2 stops being network traffic
- It talks to CoreDNS over TCP, sidestepping the conntrack UDP race that causes the five-second delays
- It removes conntrack entries for DNS, which on a busy node is a real resource saving
It is the standard answer to "our cluster has DNS latency" — but know why it works. If the real cause is ndots:5 on a service making millions of external calls, a trailing dot fixes it for free and NodeLocal is treating the symptom.
🎯 Interview questions — Kubernetes DNS
Q. Why do DNS lookups in Kubernetes sometimes take five seconds?
Two things combine. Pods get options ndots:5, so any name with fewer than five dots — essentially every external name — goes through the search list first: three NXDOMAIN queries before the real one, and since most clients send A and AAAA in parallel, eight packets instead of two.
Then a known conntrack race in the kernel can drop one of those parallel UDP packets. The client cannot detect loss, so it waits out the default 5-second resolv.conf timeout before retrying.
The framing that shows real understanding: it is not a CoreDNS fault. ndots:5 multiplies the query count until a rare race becomes routine. The cheapest fix is a trailing dot on external names, making them absolute so the search list is skipped; then ndots:2 via dnsConfig; then NodeLocal DNSCache, which also moves the traffic to TCP and removes the race entirely.
Q. ClusterIP versus headless Service, in DNS terms?
A ClusterIP Service resolves to one stable virtual address, with kube-proxy load-balancing behind it. A headless Service — clusterIP: None — resolves to the actual addresses of the ready pods, one A record each. StatefulSets add stable per-pod names on top.
Headless is what you need when clients must address individual instances: a database with a primary, Kafka brokers, anything doing its own sharding.
The operational catch: a headless RRset changes on every scale event and rollout, so it inherits every caching problem in DNS. Kubernetes serves a 30-second TTL, but a client that ignores TTLs — a JVM with default settings, or a connection pool — keeps addressing pods that no longer exist. With ClusterIP that cannot happen, because the address is stable and the balancing is elsewhere.
Q. CoreDNS pods are crash-looping with "loop detected". What is wrong?
CoreDNS forwards unresolved queries to the node's /etc/resolv.conf. If that file points at a local stub listener — 127.0.0.53 from systemd-resolved, or 127.0.0.1 — then inside the CoreDNS pod that address is the pod itself, so CoreDNS forwards to itself. The loop plugin detects this at startup and exits deliberately.
Fix: point the kubelet at the file holding the real upstream nameservers, --resolv-conf=/run/systemd/resolve/resolv.conf, or set explicit forwarders in the Corefile.
What makes this a good answer rather than a recalled one: it is the same misunderstanding as "why does my resolv.conf say 127.0.0.53" — a stub address naming a local process rather than a server — surfacing two layers away as a cluster outage. Naming that connection shows you understand the system, not just the error string.
Part C · Field recipes
C1 · Diagnosing DNS from inside a pod
There is never one in the cupboard. And taking the machine apart to install tools while it is running is not a repair strategy — you bring a second, properly equipped person alongside it instead.
# 1. a debug pod - never install tools into an application container
kubectl run dnslab --rm -it --image=nicolaka/netshoot -- bash
# 2. what was injected?
cat /etc/resolv.conf
# 3. does cluster DNS work at all?
dig +short kubernetes.default.svc.cluster.local
# 4. does EXTERNAL resolution work, and how many queries does it cost?
dig +short example.com. # absolute - one query
dig +search +short example.com # relative - the whole search list
# 5. is CoreDNS itself healthy?
kubectl -n kube-system get pods -l k8s-app=kube-dns
kubectl -n kube-system logs -l k8s-app=kube-dns --tail=50 | grep -iE 'error|loop|SERVFAIL'
# 6. what is CoreDNS actually configured with?
kubectl -n kube-system get configmap coredns -o jsonpath='{.data.Corefile}'Steps 3 and 4 split the problem in half. Cluster names failing but external names working means the kubernetes plugin or the API connection. External failing but cluster names working means forward, the node's upstream resolvers, or egress filtering. Both failing means CoreDNS itself.
C2 · The cloud DNS review checklist
Nobody enjoys it, it takes twenty minutes, and it is the only chance you get to spot the things you would otherwise be blamed for later.
| Check | Why |
|---|---|
| Does any public zone contain RFC 1918 addresses? | A private record that escaped its horizon — exactly what Module 02 D5 found on seamless.se |
| List every alias record via the API | Invisible to dig, and every one must be hand-re-created on a provider migration |
| Are health checks attached to failover records, and do they actually fail? | An unexercised health check is decoration. Test it deliberately |
| What TTLs are on records used for failover? | A 3600-second TTL makes a health check pointless. And clients may ignore it anyway |
| Are private hosted zones attached to all the VPCs that need them? | The classic "resolves in staging, NXDOMAIN in prod" cause |
| Does the pod resolv.conf still carry ndots:5? | Four-fold query amplification on every external lookup, cluster-wide |
| Is CoreDNS forwarding to a real upstream, not a stub address? | The loop crash, and the reason kubelet needs --resolv-conf pointed at the right file |
| Is NodeLocal DNSCache deployed on clusters with heavy external traffic? | Removes the conntrack UDP race that causes five-second stalls |
Part D · Putting it together
D1 · How this all fits — the complete picture
Diagram source
flowchart TD
POD["pod<br>getaddrinfo api.example.com"] --> RC["/etc/resolv.conf<br>ndots:5<br>3 search suffixes"]
RC --> AMP["4 QUERIES not 1<br>x2 for A and AAAA"]
AMP --> CD["CoreDNS<br>ClusterIP 10.96.0.10"]
CD --> K{"ends in<br>cluster.local?"}
K -->|"yes"| KP["kubernetes plugin<br>reads the API<br>ClusterIP: 1 record<br>headless: 1 per pod<br>ttl 30"]
K -->|"no"| FW["forward plugin<br>-> node resolv.conf<br>MUST NOT be a stub<br>or loop kills the pod"]
FW --> UP["upstream resolver<br>-> the public DNS<br>of Modules 01-04"]
FW -.->|"in a VPC"| R53["Route 53<br>private hosted zone<br>SPLIT HORIZON"]
R53 -.->|"public view"| PUB["public hosted zone<br>alias records<br>routing policies"]
NLC["NodeLocal DNSCache<br>per-node, over TCP"] -.->|"absorbs the amplification<br>removes the UDP race"| CD
style AMP fill:#ef4444,color:#fff
style FW fill:#f59e0b,color:#fff
style NLC fill:#22c55e,color:#fffFour ideas, and everything in this module follows.
- Alias records are provider features, not DNS. Invisible from outside, non-portable, resolved from the provider's location.
- DNS-based routing decides on the resolver's address and moves traffic at the pace of a TTL. It is not fast failover.
- ndots:5 multiplies every external lookup by four, and a trailing dot undoes it for free.
- CoreDNS forwarding to a stub address is a loop. Module 06's 127.0.0.53 confusion, two layers up.
D2 · Production practice
| Habit | Why |
|---|---|
| Write external names with a trailing dot in cluster configuration | Skips the search list entirely: one query instead of four, at zero cost |
| Export the zone from the provider's API, not from dig | Alias records and private zones are invisible on the wire. Module 02 D3 said the API is the only complete source |
| Never rely on DNS for sub-second failover | TTLs, resolver floors, serve-stale and application caches all outlive your health check |
| Keep RFC 1918 addresses out of public zones | They leak internal topology and are useless externally. Put them in a private zone |
| Point kubelet's --resolv-conf at the real upstream file | Forwarding to 127.0.0.53 makes CoreDNS forward to itself; loop then kills the pod |
| Prefer ClusterIP unless clients genuinely need individual pods | Headless RRsets change constantly and clients that ignore TTLs will address dead pods |
| Deploy NodeLocal DNSCache on clusters with heavy external traffic | Absorbs the query amplification and moves CoreDNS traffic to TCP, removing the conntrack race |
| Keep a debug image with dig, getent, curl and ss | Application images have no tools, and installing into a running pod is not a plan |
| Ask "from where?" before believing any DNS test result | With split-horizon in play, the answer depends on the asker's network position |
| Record which routing policy each record uses, in code | Weighted and latency policies are invisible in dig output and easy to lose in a migration |
D3 · Capstone exercise
Brief. A service in a Kubernetes cluster intermittently takes five seconds to reach an external API. Produce a diagnosis and a remediation plan answering all seven:
- From inside a pod, show the configuration responsible and explain the arithmetic — how many queries does one external lookup actually cost?
- Give the zero-cost fix, and say which module first taught it.
- Give two further fixes and say what each one addresses that the first does not.
- The team wants to "scale up CoreDNS". Explain why that may not help.
- The same name resolves in staging and NXDOMAINs in production. Give the two most likely causes and the command that distinguishes them.
- You are asked to point the apex at a load balancer with 30-second failover. Say what you would build and what you would refuse to promise.
- An audit finds 10.0.48.20 published in a public zone. Explain the finding and the fix.
✅ Model answer — attempt it first, then click
1. cat /etc/resolv.conf shows options ndots:5 and a three-entry search list. api.example.com has two dots — fewer than five — so it is treated as relative: three suffixed queries returning NXDOMAIN, then the real one. Four queries, and eight packets once A and AAAA are sent in parallel. The five seconds is the default timeout:5 being waited out when the conntrack race drops one of those parallel UDP packets.
2. A trailing dot: api.example.com. It makes the name absolute, so ndots never applies and the search list is skipped. Module 01 A3 taught it, in the very first module, as "the trailing dot means send this name as written".
3. dnsConfig with ndots: "2" — fixes the amplification cluster-wide rather than name-by-name, so it covers third-party code you cannot edit. NodeLocal DNSCache — addresses something different: it moves the CoreDNS conversation to TCP, which removes the UDP conntrack race itself rather than merely reducing how often it is hit.
4. Because CoreDNS is probably not the bottleneck. The load is self-inflicted: 75% of the queries exist only to receive NXDOMAIN. Scaling CoreDNS adds capacity to serve queries that should never have been sent, and does nothing about the packet-loss race, which is where the five seconds comes from. Fix the query count first, then measure.
5. Split-horizon, or a private hosted zone not attached to the production VPC. Both produce "works here, NXDOMAIN there".
The command that distinguishes them: run the same query from inside the production VPC, and compare with the provider's API export of the private zone. If the record exists in the zone but NXDOMAINs from prod, the zone is not attached to that VPC. If it does not exist in the zone at all, it was only ever in the staging view.
dig from your laptop proves nothing here — Module 06 D1's "ask from where" rule.
6. Build: a Route 53 alias record at the apex pointing at the load balancer — a CNAME is illegal there — plus failover routing with a health check, and a low TTL on any non-alias record involved.
Refuse to promise: 30-second failover. DNS moves traffic at the pace of the slowest cache holding the old answer, and that is not bounded by your TTL: resolver-side min-cache-ttl floors, serve-stale, and application caches that ignore TTLs entirely all extend it. Sub-minute recovery must come from the load balancer or an anycast address, with DNS as the slow, eventual layer.
7. 10.0.48.20 is RFC 1918 — a private address in a public zone. It is useless to external clients and it leaks internal topology: subnet ranges, and by implication how the network is laid out. It is a private record that escaped its horizon, usually because someone added it to the wrong zone.
Fix: move it to a private hosted zone attached to the VPCs that need it, and add a CI check that rejects RFC 1918 addresses in any public zone. This is exactly the finding from Module 02 D5.
The five things most people miss:
- Requirement 1's arithmetic — saying "ndots is wrong" without counting to four and then eight
- Requirement 3's distinction. NodeLocal is not "the same fix but bigger": it removes the race, while ndots removes the amplification
- Requirement 4 at all. "Scale it up" is the reflex, and resisting it with an argument about where the load comes from is the answer
- Requirement 6's refusal. Saying what you cannot promise is the senior half of the answer
- Requirement 7's second sentence. "It's a private IP" is the observation; "it leaks internal topology and is useless externally" is the finding
D4 · Official documentation
| Link | Covers |
|---|---|
| Kubernetes — DNS for Services and Pods | The whole of Part B: name shapes, headless Services, SRV records, dnsConfig and dnsPolicy |
| Debugging DNS Resolution | The official version of C1, including the standard debug pod |
| Customizing DNS Service · Using CoreDNS for Service Discovery | Corefile changes, stub domains, upstream forwarding |
| Using NodeLocal DNSCache | What it is, why it exists, and how to deploy it |
| CoreDNS — kubernetes plugin · CoreDNS home | Every Corefile plugin, with examples. The plugin index is the real reference |
| Route 53 — Supported DNS record types | What Route 53 supports, and how it differs from a zone file |
| Choosing between alias and non-alias records · simple alias values | The apex problem and its provider-specific solution |
| weighted · failover · latency · geolocation | One page per routing policy, with the exact fields each requires |
| RFC 9499 — split DNS · BIND 9 — views | The vendor-neutral definition, and the self-hosted equivalent of a private hosted zone |
Read the Kubernetes "DNS for Services and Pods" page end to end — it is the only document that states the naming rules authoritatively, and almost every blog post about Kubernetes DNS paraphrases it inaccurately.
Use the CoreDNS plugin index as a menu. Each plugin has one short page with a syntax block and examples; you never need more than the page for the plugin in front of you.
For Route 53, go to the routing-policy page for the specific policy rather than the developer guide's overview — the per-policy pages state exactly which fields are required, which is what you actually need when writing Terraform.
The offline route. Inside a cluster, kubectl -n kube-system get configmap coredns -o jsonpath='{.data.Corefile}' is the live documentation of what your resolver actually does, and cat /etc/resolv.conf in a pod tells you the rest. Between them they answer most questions without a browser.
D5 · Self-assessment
1. Why can't you use a CNAME at the apex, and what does Route 53 offer instead?
The apex must carry SOA and NS, and a CNAME excludes all other types at the same name. Route 53 offers alias records; other providers call it ALIAS, ANAME or CNAME flattening.
The provider resolves the target internally and returns a real A or AAAA, so nothing non-standard goes on the wire.
Consequences: invisible to dig, non-portable between providers, and resolved from the provider's network position rather than the user's.
2. What does a Route 53 routing policy actually see when it decides?
The resolver's address, not the user's. A user in Kuala Lumpur on a Singapore-based ISP resolver looks like a Singapore user.
EDNS Client Subnet partially fixes this by forwarding a truncated client prefix, but it is optional and not universally supported.
This is why geolocation and latency routing are approximate, and why they behave oddly for anyone using a large public resolver.
3. Why is DNS failover not fast failover?
Because resolvers hold the old answer for the TTL, resolver-side min-cache-ttl floors can extend it, serve-stale can extend it further when your servers are unreachable, and application caches ignore TTLs entirely.
So DNS moves traffic eventually, on the order of the TTL and often longer. Sub-second recovery has to come from a load balancer, an anycast address, or client-side retry.
4. Write out the DNS name of a Service, and say what each label means.
<service>.<namespace>.svc.cluster.local — the Service name, its namespace, the literal svc for Services, and the cluster domain.
A ClusterIP Service resolves to one virtual address; a headless Service resolves to one address per ready pod. Named ports also generate SRV records at _port._proto.<service>.<ns>.svc.cluster.local.
5. Explain the five-second Kubernetes DNS delay.
ndots:5 in the pod's resolv.conf makes almost every external name relative, so each lookup becomes four queries — three of them NXDOMAIN — and eight packets once A and AAAA go in parallel.
A conntrack race in the kernel can drop one of those parallel UDP packets. The client cannot detect loss, so it waits out the default 5-second timeout before retrying.
It is not a CoreDNS fault: ndots:5 multiplies the query count until a rare race becomes routine.
6. Give three fixes for it, cheapest first, and say what each addresses.
Trailing dot on external names — makes them absolute, one query instead of four, costs nothing, but must be applied name by name.
ndots:2 via dnsConfig — fixes the amplification cluster-wide, including in code you cannot edit.
NodeLocal DNSCache — addresses something different: it moves the CoreDNS conversation to TCP, removing the conntrack race itself rather than reducing how often it is hit, and absorbs cache hits on the node.
7. When would you choose a headless Service, and what does it cost you?
When clients must address individual pods: a database with a primary and replicas, Kafka brokers, anything doing its own sharding or leader election. StatefulSets add stable per-pod names.
The cost is that the RRset changes on every scale event and rollout, so it inherits every caching problem in DNS. Kubernetes serves a 30-second TTL, and any client that ignores TTLs will keep addressing pods that no longer exist.
8. CoreDNS is crash-looping with "loop detected". Cause and fix?
CoreDNS forwards unresolved queries to the node's /etc/resolv.conf. If that points at a local stub — 127.0.0.53 or 127.0.0.1 — then inside the CoreDNS pod that address is the pod itself, so it forwards to itself. The loop plugin detects it at startup and exits.
Fix: point kubelet's --resolv-conf at the file with the real upstream servers, or set explicit forwarders in the Corefile.
9. A name resolves in staging and NXDOMAINs in production. First two hypotheses?
Split-horizon — the name exists only in a private view that production's resolver does not see. Or a private hosted zone not attached to the production VPC.
Distinguish them by querying from inside the production VPC and comparing with the provider's API export of the zone. If the record is in the zone but NXDOMAINs from prod, the zone is not attached; if it is not in the zone at all, it only ever existed in the staging view.
A test from your laptop proves nothing — always ask from where a result was obtained.
10. Why are RFC 1918 addresses in a public zone a finding?
They are useless to external clients — nobody outside can route to 10.0.48.20 — and they leak internal topology: subnet ranges, addressing conventions, and by implication how the network is laid out.
It is almost always a private record added to the wrong zone. Move it to a private hosted zone and add a CI check that rejects RFC 1918 addresses in any public zone.
The last module. Anycast and how 13 root server names become a thousand machines; GeoDNS and the real limits of DNS load balancing; TTL strategy for migrations; amplification, reflection and response rate limiting; what cache poisoning defences actually buy; DoT, DoH and DoQ; DNS observability; and the incident playbooks that tie all nine modules together.
📚 Sources for the interview questions
A note on this module's transcripts. The Route 53 and Kubernetes behaviour described here is drawn from the vendor documentation linked above rather than captured live — this environment has neither an AWS account nor a cluster. The resolv.conf contents, name shapes and Corefile are the documented defaults; the latency figures in B2.1 are labelled in place as illustrative. Run B2.1 in your own cluster — the ratio between the two lines is the point, and it will reproduce.
The one thing verified live is the ndots mechanism itself: Module 06 B3's demonstration against seamless.se was captured on 20 August 2026 and is the same rule that produces the Kubernetes behaviour here.
Documentation verified directly: Kubernetes DNS for Services and Pods, NodeLocal DNSCache, Using CoreDNS for Service Discovery, Customizing DNS Service, CoreDNS kubernetes plugin, the Route 53 developer guide pages, and RFC 9499.
Question selection cross-referenced against publicly published 2026 DNS, Kubernetes and DevOps interview question sets:
- Kubernetes CoreDNS: Configuration, ndots Tuning, and Production Optimization (2026) — Coding Protocols
- Kubernetes DNS Troubleshooting: CoreDNS, ndots, and the 5-Second Timeout — kubenatives
- How Kubernetes DNS-Based Service Discovery Works — OneUptime
- Top 25 DNS Interview Questions and Answers for 2026 — nitizsharma.com — split-horizon DNS, DNS load balancing, forwarders vs conditional forwarders
- Top 30 Most Common DNS Interview Questions — Verve AI — Anycast and GeoDNS for load balancing
Answers were rewritten and deepened rather than reproduced. The published Kubernetes write-ups describe the five-second delay well but usually stop at "set ndots to 2". The parts that matter in an interview — that the amplification and the race are two different problems, that scaling CoreDNS treats neither, and that a trailing dot fixes the first for free — appear in almost none of them.