9 min read

K8s Networking: Cluster DNS and DNS Client Configurations.

K8s Networking: Cluster DNS and DNS Client Configurations.

In k8s, DNS is provided as a cluster service. As part of the default installation of k8s, 2 pods for the DNS functionality are launched (for purposes of robustness and load balancing). Pods are required to use this internal cluster DNS for their networking needs. There are multiple options available for k8s internal DNS but we will focus on CoreDNS, the out of the box service we get when we install k8s. CoreDNS is a lean implementation of the Layer 7 DNS Protocol and uses plugins for providing a full-features DNS service. This makes customizing it for specific needs very easy.

🗣️
If you are interested in learning more about k8s installation, read K8s Installation: The What's and Why's.

In the DNS service, DNS records are created for objects in the cluster. For example, Services will get either A records (for IPv4) or Quad A ('AAAA') records (for IPv6) added to the DNS. Namespaces have DNS subdomains created for them.

There should be no doubt that the DNS functionality is CORE to Service Discovery inside a cluster. As resources and services are created, they are added to the DNS service and can then be referred to by the pods. This referral is done as a Service Discovery, using DNS names, facilitated by (no prizes for guessing) the DNS service.


On a tangent

The image below is a succinct flow of bits and bytes as we try to load our favorite web site in our browser.

Source: https://www.hostinger.com/tutorials/wp-content/uploads/sites/2/2023/01/how-does-dns-work.png

An investigation into the Cluster DNS Service.

The pods and services that make the CoreDNS Service work are installed, out of the box, in the kube-system namespace. We can confirm that by getting a list of all services and pods running in this namespace:

Figure 1: 2 pods and 1 service that configure CoreDNS.

The service is of type ClusterIP (because it will only be accessible to cluster objects internally) and has an IP address of 10.96.0.10.

In addition, we see 2 pods deployed, for scaling and load balancing purposes.

Finally, we also see the deployment that installed both the service and the pods.

A peek inside one of the CoreDNS pods.

Using kubectl describe, print a pod's metadata on the terminal:

Figure 2: kubectl describe prints a pods metadata to the terminal.

Scrolling through the output, we should focus on three specific sections of the metadata: Container, Mounts and Volume to understand how a pod is set up to use CoreDNS.

Figure 3: CoreDNS configuration is loaded via a configmap mounted as a volume from which the container will get CoreDNS settings.

As seen in Figure 3, a ConfigMap object (called coredns) is saved as a Volume, mounted in the /etc/coredns folder and finally passed as an argument to the CoreDNS container. This seperation of configuration values and how they are consumed makes it easy for us to change the contents of coredns, and simply reload them into the container (something we will see later).

What does this coredns ConfigMap look like?

Using the handy kubectl describe command, we can view the contents of coredns (the ConfigMap).

Figure 4: Notice the Corefile section. This is the same Corefile that is passed as argument to coredns docker container.

The contents of the Corefile is a series of plugins which have been chained together to provide the basic out of the box DNS capabilities.

🗣️
You can read more about the plugins and the content of Corefile here.
We will only discuss some of the more important plugins in this article.
  • errors: This plugin is responsible for printing errors on stdout.
  • kubernetes: This plugin implements Kubernetes DNS-Based Service Discovery Specification and allows CoreDNS to be used as a replacement for kube-dns in a k8s cluster.
  • cluster.local: The kubernetes plugin declaration is followed by the name of zones to be used for name resolution. Cluster.local is the default domain name used for DNS resolution within the cluster. For example, a service will have a DNS name in the format <service-name>.<namespace the services is in>.svc.cluster.local while the DNS name for a Pod has the format of <pod-ip-address-with-hypens>.<namespace>.pod.cluster.local.
🗣️
Read Claire Lee's medium post for getting more information about cluster.local and its part in DNS resolution within the cluster.
  • prometheus: Metrics of CoreDNS are available at http://localhost:9153/metrics in the Prometheus format (also known as OpenMetrics).
  • forward: Tells coredns where to forward queries that are not within the k8s cluster domain. In this case, such queries are to be forwarded to /etc/resolv.conf.

A bit more about forward.

The plugin forward will direct request for ANY domain to the /etc/resolv.conf file of the node on which the pod is deployed (the '.' right after forward is a catch all for any domain being queried).

The /etc/resolv.conf file on each node has the entries needed to resolve a query and forward it to the right pod or service.

Figure 5: /etc/resolv.conf file belongs to the node the pod is deployed on.

The /etc/resolv.conf file is called a forwarder and this one is an out of the box inclusion for our nodes, and k8s defaults to this forwarder. However, if needed, we can direct queries to a custom forwarder by making a simple change in the coredns ConfigMap.

Configure CoreDNS to use custom forwarder.

Since the coredns ConfigMap is the source for CoreDNS configuration, it makes sense to edit this ConfigMap for custom forwarding of DNS queries.

To do so, we will need to make a change in the coredns ConfigMap. Start this by first generating the YAML file representing the default coredns ConfigMap as shown in Figure 6 below:

Figure 6: Generate the ConfigMap coredns and save it to a local file.

