Kubernetes/Network Study

CoreDNS와 NodeLocal DNS 이해하기

백곰곰 2024. 10. 19. 15:17
728x90
반응형

가시다님의 Kubernetes Advanced Networking Study에 참여하게 되어, 스터디 때 다룬 주제를 정리하려고 합니다.
6주차는 Ingress & Gateway API + CoreDNS를 주제로 진행되었습니다.
이번 글에서는 CoreDNS와 NodeLodalDNS에 대해 다루고 있습니다.
 

CoreDNS

CoreDNS는 Kubernetes 클러스터 내부 DNS 서버입니다.
클러스터에서 Pod와 Service가 생성되면 자동으로 이에 대한 DNS 레코드가 생성됩니다.
 

Corefile

CoreDns deployment에는 아래 configmap을 /etc/coredns에 마운트하여 사용하고 있습니다.

apiVersion: v1
data:
  Corefile: |
    .:53 {
        errors
        health
        ready
        kubernetes cluster.local in-addr.arpa ip6.arpa {
          pods insecure
          fallthrough in-addr.arpa ip6.arpa
        }
        hosts /etc/coredns/NodeHosts {
          ttl 60
          reload 15s
          fallthrough
        }
        prometheus :9153
        forward . /etc/resolv.conf
        cache 30
        loop
        reload
        loadbalance
        import /etc/coredns/custom/*.override
    }
    import /etc/coredns/custom/*.server
  NodeHosts: |
    192.168.10.10 k3s-s
    192.168.10.101 k3s-w1
    192.168.10.102 k3s-w2
    192.168.10.103 k3s-w3
kind: ConfigMap
metadata:
  name: coredns
  namespace: kube-system

Corefile은 CoreDNS 설정 파일이며, 기본적으로 아래와 같은 형태를 갖습니다.

ZONE:[PORT] {
    [PLUGIN]...
}

사용 가능한 플러그인 목록은 공식 문서에 있습니다.
 
현재 클러스터(k3s로 구성)의 Corefile 내용과 몇가지 플러그인을 살펴보겠습니다.

    .:53 {    ## 모든 도메인(zone)의 53(DNS 기본 포토)번 포트를 처리
        errors   ## error logging 활성화
        health   ## health check endpoint 활성화
        ready    ## readiness check 활성화
        kubernetes cluster.local in-addr.arpa ip6.arpa {  ## k8s 클러스터에서 zone 데이터를 가져옴
          pods insecure  ## POD-MODE 설정, 항상 A 레코드를 IP 주소와 함께 반환
          fallthrough in-addr.arpa ip6.arpa ## DNS 쿼리 결과가 NXDOMAIN일 때 in-addr.arpa, ip6.arpa에 대해서만 fallthrough 적용 
        }
        hosts /etc/coredns/NodeHosts {  ## hosts 파일 위치 지정
          ttl 60 ## dns TTL
          reload 15s ## hosts 파일 변경 반영을 위한 리로드 주기
          fallthrough ## 해당 플러그인이 권한을 갖는 모든 zone에 대해 fallthrough
        }
        prometheus :9153 ## prometheus 메트릭 활성화
        forward . /etc/resolv.conf ## 상위 resolver로 DNS 메세지 프록시
        cache 30  ## frontend cache 활성화
        loop ## 간단한 forwarding 루프를 감지하고 프로세스 중지
        reload ## Corefile의 변경 사항을 자동 reload
        loadbalance ## A, AAAA, MX 레코드의 응답을 무작위로 섞어서 여러 IP간 분산
        import /etc/coredns/custom/*.override ## Corefile에 포함할 파일 지정
    }

 
kubernetes 플러그인
참고) https://github.com/coredns/coredns/tree/master/plugin/kubernetes

kubernetes [ZONES...] {
    endpoint URL
    tls CERT KEY CACERT
    kubeconfig KUBECONFIG [CONTEXT]
    namespaces NAMESPACE...
    labels EXPRESSION
    pods POD-MODE
    endpoint_pod_names
    ttl TTL
    noendpoints
    fallthrough [ZONES...]
    ignore empty_service
}

클러스터 내부 도메인(cluster.local)에 대한 질의을 처리하기 위해 사용합니다.
pods : 파드 A record를 처리하는 방법 지정

  • disabled : Pod에 대한 DNS 요청을 처리하지 않으며, 항상 NXDOMAIN(도메인이 존재하지 않음) 응답을 반환
  • insecure : A 레코드를 IP와 함께 항상 반환
  • verified : 요청된 IP(resolve된 IP)와 일치하는 Pod가 동일한 네임스페이스에 존재하는 경우에만 A 레코드를 반환

fallthrough : DNS 쿼리 결과가 NXDOMAIN일 때의 동작을 결정하며, 설정 시 쿼리가 다른 플러그인으로 전달
 
hosts 플러그인
참고) https://github.com/coredns/coredns/tree/master/plugin/hosts

hosts [FILE [ZONES...]] {
    [INLINE]
    ttl SECONDS
    no_reverse
    reload DURATION
    fallthrough [ZONES...]
}

/etc/hosts 형식의 파일에서 zone 데이터를 제공하며, 파일을 지정하지 않으면 기본적으로 /etc/hosts 참조
 
forward 플러그인
참고) https://github.com/coredns/coredns/tree/master/plugin/forward

forward FROM TO... {
    except IGNORED_NAMES...
    force_tcp
    prefer_udp
    expire DURATION
    max_fails INTEGER
    tls CERT KEY CA
    tls_servername NAME
    policy random|round_robin|sequential
    health_check DURATION [no_rec] [domain FQDN]
    max_concurrent MAX
    next RCODE_1 [RCODE_2] [RCODE_3...]
}

업스트림 DNS 서버에 DNS 메세지 프록시하는 역할

  • FROM : 요청을 전달하기 위해 일치해야하는 도메인 설정
  • TO : 전달할 대상 엔드포인트

forward . /etc/resolv.conf 로 설정시 모든 도메인에 대한 DNS 질의를 /etc/resolv.conf 파일에 정의된 업스트림 DNS 서버로 전달하도록 지정하며, 이를 통해 kubernetes 외부 도메인에 대한 질의를 처리합니다.
 
참고로, coredns 파드의 /etc/resolv.conf는 아래와 같이 설정되어 있습니다.
VPC 대역이 192.168.0.0/16으로 VPC의 nameserver인 192.168.0.2로 설정된 것을 볼 수 있습니다.

 coredns-7b98449c4-tlmtw  ~  cat /etc/resolv.conf
search ap-northeast-2.compute.internal
nameserver 192.168.0.2

 
이제 파드를 생성하여 해당 파드의 설정과 질의 과정을 살펴보겠습니다.
파드 생성 후 파드의 /etc/resolv.conf 파일 설정을 확인해보겠습니다.

 webpod1  ~  cat /etc/resolv.conf
search default.svc.cluster.local svc.cluster.local cluster.local ap-northeast-2.compute.internal
nameserver 10.10.200.10
options ndots:5

search : 나열된 도메인 목록을 차례대로 붙여가며 추가로 DNS 서버를 호출
nameserver : 질의를 전달할 서버, CoreDNS Service의 Cluster IP
options ndots : FQDN으로 처리할 도메인에 포함될 '.'의 최소 개수, default 5
 
노드 및 파드 정보

(⎈|default:N/A) root@k3s-s:~# k get no -owide
NAME     STATUS   ROLES                  AGE    VERSION        INTERNAL-IP      EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION   CONTAINER-RUNTIME
k3s-s    Ready    control-plane,master   102m   v1.30.5+k3s1   192.168.10.10    <none>        Ubuntu 22.04.5 LTS   6.8.0-1015-aws   containerd://1.7.21-k3s2
k3s-w1   Ready    <none>                 102m   v1.30.5+k3s1   192.168.10.101   <none>        Ubuntu 22.04.5 LTS   6.8.0-1015-aws   containerd://1.7.21-k3s2
k3s-w2   Ready    <none>                 102m   v1.30.5+k3s1   192.168.10.102   <none>        Ubuntu 22.04.5 LTS   6.8.0-1015-aws   containerd://1.7.21-k3s2
k3s-w3   Ready    <none>                 102m   v1.30.5+k3s1   192.168.10.103   <none>        Ubuntu 22.04.5 LTS   6.8.0-1015-aws   containerd://1.7.21-k3s2
(⎈|default:N/A) root@k3s-s:~# k get po -owide
NAME      READY   STATUS    RESTARTS   AGE   IP           NODE     NOMINATED NODE   READINESS GATES
webpod1   1/1     Running   0          30m   172.16.3.2   k3s-w1   <none>           <none>
webpod2   1/1     Running   0          30m   172.16.1.3   k3s-w2   <none>           <none>
webpod3   1/1     Running   0          30m   172.16.2.3   k3s-w3   <none>           <none>
(⎈|default:N/A) root@k3s-s:~# k get po -n kube-sysetm -owide
No resources found in kube-sysetm namespace.
(⎈|default:N/A) root@k3s-s:~# k get po -n kube-system -owide
NAME                                      READY   STATUS    RESTARTS   AGE    IP           NODE     NOMINATED NODE   READINESS GATES
coredns-7b98449c4-tlmtw                   1/1     Running   0          102m   172.16.1.2   k3s-w2   <none>           <none>
local-path-provisioner-6795b5f9d8-8qmxr   1/1     Running   0          102m   172.16.0.2   k3s-s    <none>           <none>
metrics-server-cdcc87586-crzps            1/1     Running   0          102m   172.16.2.2   k3s-w3   <none>           <none>
(⎈|default:N/A) root@k3s-s:~# k get svc -n kube-system
NAME             TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                  AGE
kube-dns         ClusterIP   10.10.200.10    <none>        53/UDP,53/TCP,9153/TCP   122m
metrics-server   ClusterIP   10.10.200.244   <none>        443/TCP                  122m
(⎈|default:N/A) root@k3s-s:~# k get svc
NAME            TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
kubernetes      ClusterIP   10.10.200.1     <none>        443/TCP    123m
svc-clusterip   ClusterIP   10.10.200.207   <none>        9000/TCP   51m

 
외부 도메인 질의
webpod1에서 nslookup 실행 시 coredns 파드에 요청이 전달되고, coredns 파드에서는 /etc/resolv.conf에 설정된 nameserver 192.168.0.2에 질의를 하는 것을 확인할 수 있습니다. 해당 통신은 coredns 파드가 배포된 노드에서도 확인할 수 있습니다.

추가로 google.com와 google.com.에 대해 질의를 비교해보겠습니다.
google.com 질의 시 type = A 레코드에 대해 5회의 질의를 하는 것을 확인할 수 있습니다.  

nslookup -debug google.com
Server:		10.10.200.10
Address:	10.10.200.10#53

------------
    QUESTIONS:
	google.com.default.svc.cluster.local, type = A, class = IN
    ANSWERS:
    AUTHORITY RECORDS:
    ->  cluster.local
--
    QUESTIONS:
	google.com.svc.cluster.local, type = A, class = IN
    ANSWERS:
    AUTHORITY RECORDS:
    ->  cluster.local
--
    QUESTIONS:
	google.com.cluster.local, type = A, class = IN
    ANSWERS:
    AUTHORITY RECORDS:
    ->  cluster.local
--
    QUESTIONS:
	google.com.ap-northeast-2.compute.internal, type = A, class = IN
    ANSWERS:
    AUTHORITY RECORDS:
    ->  ap-northeast-2.compute.internal
--
    QUESTIONS:
	google.com, type = A, class = IN
    ANSWERS:
    ->  google.com
	internet address = 142.250.206.238
--
    QUESTIONS:
	google.com, type = AAAA, class = IN
    ANSWERS:
    ->  google.com
	has AAAA address 2404:6800:400a:80e::200e

google.com. 질의 시 type = A 레코드에 대해 1회의 질의를 하는 것을 확인할 수 있습니다.  

  webpod1  ~  nslookup -debug google.com.
Server:		10.10.200.10
Address:	10.10.200.10#53

 ------------
    QUESTIONS:
	google.com, type = A, class = IN
    ANSWERS:
    ->  google.com
	internet address = 142.250.206.238
--
    QUESTIONS:
	google.com, type = AAAA, class = IN
    ANSWERS:
    ->  google.com
	has AAAA address 2404:6800:400a:80e::200e

이는 FQDN으로 간주하는 도메인이냐 아니냐에 따라 질의 횟수가 다르다고 볼 수 있으며, 맨 뒤에 '.'이 있는 도메인은 FQDN으로 인식하여 한번의 질의를 하지만, 그렇지 않은 도메인의 경우 현재 ndots 설정이 5로 되어있기 때문에 '.'이 5개보다 적다면 search에 포함된 도메인들을 차례로 추가하며 질의를 합니다.
질의 횟수가 많으면 CoreDNS에 부하가 생길 수 있고, 더 빠르게 DNS 질의 결과를 받기 위해서는 모든 도메인 뒤에 '.'를 포함하거나, ndots를 1로 설정하는 것이 좋습니다.
해당 설정은 파드의 spec에서 변경이 가능합니다.
 
 
내부 도메인 질의
webpod1에서 nslookup 실행 시 coredns 파드에 요청이 전달됩니다. 외부 도메인 질의와 다르게 VPC nameserver에 별도로 질의를 하지 않습니다.

Pod dnsPolicy

파드 개별로 DNS 정책을 설정할 수 있고 여러 옵션을 살펴보겠습니다.
참고) https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy
 
Default : 노드 설정(ex. /etc/resolv.conf)에 정의된 DNS 서버를 사용하여 DNS 질의하며, CoreDNS, NodeLocal DNS 모두 해당 설정으로 사용
ClusterFirst(기본 옵션) : 클러스터 도메인 접미사(cluster.local 등)와 일치하지 않는 DNS 질의는 업스트림 DNS 서버로 전달하며, dnsPolicy를 지정하지 않았을 때 사용하는 기본 옵션
ClusterFirstWithHostNet : hostNetwork로 실행되는 파드에 사용하며, hostNetwork 파드에서 ClusterFirst를 사용하는 경우 Default 정책을 사용
None : 파드가 클러스터 내 DNS 설정을 무시하고 모든 설정은 파드의 dnsConfig 필드를 통해 제공
 

NodeLocal DNSCache

NodeLocal DNS는 CoreDNS를 보조하는 기능으로, 모든 노드에 배포(DaemonSet)되어 local cache를 사용합니다.
도메인 질의 발생 시 1차적으로 NodeLocal DNS에서 응답하며 해당 도메인이 cache에 있다면 바로 반환하고, cache에 없다면 업스트림 DNS 서버(CoreDNS)에 요청을 전달합니다.
참고) NodeLocal DNSCache의 이점

https://kubernetes.io/docs/tasks/administer-cluster/nodelocaldns/#architecture-diagram

NodeLocal DNS 설치

NodeLocal DNS를 설치하고 DNS 질의의 흐름을 살펴보겠습니다.
참고)

wget https://github.com/kubernetes/kubernetes/raw/master/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml
kubedns=`kubectl get svc kube-dns -n kube-system -o jsonpath={.spec.clusterIP}`
domain='cluster.local'    ## default 값
localdns='169.254.20.10'  ## default 값

## iptables 모드 사용 중으로 아래 명령어 수행
sed -i "s/__PILLAR__LOCAL__DNS__/$localdns/g; s/__PILLAR__DNS__DOMAIN__/$domain/g; s/__PILLAR__DNS__SERVER__/$kubedns/g" nodelocaldns.yaml

## nodelocaldns 설치
kubectl apply -f nodelocaldns.yaml

## nodelocaldns 설치 확인
(⎈|default:N/A) root@k3s-s:~# k get po -n kube-system -owide | grep dns
coredns-7b98449c4-tlmtw                   1/1     Running   0          3h15m   172.16.1.2       k3s-w2   <none>           <none>
node-local-dns-5c958                      1/1     Running   0          14m     192.168.10.101   k3s-w1   <none>           <none>
node-local-dns-g2ss2                      1/1     Running   0          14m     192.168.10.103   k3s-w3   <none>           <none>
node-local-dns-ml8zp                      1/1     Running   0          14m     192.168.10.10    k3s-s    <none>           <none>
node-local-dns-q4qnc                      1/1     Running   0          14m     192.168.10.102   k3s-w2   <none>           <none>

node-local-dns 파드의 로그를 보면, CoreDNS ClusterIP와 local dns IP에 대한 iptables 규칙을 추가하는 것을 볼 수 있습니다.
이를 통해 기존 CoreDNS Service에 대한 요청을 NodeLocal DNS에서 처리하도록 합니다.
 
클러스터 외부 도메인 질의
webpod에서 google.com.으로 질의를 하면, CoreDNS 파드를 거치지 않고 node-local-dns 파드에서 처리되는 것을 확인할 수 있습니다.

 
cache에 없는 경우의 통신 흐름을 확인하기 위해 의도적으로 존재하지 않는 도메인을 질의해보겠습니다.

NodeLocal DNS의 경우 upstream DNS 서버로 요청 시 TCP를 사용하기 때문에 tcpdump 옵션을 수정했습니다.
google.com.xxx에 대해 질의 시 NodeLocal DNS를 통해 CoreDns에 요청하는 것을 확인할 수 있습니다.
 
클러스터 내부 도메인 질의의 경우에도 cache에 있는 경우, 없는 경우에 따라 CoreDNS에 요청 전달 여부는 동일하게 동작하여 따로 tcpdump를 남기지 않았습니다.
 
큰 규모의 클러스터에서는 DNS 질의가 다량 발생하므로 그에 비례하게 CoreDNS 파드 수와 스펙을 증가시킵니다. 
서비스와 파드 수에 따라 CoreDNS 리소스 사용량이 증가할 수 있으므로 모니터링이 필요합니다.
이때 NodeLocal DNS를 사용하면 해당 파드가 배포된 노드의 cache에 의해서 1차적으로 DNS 쿼리가 수행되기 때문에 CoreDNS의 부하를 줄일 수 있습니다.
 
 
참고 자료)

728x90