Module 08 — DNS in the Cloud & in Kubernetes

Updated 20 August 2026

Module 08 · DNS in the Cloud & in Kubernetes

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 analogy. Think of hiring a cleaning service instead of buying a mop.

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.

Module 02 D1 read seamless.se's four awsdns nameservers and concluded "this is Route 53". That inference is the practical value of knowing the protocol: the NS record set names your provider, and from there you know which console, which API, which quirks, and which of this module's sections apply.

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

The analogy. Think of a receptionist who quietly looks something up and reads the answer back to you as if she had always known it.

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.

ProviderName for it
Route 53alias record
CloudflareCNAME flattening
DNSimple, easyDNSALIAS
DNS Made Easy and othersANAME
None of them is a DNS feature, and understanding that tells you every consequence. The authoritative server resolves the target internally and answers with a genuine A or AAAA. What leaves the server is an ordinary address record, so nothing non-standard ever appears on the wire and no resolver needs to know.

Three things follow:

  1. 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
  2. They do not survive a provider migration. Every one must be re-created by hand, and forgetting one is an apex outage
  3. 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
The Route 53 detail worth knowing: alias records to AWS targets are free and health-aware. Queries against an alias pointing at an ELB, CloudFront distribution or S3 website endpoint are not billed, and Route 53 tracks the target's address changes automatically. That is why the AWS-native answer to "point the apex at the load balancer" is always an alias and never a CNAME on www with a redirect.

A3 · Routing policies — where DNS stops being a lookup

The analogy. Think of a call centre that routes you by the area code of the phone you dialled from.

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.

PolicyWhat decides the answer
SimpleNothing. One RRset, returned to everyone
WeightedA weight per record — canary and blue/green deployments
FailoverA health check. Primary while healthy, secondary otherwise
LatencyMeasured latency from the querying resolver to each AWS region
Geolocation / GeoproximityWhere the querying resolver appears to be
Multivalue answerUp to eight health-checked records, returned together
Every one of these decides based on the RESOLVER's address, not the user's — and that single fact is the source of most disappointment with DNS-based traffic management.

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

The analogy. Think of the staff entrance and the public entrance.

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.

This is the correct answer to "how do we keep internal hostnames secret", and Module 07 B4 explains why it is the only correct answer. NSEC3 obfuscates names in a signed public zone; split-horizon means they were never in the public zone at all. Obfuscation versus absence.

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.

Split-horizon is also the reason "it works for me" is worthless evidence, and you have already met the symptom twice. Module 06 C2's VPN example — corp.internal routed to the VPN's resolver — is split-horizon from the client side. Module 06 D1's triage exists because of it.

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 outsidedig 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

The analogy. Think of a building where every desk gets a name badge automatically the moment someone sits down at it.

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:

plain text
<service>.<namespace>.svc.cluster.local
        │          │      │      │
        │          │      │      └── the cluster domain, configurable
        │          │      └───────── always "svc" for Services
        │          └──────────────── the namespace
        └─────────────────────────── the Service name
ObjectWhat DNS returns
ClusterIP ServiceOne 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 portsSRV records at _port._proto.<service>.<ns>.svc.cluster.local — Module 02 B8, generated for you
ExternalName ServiceA CNAME to a name outside the cluster
StatefulSet podsStable per-pod names: <pod>.<service>.<ns>.svc.cluster.local
The distinction that matters most in practice is ClusterIP versus headless.

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

The analogy. Think of an office phone that tries three internal extensions before every outside call.

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:

plain text
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
Apply Module 06 B3's rule to that file and the famous Kubernetes DNS problem falls straight out.

ndots: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:

plain text
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 answer

Four 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.

FixEffect
Trailing dot on external namesapi.example.com.Absolute, so ndots never applies. One query. Costs nothing, fixes it completely
dnsConfig with ndots: "2" per podCluster names still work; external names skip the search list
NodeLocal DNSCacheA per-node caching resolver, talking to CoreDNS over TCP. Removes the UDP race and most of the latency
single-request-reopen in dnsConfigSeparates the A and AAAA queries, working around the conntrack race
🧪 Exercise B2.1 — Count the queries a pod actually sends
bash
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
plain text
$ 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.local

Run 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

The analogy. Think of the switchboard's routing rules, written as a short list read from the top.

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.

plain text
.: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
}
PluginJob
kubernetesAnswers *.cluster.local from the Kubernetes API. ttl 30 is the 30 seconds from B1
forwardEverything else goes upstream, to the node's /etc/resolv.conf. Module 03 C3's forwarding
cache 30CoreDNS's own cache — Module 03 B, inside the cluster
loopDetects a forwarding loop at startup and refuses to run. See below
reloadPicks up ConfigMap changes with no restart
loadbalanceRotates A records in responses — the round-robin from Module 01 C3
The loop plugin's crash is one of the most-searched Kubernetes errors, and it is a configuration bug rather than a CoreDNS bug. CoreDNS forwards unresolved queries to the node's /etc/resolv.conf. If that file points at a local stub — 127.0.0.53 from Module 06 C1, or 127.0.0.1 — then inside the CoreDNS pod that address is the pod itself. CoreDNS forwards to itself, forever.

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

