Kubernetes - DNS Resolution

You can follow this post with the interactive scenario on Katacoda.

We are going to explore how DNS resolution in Kubernetes works. First we create a namespace, then we create a pod and expose it via service. Afterwards, a second pod is used to perform DNS queries against the Kubernetes DNS Resolver to get the IP address of the service.

Namespace

We create a namespace that we will use to bind our resources.

kubectl create namespace dev

Next, we run a pod via imperative command.

Running some Pod

kubectl run my-app --image nginx --namespace dev --port 80

Describe the Pod

kubectl describe pod my-app -n dev
Name:         my-app
Namespace:    dev
Priority:     0
Node:         raspberrypi/192.168.1.41
Start Time:   Fri, 25 Jun 2021 01:21:16 +0100
Labels:       run=my-app
Annotations:  <none>
Status:       Running
IP:           10.42.0.126
IPs:
  IP:  10.42.0.126
Containers:
  my-app:
    Container ID:   containerd://086772d833ec67917a98ef43561d6f18779f086daa5b93a3390474a6aa707160
    Image:          nginx
    Image ID:       docker.io/library/nginx@sha256:47ae43cdfc7064d28800bc42e79a429540c7c80168e8c8952778c0d5af1c09db
    Port:           80/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Fri, 25 Jun 2021 01:21:20 +0100
    Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-fnb62 (ro)
Conditions:
  Type              Status
  Initialized       True
  Ready             True
  ContainersReady   True
  PodScheduled      True
Volumes:
  kube-api-access-fnb62:
    Type:                    Projected (a volume that contains injected data from multiple sources)
    TokenExpirationSeconds:  3607
    ConfigMapName:           kube-root-ca.crt
    ConfigMapOptional:       <nil>
    DownwardAPI:             true
QoS Class:                   BestEffort
Node-Selectors:              <none>
Tolerations:                 node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
                             node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  30s   default-scheduler  Successfully assigned dev/my-app to raspberrypi
  Normal  Pulling    29s   kubelet            Pulling image "nginx"
  Normal  Pulled     28s   kubelet            Successfully pulled image "nginx" in 1.369183373s
  Normal  Created    28s   kubelet            Created container my-app
  Normal  Started    27s   kubelet            Started container my-app

Note the label that has been set by Kubernetes. run=my-app By default, Kubernetes will set labels that match the resource name. For resources started from a run it will have the form run=<resource-name>.

Now we can expose the pod. This will create a service matching the pods label.

Create Service

kubectl expose pod my-app --namespace dev

Check The Service

kubectl describe service my-app -n dev
Name:              my-app
Namespace:         dev
Labels:            run=my-app
Annotations:       <none>
Selector:          run=my-app
Type:              ClusterIP
IP Family Policy:  SingleStack
IP Families:       IPv4
IP:                10.43.52.98
IPs:               10.43.52.98
Port:              <unset>  80/TCP
TargetPort:        80/TCP
Endpoints:         10.42.0.126:80
Session Affinity:  None
Events:            <none>

Note how the service selector is matching the label run=my-app. That means it will match the pod we have previously deployed.

Now we can deploy another pod from which we query the Kubernetes DNS Resolver.

Run dnsutils Pod

We run this pod in interactive mode and attach stdin so that we can use nslookup and dig from within the container to query the Kubernetes DNS Resolver.

kubectl run dnsutils --namespace dev --image tutum/dnsutils -ti -- bash

Making DNS Queries

nslookup resolves the service ok

nslookup my-app
Server:         10.43.0.10
Address:        10.43.0.10#53

Name:   my-app.dev.svc.cluster.local
Address: 10.43.52.98

But dig doesn't find the service, why?

dig my-app
; <<>> DiG 9.11.5-P4-5.1+deb10u5-Debian <<>> my-app
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 51094
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
; COOKIE: 4c23b7d697ed3587 (echoed)
;; QUESTION SECTION:
;my-app.                                IN      A

;; AUTHORITY SECTION:
.                       13      IN      SOA     a.root-servers.net. nstld.verisign-grs.com. 2021062402 1800 900 604800 86400

;; Query time: 0 msec
;; SERVER: 10.43.0.10#53(10.43.0.10)
;; WHEN: Fri Jun 25 01:41:30 UTC 2021
;; MSG SIZE  rcvd: 122

/etc/resolv.conf

In order to understand why dig doesn't find the service, let's take a look at /etc/resolv.conf

cat /etc/resolv.conf
search dev.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.43.0.10
options ndots:5

This file contains a line with the following format.

search <namespace>.svc.cluster.local svc.cluster.local cluster.local

That means, when providing an incomplete part of the fully qualified domain name (FQDN), this file can be used to complete the query. However, dig doesn't do it by default. We can use the +search flag in order to enable it.

dig +search my-app
; <<>> DiG 9.11.5-P4-5.1+deb10u5-Debian <<>> +search my-app
;; global options: +cmd
;; Got answer:
;; WARNING: .local is reserved for Multicast DNS
;; You are currently testing what happens when an mDNS query is leaked to DNS
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 39376
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
; COOKIE: de26c4eaa4e53026 (echoed)
;; QUESTION SECTION:
;my-app.dev.svc.cluster.local.  IN      A

;; ANSWER SECTION:
my-app.dev.svc.cluster.local. 5 IN      A       10.43.52.98

;; Query time: 0 msec
;; SERVER: 10.43.0.10#53(10.43.0.10)
;; WHEN: Fri Jun 25 01:42:34 UTC 2021
;; MSG SIZE  rcvd: 113

Now the service-name has been correctly resolved.

We can get the same service without +search flag when using the FQDN. The +short flag isn't required, but it will reduce the output to only the IP address.

$ dig +short my-app.dev.svc.cluster.local
10.43.52.98

However, the benefit of using the search method it that queries will automatically resolve to resources within the same namespace. This can be useful to apply the same configuration to different environments, such as production and development.

Resources in different namespaces always need to be looked up by the FQDN.

The same way the search entry in resolv.conf completes the query with the default name space, it will complete any part of the FQDN from left to right. So in the below example, it will resolve to the local cluster.

$ dig +short +search my-app.dev
10.43.52.98