The most surprising thing about CoreDNS’s plugin chain is that it’s not a chain at all, but a series of conditional transformations applied to a DNS request as it passes through the server.
Let’s watch a query for www.example.com arrive.
# Simulate a DNS query using dig
dig @127.0.0.1 www.example.com A +short
Imagine Corefile looks like this:
.:53 {
log
errors
cache 30
kubernetes cluster.local in-addr.arpa ip6.arpa
forward . 8.8.8.8 8.8.4.4
prometheus
}
When www.example.com hits CoreDNS, the log plugin is the first one to see it. It doesn’t change the request; it just records that it arrived. Then, the errors plugin checks if any previous plugin (there are none yet) returned an error. It doesn’t.
Next, cache looks to see if it has a recent answer for www.example.com A. If it does, it returns that answer immediately, and the rest of the plugins are skipped for this request. If not, it passes the request along.
The kubernetes plugin is where things get interesting. It checks if the query matches any of its configured zones (cluster.local, in-addr.arpa, ip6.arpa). If www.example.com were my-service.cluster.local, this plugin would try to resolve it using the Kubernetes API. Since it’s not, it passes the request down.
The forward plugin is the workhorse for external lookups. It sees that the query isn’t handled by any preceding plugin and forwards it to the upstream DNS servers 8.8.8.8 and 8.8.4.4. These servers respond, and the answer travels back up the plugin "chain."
Finally, the prometheus plugin intercepts the response. It doesn’t alter the response itself, but it scrapes metrics about the request and its eventual answer (like query type, response code, and latency) and exposes them on its /metrics endpoint. The answer is then sent back to the client.
The mental model to grasp is that each plugin has a ServeDNS method. When a request comes in, CoreDNS iterates through the plugins in the order they appear in the Corefile. Each plugin’s ServeDNS method is called. It can:
- Handle the request: If a plugin has the answer, it writes the response and returns
nilerror. The iteration stops. - Pass the request on: If a plugin cannot handle the request, it calls
Next.ServeDNS(ctx, w, r). This passes the request to the next plugin in the configuration. - Return an error: If something goes wrong within a plugin, it returns an error. This error will propagate back up the chain.
You control the behavior by ordering the plugins. For instance, placing cache after kubernetes would mean cached responses for cluster.local domains would never be served. A common pattern is to put authoritative zone plugins first, then internal resolution (like kubernetes), then forward, and finally metrics and logging plugins at the end to capture everything.
The kubernetes plugin doesn’t just resolve *.cluster.local. It also handles reverse lookups for Pod IPs within the cluster’s service IP range, translating an IP address back into a hostname like 10-0-1-5.my-namespace.pod.cluster.local. This is crucial for tools that rely on reverse DNS for introspection.
Understanding how the kubernetes plugin interacts with the forward plugin is key to debugging why internal cluster services might be intermittently unreachable from pods.