The analogy. Think of putting a small fridge in the office instead of walking to the shop for every drink.

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.

A DaemonSet that puts a caching resolver on every node, so pods query an address on their own host instead of crossing the network to a CoreDNS pod.

It fixes three things at once, and each is something you have already met:

  1. Cache hits never leave the node — the ndots query amplification from B2 stops being network traffic
  2. It talks to CoreDNS over TCP, sidestepping the conntrack UDP race that causes the five-second delays
  3. 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 listener127.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

The analogy. Think of bringing your own toolbox to a site rather than hoping there is one in the cupboard.

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.

bash
# 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}'
Step 1 is Module 02 C1.1's lesson applied. Production images have no dig, and installing packages into a running application container is not a plan. A purpose-built debug image attached alongside is.

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

The analogy. Think of the walkthrough you do before accepting the keys to a rented flat.

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.

CheckWhy
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 APIInvisible 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:#fff

Four ideas, and everything in this module follows.

  1. Alias records are provider features, not DNS. Invisible from outside, non-portable, resolved from the provider's location.
  2. DNS-based routing decides on the resolver's address and moves traffic at the pace of a TTL. It is not fast failover.
  3. ndots:5 multiplies every external lookup by four, and a trailing dot undoes it for free.
  4. CoreDNS forwarding to a stub address is a loop. Module 06's 127.0.0.53 confusion, two layers up.

D2 · Production practice

HabitWhy
Write external names with a trailing dot in cluster configurationSkips the search list entirely: one query instead of four, at zero cost
Export the zone from the provider's API, not from digAlias 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 failoverTTLs, resolver floors, serve-stale and application caches all outlive your health check
Keep RFC 1918 addresses out of public zonesThey leak internal topology and are useless externally. Put them in a private zone
Point kubelet's --resolv-conf at the real upstream fileForwarding to 127.0.0.53 makes CoreDNS forward to itself; loop then kills the pod
Prefer ClusterIP unless clients genuinely need individual podsHeadless RRsets change constantly and clients that ignore TTLs will address dead pods
Deploy NodeLocal DNSCache on clusters with heavy external trafficAbsorbs the query amplification and moves CoreDNS traffic to TCP, removing the conntrack race
Keep a debug image with dig, getent, curl and ssApplication images have no tools, and installing into a running pod is not a plan
Ask "from where?" before believing any DNS test resultWith split-horizon in play, the answer depends on the asker's network position
Record which routing policy each record uses, in codeWeighted and latency policies are invisible in dig output and easy to lose in a migration

D3 · Capstone exercise

A cloud-and-cluster investigation using only what this track has taught. No scrolling back.

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:

  1. From inside a pod, show the configuration responsible and explain the arithmetic — how many queries does one external lookup actually cost?
  2. Give the zero-cost fix, and say which module first taught it.
  3. Give two further fixes and say what each one addresses that the first does not.
  4. The team wants to "scale up CoreDNS". Explain why that may not help.
  5. The same name resolves in staging and NXDOMAINs in production. Give the two most likely causes and the command that distinguishes them.
  6. 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.
  7. 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:

  1. Requirement 1's arithmetic — saying "ndots is wrong" without counting to four and then eight
  2. Requirement 3's distinction. NodeLocal is not "the same fix but bigger": it removes the race, while ndots removes the amplification
  3. 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
  4. Requirement 6's refusal. Saying what you cannot promise is the senior half of the answer
  5. 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

LinkCovers
Kubernetes — DNS for Services and PodsThe whole of Part B: name shapes, headless Services, SRV records, dnsConfig and dnsPolicy
Debugging DNS ResolutionThe official version of C1, including the standard debug pod
Customizing DNS Service · Using CoreDNS for Service DiscoveryCorefile changes, stub domains, upstream forwarding
Using NodeLocal DNSCacheWhat it is, why it exists, and how to deploy it
CoreDNS — kubernetes plugin · CoreDNS homeEvery Corefile plugin, with examples. The plugin index is the real reference
Route 53 — Supported DNS record typesWhat Route 53 supports, and how it differs from a zone file
Choosing between alias and non-alias records · simple alias valuesThe apex problem and its provider-specific solution
weighted · failover · latency · geolocationOne page per routing policy, with the exact fields each requires
RFC 9499 — split DNS · BIND 9 — viewsThe vendor-neutral definition, and the self-hosted equivalent of a private hosted zone
How to read these efficiently.

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.


Next — Module 09 · Performance, Reliability & Security at Scale.

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:

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.

Spotted a mistake or want something added? Send me a note.