Kubernetes Lesson 35 – DNS in Kubernetes | Dataplexa
Networking, Ingress & Security · Lesson 35

DNS in Kubernetes

Every time a Pod calls another service by name — http://payment-svc or postgres.payments.svc.cluster.local — something resolves that name to an IP. That something is CoreDNS, and understanding exactly how it works is what separates engineers who debug network issues in two minutes from those who spend two hours.

CoreDNS: The Cluster's DNS Server

CoreDNS is a DNS server that runs as a Deployment in the kube-system namespace. Every Pod in the cluster has its /etc/resolv.conf configured to point at CoreDNS as its nameserver. When your application code calls payment-svc, the DNS resolution chain goes: process → kernel resolver → CoreDNS → Kubernetes Service record → ClusterIP.

CoreDNS is not just a passive DNS server — it actively watches the Kubernetes API. When you create a Service, CoreDNS immediately registers a DNS record for it. When you delete the Service, the record is removed. This happens in real time, without any manual DNS configuration. Creating a Service and calling it by name from any Pod in the cluster works immediately.

What CoreDNS creates for each Service

For every Service, CoreDNS creates an A record (or AAAA for IPv6) mapping the fully-qualified domain name to the Service's ClusterIP. For headless Services (clusterIP: None), it creates individual A records for each Pod IP instead. For ExternalName Services, it creates a CNAME record. For StatefulSet Pods, it creates individual A records per Pod with stable hostnames.

The DNS Name Format

Every Service in Kubernetes gets a DNS name following a predictable pattern. Understanding the pattern lets you construct the correct DNS name for any service without guessing.

DNS Name Components

payment-svc
.
payments
.
svc
.
cluster.local
Service name
The metadata.name of the Service object
Namespace
The namespace the Service lives in
svc
Literal — indicates this is a Service record
cluster.local
The cluster domain — configurable, defaults to cluster.local
Full form: payment-svc.payments.svc.cluster.local
Short forms (within same namespace): payment-svc or payment-svc.payments

The DNS Search Path — Why Short Names Work

In the previous lesson you called services by their short name — just payment-svc instead of the full FQDN. This works because Kubernetes configures each Pod's /etc/resolv.conf with a search path. When you query an unqualified name, the resolver automatically appends each domain in the search list and tries them in order until one resolves.

kubectl exec -it checkout-api-6f8b9d-2xkpj -n production -- cat /etc/resolv.conf
# Show the DNS configuration inside any running container
# This file is injected by Kubernetes when the Pod starts
nameserver 10.96.0.10
search production.svc.cluster.local svc.cluster.local cluster.local eu-west-1.compute.internal
options ndots:5

What just happened?

nameserver 10.96.0.10 — This is the ClusterIP of the kube-dns Service in kube-system — the stable IP that CoreDNS is reachable at. Every Pod in the cluster sends DNS queries to this IP, regardless of which namespace they're in.