Change the text /etc/resolv.conf to 1.1.1.1 and add a new server block for conditional forwarding of DNS queries to 9.9.9.9 as shown in Figure 7 below:

Figure 7: Customizing CoreDNS configuration.
🗣️
Both 1.1.1.1. and 9.9.9.9 are popular DNS servers. Read more about 1.1.1.1 here and about 9.9.9.9 here.

Save the changes to defaultCoreDNSCM.yaml and apply it (as shown in Figure 8 below):

Figure 8: Updated ConfigMap coredns is applied.

The changes to the configuration takes a minute or two to get applied to the pods.

To test the modified ConfigMap and the CoreDNS Server, we can use the nslookup application in Linux:

Figure 9: CoreDNS is up and running with modified configuration.
Figure 10: Custom Forwarding of any DNS query for domain ghost.com is up and running.
🚨
Revert the ConfigMap values to the default values (i.e. change 1.1.1.1. to /etc/resolv.conf and remove the block for ghost.com completely).

Configureing Pod DNS Configuration.

The previous section was all about making changes to CoreDNS Service's configuration at the global cluster level i.e. the changes apply to all the pods in the cluster. However, we may have conditions where the global cluster level change needs to be overriden at the pod level. In such cases, the ANY pod that has an overriding DNS configuration will IGNORE CoreDNS.

As an example, let's assume there is some reason due to which we want some to-be-deployed pods to bypass the default CoreDNS rule of forwarding DNS queries to the nodes /etc/resolv.conf forwarder. However, we may have many other existing pods which do not need to bypass the default CoreDNS configuration.

For the to-be-deployed pods, we can add the dnsPolicy attribute in their specifications.

🗣️
Read more about DNS Services in Kubernetes here.

Let's quickly examine the contents of the override_dns_pod_level.yaml file.

Figure 11: Adding dnsPolicy: None allows us to define a custom policy at the pod level.

The outlined section indicates that the pods that are deployed as part of this manifest will have their DNS queries passed onto the DNS Server at 9.9.9.9.

Figure 12: Manifest deployed using kubectl create.

To confirm our custom DNS policy for the pods in this deployment were actually implemented, we can check the /etc/resolv.conf file inside the pod.

Figure 13: Get a list of the pods just deployed and select any one of them for the next step.

Selecting the first pod in the list above (in Figure 13), we can get a glimpse of the content of its /etc/resolv.conf file:

Figure 14: Notice the nameserver shows up as 9.9.9.9
🚨
Strongly suggest clearing out these pods and the service before proceeding. You can use:
$ kubectl delete -f override_global_coredns_config.yaml.

Pod and Service DNS Records.

By this point, we have:

  • Investigated the default CoreDNS configuration values
  • Modified the default CoreDNS configuration values and also tested a domain-specific custom forwarder and
  • Applied a pod-level DNS query rule using the dnsPolicy attribute.

Before closing out this article, we will do one last thing and that is view the actual DNS A records for pods and services, saved in CoreDNS Service.

First, lets complete a simple deployment consisting of 3 replicas and one service.

Figure 15: A simple deployment with 3 replicas and 1 service.

To get the DNS A record for the pods and the service just deployed, we have use nslookup again.

nslookup expects, at least 2 parameters to be passed to it (as shown in Figure 16 below):

Figure 16: nslookup wants the FQDN of the pod or service along with the IP of the ClusterIP Service that was set up with this deployment.

Following the structure of the FQDN provided above, we can check the DNS A record of any one of the pods if we have the following data:

  • The IP of one of the pods with the '.' replaced by a '-'
  • The namespace the pod is deployed in
  • The word 'pod' in place of type-of-k8s-object
  • The word 'cluster.local'

We can get both 1 & 2 from the list above by using kubectl get pods -all-namespaces --selector=app=hello-world -o wide:

Figure 17: We will select the first pod in this list for our demo.

The FQDN for the first pod will be:

192-168-235-77.default.pod.cluster.local

We can also get the IP of the ClusterIP Service in kube-system namespace:

Figure 18: We also have the IP of the ClusterIP service associated with the hello-world pods.

Finally, we can use nslookup to get the IP address of the pod:

Figure 19: IP address of the pod as its cataloged in CoreDNS Service.

Services also have a DNS A record in CoreDNS which can be confirmed using the same type of steps as those taken for a pod with some minor difference.

  • Difference # 1: For pods, the FQDN begins with their IP address while for services, the FQDN begins with the name of the service. In our case, the name of the service deployed with the pods is 'hello-world'.
  • Difference # 2: The term pod is replaced by the term svc.
Figure 20: With the outlined changes in place, we can also confirm the IP address of the hello-world service, as it has been documented by CoreDNS.
🧩
In summary:
- CoreDNS is the default DNS Service for k8s.
- We can configure DNS rules and forwarders by editing the coredns ConfigMap in kube-system namespace.
- We can also override DNS Service locations at the pod level by setting the dnsPolicy attribute to "None".

I write to remember and if in the process, I can help someone learn about Containers, Orchestration (Docker Compose, Kubernetes), GitOps, DevSecOps, VR/AR, Architecture, and Data Management, that is just icing on the cake.