search path explained — When a Pod in the production namespace queries payment-svc, the resolver tries these names in order until one answers:
1. payment-svc.production.svc.cluster.local ← hits immediately (same namespace)
2. payment-svc.svc.cluster.local (would try if #1 fails)
3. payment-svc.cluster.local (would try if #2 fails)
4. payment-svc.eu-west-1.compute.internal (external DNS, last resort)

options ndots:5 — A name with fewer than 5 dots is treated as a relative name and the search path is applied. A name with 5 or more dots is treated as absolute — the search path is skipped. This means payment-svc (0 dots) goes through the search path, but payment-svc.payments.svc.cluster.local. (with trailing dot = absolute) does not.

DNS Lookup Behaviour in Practice

The scenario: You're a developer trying to understand exactly how your checkout service resolves the payment service name. You need to trace the full DNS resolution chain from inside the Pod to understand a latency issue you're seeing on the first request.

kubectl exec -it checkout-api-6f8b9d-2xkpj -n production -- nslookup payment-svc
# Basic DNS lookup — resolves the short name using the search path
# Shows which nameserver answered and what IP was returned

kubectl exec -it checkout-api-6f8b9d-2xkpj -n production -- \
  nslookup payment-svc.production.svc.cluster.local
# Fully-qualified lookup — bypasses search path, direct query
# Returns same result but slightly faster (no search path iteration)

kubectl exec -it checkout-api-6f8b9d-2xkpj -n production -- \
  nslookup payment-svc.payments.svc.cluster.local
# Cross-namespace lookup by FQDN — calling a service in the 'payments' namespace
# from a Pod in the 'production' namespace

kubectl exec -it checkout-api-6f8b9d-2xkpj -n production -- \
  nslookup postgres-0.postgres-headless.payments.svc.cluster.local
# StatefulSet Pod DNS — stable per-Pod hostname
# Format: [pod-name].[headless-service-name].[namespace].svc.cluster.local
# postgres-0, postgres-1, postgres-2 are reachable individually via this pattern

kubectl run dns-debug --image=nicolaka/netshoot --rm -it --restart=Never -n production -- \
  dig payment-svc.production.svc.cluster.local +short
# dig: lower-level DNS query tool — shows TTL, query time, answer section
# +short: just the IP address — good for scripting
# Useful when nslookup doesn't give enough detail
$ kubectl exec -it checkout-api-6f8b9d-2xkpj -n production -- nslookup payment-svc
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      payment-svc
Address 1: 10.96.214.88 payment-svc.production.svc.cluster.local

$ kubectl exec -it checkout-api-6f8b9d-2xkpj -n production -- \
  nslookup payment-svc.payments.svc.cluster.local
Server:    10.96.0.10

Name:      payment-svc.payments.svc.cluster.local
Address 1: 10.96.77.31 payment-svc.payments.svc.cluster.local

$ kubectl exec -it checkout-api-6f8b9d-2xkpj -n production -- \
  nslookup postgres-0.postgres-headless.payments.svc.cluster.local
Name:      postgres-0.postgres-headless.payments.svc.cluster.local
Address 1: 10.244.2.14

What just happened?

Two different payment-svc Services — The short name payment-svc resolved to 10.96.214.88 (the one in the production namespace). The FQDN payment-svc.payments.svc.cluster.local resolved to 10.96.77.31 (the one in the payments namespace). Two completely different Services with the same name in different namespaces — namespace isolation at work. Short names are local to the namespace; FQDNs are global.

StatefulSet Pod DNS — The headless Service creates individual A records for each StatefulSet Pod. postgres-0 resolves to a Pod IP (10.244.2.14), not a ClusterIP. This is what makes StatefulSets work for databases — each replica has a stable, addressable DNS name that applications can use to target specific replicas for reads.

The ndots:5 latency problem — When a Pod queries a short name like payment-svc, the resolver tries up to four search path suffixes sequentially until one answers. That's potentially 4 DNS queries instead of 1. For services that make thousands of DNS lookups per second, this overhead is real. The fix: use the FQDN in performance-critical code paths to skip the search path entirely.

CoreDNS Configuration

CoreDNS is configured via a ConfigMap called coredns in the kube-system namespace. The config uses CoreDNS's own DSL (called a Corefile). Most clusters use the default configuration, but two customisations are commonly needed: forwarding specific domains to internal DNS servers, and tweaking caching behaviour.

The scenario: Your company has an on-premises Active Directory domain at corp.internal with an internal DNS server at 10.0.0.53. Pods in Kubernetes need to resolve db.corp.internal to reach on-premises databases. You need to configure CoreDNS to forward queries for corp.internal to the on-premises DNS server while still handling all cluster.local queries internally.

kubectl get configmap coredns -n kube-system -o yaml
# View the current CoreDNS configuration
apiVersion: v1
kind: ConfigMap
metadata:
  name: coredns
  namespace: kube-system
data:
  Corefile: |
    .:53 {                              # Handle all DNS queries on port 53
        errors                          # Log errors
        health {                        # Health check endpoint at :8080/health
            lameduck 5s                 # 5s grace period during shutdown
        }
        ready                           # Ready check endpoint at :8181/ready
        kubernetes cluster.local in-addr.arpa ip6.arpa {
                                        # Handle cluster.local and reverse DNS queries
            pods insecure               # Create DNS records for Pod IPs (insecure = no pod name validation)
            fallthrough in-addr.arpa ip6.arpa  # Fall through to next plugin for reverse DNS
            ttl 30                      # TTL for cluster.local records (30 seconds)
        }
        prometheus :9153                # Expose Prometheus metrics on port 9153
        forward . /etc/resolv.conf {    # Forward all other queries to the node's DNS resolver
            max_concurrent 1000         # Max concurrent upstream DNS queries
        }
        cache 30                        # Cache DNS responses for 30 seconds
        loop                            # Detect and break forwarding loops
        reload                          # Hot-reload config changes without restart
        loadbalance                     # Round-robin order of resolved IPs
    }

    corp.internal:53 {                  # Custom stanza: handle corp.internal queries specially
        errors
        cache 30
        forward . 10.0.0.53 {           # Forward all corp.internal queries to on-premises DNS
            force_tcp                   # Use TCP (more reliable for large responses like AD)
        }
    }
$ kubectl apply -f coredns-configmap.yaml
configmap/coredns configured

$ kubectl rollout restart deployment/coredns -n kube-system
deployment.apps/coredns restarted

$ kubectl exec -it checkout-api-6f8b9d-2xkpj -n production -- \
  nslookup db.corp.internal
Server:    10.96.0.10
Name:      db.corp.internal
Address 1: 10.0.1.45   ← resolved by on-premises DNS via CoreDNS forwarding

$ kubectl exec -it checkout-api-6f8b9d-2xkpj -n production -- \
  nslookup payment-svc.production.svc.cluster.local
Server:    10.96.0.10
Name:      payment-svc.production.svc.cluster.local
Address 1: 10.96.214.88   ← still resolved locally by CoreDNS kubernetes plugin

What just happened?

Selective forwarding — The custom corp.internal:53 stanza intercepts all queries for that domain before they reach the .:53 catch-all. It forwards them to the on-premises DNS at 10.0.0.53. The cluster.local queries are still handled entirely by the Kubernetes plugin — they never reach the external DNS.

CoreDNS hot-reload — The reload plugin in the Corefile means CoreDNS watches its own ConfigMap for changes and reloads automatically. In practice, a kubectl rollout restart is still the cleanest way to ensure the new config takes effect immediately across all replicas.

Prometheus metrics — CoreDNS exposes rich metrics at port 9153: query rate, error rate, cache hit/miss ratio, upstream latency. If your cluster is experiencing DNS resolution problems at scale (a very real production issue — DNS is often the bottleneck on high-RPS clusters), these metrics are essential for diagnosing it.

Pod DNS Settings

The default DNS behaviour can be overridden per-Pod using the dnsPolicy and dnsConfig fields. This is rarely needed but important to know for edge cases.

apiVersion: v1
kind: Pod
metadata:
  name: custom-dns-pod
  namespace: production
spec:
  dnsPolicy: ClusterFirst             # Default: use CoreDNS, fall back to node DNS for external
                                      # Other options:
                                      # Default: use the node's DNS (not CoreDNS) — inherits node's resolv.conf
                                      # ClusterFirstWithHostNet: use CoreDNS when hostNetwork: true
                                      # None: DNS entirely controlled by dnsConfig below
  dnsConfig:
    nameservers:
      - 10.96.0.10                    # CoreDNS ClusterIP — usually already set by dnsPolicy
      - 8.8.8.8                       # Add Google DNS as a second nameserver (failover)
    searches:
      - production.svc.cluster.local  # Custom search domains — appended to the defaults
      - svc.cluster.local
      - cluster.local
      - company.internal              # Add a custom corporate domain to the search path
    options:
      - name: ndots
        value: "3"                    # Lower ndots from 5 to 3 — reduces unnecessary search lookups
                                      # Names with fewer than 3 dots go through search path
                                      # Names with 3+ dots are treated as absolute
  containers:
    - name: app
      image: company/app:1.0.0
$ kubectl apply -f custom-dns-pod.yaml
pod/custom-dns-pod created

$ kubectl exec -it custom-dns-pod -n production -- cat /etc/resolv.conf
nameserver 10.96.0.10
nameserver 8.8.8.8
search production.svc.cluster.local svc.cluster.local cluster.local company.internal
options ndots:3

What just happened?

ndots: 3 performance optimisation — Reducing ndots from 5 to 3 means names with 3 or more dots are treated as absolute (no search path applied). The full FQDN payment-svc.production.svc.cluster.local has 4 dots — with ndots:5 it would still go through the search path, with ndots:3 it's treated as absolute and resolved directly. This eliminates unnecessary DNS queries for services that already use FQDNs. High-RPS services that call external APIs by FQDN benefit noticeably from this change.

dnsPolicy: None — When set to None, the Pod gets no DNS configuration whatsoever unless you specify it entirely in dnsConfig. This is the escape hatch for highly specialised workloads (network appliances, DNS forwarders) that need complete control over their resolver configuration. For 99% of applications, leave dnsPolicy at the default ClusterFirst.

DNS Record Types Created by Kubernetes

Different Kubernetes objects create different DNS record types. Here's the full picture:

Object type DNS name pattern Record type Resolves to
ClusterIP Service svc.ns.svc.cluster.local A record The Service's ClusterIP (stable virtual IP)
Headless Service svc.ns.svc.cluster.local Multiple A records Individual Pod IPs (changes with Pod churn)
StatefulSet Pod pod-N.svc.ns.svc.cluster.local A record That specific Pod's IP (stable name, IP may change on reschedule)
ExternalName Service svc.ns.svc.cluster.local CNAME record The externalName value (e.g. api.stripe.com)
Pod (with hostname set) hostname.subdomain.ns.svc.cluster.local A record The Pod's IP (requires subdomain + hostname fields set)

Teacher's Note: DNS is where most "why can't my service reach X" bugs hide

In my experience, about 40% of "Service A can't reach Service B" issues are actually DNS issues. Either the name is wrong, the namespace suffix is missing for a cross-namespace call, the Service was recently deleted and CoreDNS cached a negative response, or CoreDNS itself is having a bad day. The first thing I check is always: can I resolve the name from inside the calling Pod?

The two-minute DNS debug runbook: (1) kubectl exec -it [pod] -- nslookup [service-name] — does the name resolve at all? (2) If not, try the FQDN: nslookup [svc].[ns].svc.cluster.local. (3) Check the CoreDNS pods: kubectl get pods -n kube-system -l k8s-app=kube-dns. (4) Check CoreDNS logs: kubectl logs -n kube-system -l k8s-app=kube-dns. Most DNS issues are found in one of these four steps.

A real-world gotcha: the DNS cache TTL is 30 seconds by default. If you delete and recreate a Service (maybe you changed the type), Pods that already resolved the old IP may keep using it for up to 30 seconds. During blue-green deployments or Service changes, this caching window is real. Build in a 30–60 second wait after Service changes before assuming DNS has propagated.

Practice Questions

1. A Pod in the checkout namespace needs to call a Service named payment-svc in the payments namespace. What is the correct fully-qualified DNS name to use?



2. What is the name of the ConfigMap in the kube-system namespace that contains the CoreDNS configuration (Corefile)?



3. Which /etc/resolv.conf option controls the threshold for when a DNS name is treated as absolute (skipping the search path) vs relative (going through the search path)? What is Kubernetes's default value?



Quiz

1. A Pod in the production namespace calls http://order-svc/api. There is a Service named order-svc in the same namespace. Does the short name resolve successfully?


2. What is the key difference in DNS behaviour between a regular ClusterIP Service and a headless Service (clusterIP: None)?


3. A high-throughput service makes thousands of DNS lookups per second using short names like payment-svc. Why might this cause unnecessary DNS load, and what is the fix?


Up Next · Lesson 36

Network Policies

By default every Pod can talk to every other Pod. Network Policies let you define firewall rules inside the cluster — restricting which Pods can communicate with which, and blocking lateral movement if a service is compromised.