kubeadm 은 CoreDNS 를 어떻게 설치할까요? 파드는 서비스 도메인을 어떻게 처리할까요? 그리고 설정은 어떻게 할까요? (feat: NodeLocal DNSCache)

들어가며
Kubeadm 의 경우 CoreDNS 및 kube-proxy 를 addon 으로 부르며, 클러스터 프로비저닝 시점에서 자동으로 설치합니다. Kubeadm 을 wrapping 한 Kubespray 또한 마찬가지입니다.
만약 Kubeadm 과 같은 프로비저닝 툴을 사용하지 않고 클러스터를 프로비저닝 했다면 CNI 설치, CoreDNS , kube-proxy의 설치같은 작업은 관리자가 직접 해야합니다.
바닐라 쿠버네티스 프로비저닝 툴 작업을 진행하면서 Kubeadm 은 과연 어떻게 CoreDNS 를 설치하고, 또 어떻게 10.96.0.10
등의 ClusterIP 를 고정해서 사용하게 되는지 궁금하여 Kubeadm 의 코드를 확인해가면서 그 동작을 확인해보았습니다.
그 이후 과연 파드들은 어떻게 nameserver 설정을 디폴트로 갖고 오게 되는지 확인해보고, PodSpec 내의 dnsPolicy, dnsConfig 등의 내용을 확인해보았습니다.
마지막으로는 NodeLocal DNSCache 라는 각 노드별로 설치되는 DNS 캐싱 에이전트에 대해서 설명하고, 설치하는 방법에 대해서 소개합니다.
Kubespary로 클러스터를 프로비저닝 하게 되면 NodeLocal DNSCache 는 디폴트로 설치하게 됩니다. 하지만 Kubeadm 으로 클러스터를 프로비저닝하면 수동으로 설치해야합니다.
CoreDNS
CoreDNS 는 Go 로 작성된 DNS 서버입니다. 다양한 환경에서 사용 가능한 DNS 서버로 보통은 쿠버네티스 클러스터에 설치되어서 클러스터내에서 도메인 네임 resolving 을 위해 사용됩니다.(cluster.local)
Kubernetes Pod 의 쿠버네티스 서비스 디스커버리를 위한 nameserver 설정
쿠버네티스 파드를 띄우면 모든 파드에는 /etc/resolv.conf
파일이 있고. 이 파일에는 nameserver
정보가 들어있습니다. kubeadm 으로 프로비저닝 한 클러스터 내의 파드의 /etc/resolv.conf
를 한번 확인해보겠습니다.
$ kubectl run -i -t --rm nginx --image=nginx --restart=Never – sh

$ cat /etc/resolv.conf
를 수행한 결과nginx-0
에 대한 요청이 들어오면 nginx-0.default.svc.cluster.local
에 대해서 resolve 되는지 확인하고, 실패하면 nginx-0.svc.cluster.local
로 시도하고 하는 식이다.확인해보면 nameserver 10.96.0.10
이라는 값이 들어가있는 것을 볼 수 있습니다. 이 네임서버는 어디에 있는 것일까요?
클러스터 내에 설치된 coreDNS
의 서비스 오브젝트를 확인해보겠습니다.
$ kubectl get svc -n kube-system

kube-dns
라는 이름의 서비스 오브젝트를 확인 할 수 있습니다. 위 서비스 오브젝트의 ClusterIP
를 확인해보면 아까 nameserver 10.96.0.10
에서 본것과 동일하다는 것을 알 수 있습니다.
그럼 이 파드별로 들어가게 될 /etc/resolv.conf
는 어떻게 지정이 되었고, kubeadm 을 사용하면서 별도로 지정한적도 없는 coreDNS(혹은 kube-dns)의 ClusterIP 는 어떻게 10.96.0.10 으로 고정이 되었을까요?
Kubelet 의 clusterDNS 설정
각 파드별로 생성될 /etc/resolv.conf
의 정보는 kubelet configuration
에서의 clusterDNS
항목을 통해 설정이 됩니다. kubelet config
파일을 확인해보면

clusterDNS
항목에 클러스터 Domain Name 을 처리하기 위한 nameserver 리스트를 넣을 수 있도록 되어있음을 확인 할 수 있습니다. (Customizing DNS Service )
기본적으로 node 의 kubelet 의 clusterDNS
항목을 상속하지만, 파드별로 별도로 수정해야 할 필요가 있다면 podSpec 내의 dnsPolicy
및 dnsConfig
를 이용해 변경 가능합니다.
Pod 의 DNS Policy 는 파드별로 설정이 가능합니다. 여기에 설정 가능한 값으로는 Default
, ClusterFirst
, ClusterFirstWithHostNet
, None
등이 설정 가능합니다. 기본적으로 명시하지 않고 파드를 생성하면 ClusterFirst
로 설정됩니다.
ClusterFirst
로 지정하면 cluster.local
과 같은 suffix 가 아닌 www.google.com
과 같은 DNS 쿼리는 DNS 서버의 업스트림 네임서버로 넘어가도록 하는, 기본적으로 CoreDNS(kube-dns) 를 사용하는 옵션입니다.
Default
로 지정하면 해당 파드가 떠있는 노드의 /etc/resolv.conf
설정을 그대로 사용합니다. 따라서 이렇게 지정하고 cluster.local
suffix 를 가진 DNS 쿼리를 하게 되었을 때, 노드 에 적절한 네임서버가 설정되지 않은경우 처리되지 않을 수 있습니다.
ClusterFirstWithHostNet
은 파드의 hostNetwork: true
설정을 했을 때 사용해야하며, 만약 hostNetwork: true
설정을 했지만 , ClusterFirst
로 설정하거나, 명시적으로 설정하지 않으면 Default
설정으로 fallback 하게 됩니다.
None
의 경우는 해당 파드가 떠있는 쿠버네티스 환경에서 상속받는 모든 DNS 설정을 무시하고, dnsConfig
를 통해 DNS 설정을 커스텀 할 때 사용합니다.
만약 Pod 내의 DNS 설정을 직접 하고 싶다면, dnsConfig 를 통해 namserver , searches, options 등을 지정 할 수 있습니다.
apiVersion: v1
kind: Pod
metadata:
name: dnsutils
namespace: hayden
spec:
containers:
- name: dnsutils
image: registry.k8s.io/e2e-test-images/jessie-dnsutils:1.3
command:
- sleep
- "infinity"
imagePullPolicy: IfNotPresent
restartPolicy: Always
dnsConfig:
nameservers:
- 10.96.0.10
- 123.123.123.123
- 8.8.8.8
searches:
- consul.local
위와 같이 dnsConfig 를 설정하고 실제 생성된 Pod 내에서 /etc/resolv.conf
를 확인해보면

CoreDNS(kube-dns)의 IP 주소는 어떻게 결정되었을까요?
kubeadm
에서는 kubernetes/cmd/kubeadm/app/phases/addons/dns/manifests.go at 1d5589e4910ed859a69b3e57c25cbbd3439cd65f · kubernetes/kubernetes 여기서 .DNSIP
라는 변수를 통해서 받아서 CoreDNS 의 ClusterIP 주소를 지정하는데요.
위 코드부분을 보면 알겠지만, Service Object 의 CIDR 블록에서 10번째의 IP 주소를 사용하도록 하고있습니다. 그래서 kubeadm 으로 클러스터를 프로비저닝하면 DNS 를 위한 서비스 오브젝트의 ClusterIP 주소는 10.96.0.10 과 같이 10번째 IP 주소로 고정이 되는것입니다.
이것을 kubelet 의 clusterDNS 항목에도, CoreDNS 의 ClusterIP 항목에도 넣어서 일치시켜주는것입니다.
kubeadm 을 사용하지 않고 쿠버네티스 클러스터를 프로비저닝 한 경우(Vanilia) 위와 같은 형태로 사전에 CoreDNS(혹은 kube-dns) 서비스에 대한 ClusterIP 를 미리 계획하고, Kubelet 의 clusterDNS
항목에 설정해줘야 별다른 설정을 하지 않은 파드들이 올바르게 서비스 도메인에 대한 DNS 쿼리 Resolve 를 수행 할 수 있습니다.
Kubeadm 은 어떻게 CoreDNS 를 설치하나요?
$ kubeadm init
으로 컨트롤 플레인 노드를 최초로 생성하는 시점에서 https://github.com/kubernetes/kubernetes/blob/bce55b94cdc3a4592749aa919c591fa7df7453eb/cmd/kubeadm/app/cmd/init.go#L106-L192
혹은 $ kubeadm init phase addon
명령을 수행하는 시점에서 (https://kubernetes.io/docs/reference/setup-tools/kubeadm/kubeadm-init-phase/#cmd-phase-addon)
kubeadm workflow runner 에 아래 코드와 같은 명령어를 등록하게 됩니다.
여기서 runCoreDNSAddon
은 아래 코드와 같은것을 수행하고
여기에서의 EnsureDNSAddon는 아래쪽 코드에 정의되어있고
전반적으로 사용하는 템플릿 manifests 는 아래에 정의되어있습니다.
쉽게 추상화해보면, kubeadm init
을 수행하면 여러 phases
를 수행하는데, 그 중 addon phase
가 있고, 별도로 kubeadm init phase addon
으로 해당 phase
만 수행이 가능하기도 합니다.
addon phase
에서는 사전에 정의된 manifests 를 템플리팅하여서 kubeadm 설정에 맞게 manifests를 렌더링하고 그것을 쿠버네티스 클러스터에 반영하는 형태로 CoreDNS
를 설치하고있습니다. (kube-proxy 도 마찬가지입니다.)
NodeLocal DNSCache
Using NodeLocal DNSCache in Kubernetes Clusters
kubeadm
으로 프로비저닝 한 클러스터는 CoreDNS 정도만 설치하는 반면, kubeadm
을 wrapping 한 kubespray
의 경우 기본적으로 NodeLocal DNSCache
를 설치하도록 되어있습니다.
NodeLocal DNSCache
는 각 노드별로 DNS 캐싱 에이전트를 DaemonSet
으로 실행하여 클러스터의 DNS 성능을 향상시키기 위해 사용합니다.
쿠버네티스에서 내부 파드간 통신은 대부분 서비스 오브젝트를 통해서 하게 되는데, 이 때 DNS Query 과정은 Pod
가 로컬의 /etc/resolv.conf
설정을 보고 네임서버에 DNS Query 를 하게 되고. 해당 설정에는 보통 CoreDNS
에 대한 서비스 오브젝트 ClusterIP 가 지정되어있습니다. 이 ClusterIP
에 대해서 요청하게되면 내부적으로는 kube-proxy
가 추가한 iptables rule
에 의해 실제 CoreDNS(kube-dns) 엔드포인트로 변환되는 과정을 거치게 됩니다.
만약 각 노드별로 쿠버네티스 클러스터 도메인에 대한 DNS 정보를 캐싱한 데몬셋이 있는 경우 이 iptables 의 DNAT 규칙, connection tracking 을 피하고 단순히 각 노드의 로컬에 있는 DNS 캐시 서버에 질의를 하는 방식으로 단순화 되고, 만약 각 로컬에 있는 DNS 캐싱 에이전트가 Cache Miss 되는 경우에만 CoreDNS(kube-dns)에 캐싱 에이전트가 대신 DNS 정보를 업데이트 하는 식으로 구조가 간단하게 바뀝니다.

NodeLocal DNSCache
는 클러스터의 서비스 오브젝트 IP 대역과 충돌하지 않는 대역의 IP 를 기반으로 사용되고, 권장되는 대역은 169.254.0.0/16
입니다. 보통은 169.254.xx.10
를 주로 사용합니다. NodeLocal DNSCache 설치 방법
Using NodeLocal DNSCache in Kubernetes Clusters
만약 기존 클러스터에 NodeLocal DNSCache
를 설치하고 싶다면 아래와 같은 방식으로 구성해야 합니다.
우선 kubernetes/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml at master · kubernetes/kubernetes 주소의 yaml 파일을 내려받습니다.
# Copyright 2018 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
apiVersion: v1
kind: ServiceAccount
metadata:
name: node-local-dns
namespace: kube-system
labels:
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
---
apiVersion: v1
kind: Service
metadata:
name: kube-dns-upstream
namespace: kube-system
labels:
k8s-app: kube-dns
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
kubernetes.io/name: "KubeDNSUpstream"
spec:
ports:
- name: dns
port: 53
protocol: UDP
targetPort: 53
- name: dns-tcp
port: 53
protocol: TCP
targetPort: 53
selector:
k8s-app: kube-dns
---
apiVersion: v1
kind: ConfigMap
metadata:
name: node-local-dns
namespace: kube-system
labels:
addonmanager.kubernetes.io/mode: Reconcile
data:
Corefile: |
__PILLAR__DNS__DOMAIN__:53 {
errors
cache {
success 9984 30
denial 9984 5
}
reload
loop
bind __PILLAR__LOCAL__DNS__ __PILLAR__DNS__SERVER__
forward . __PILLAR__CLUSTER__DNS__ {
force_tcp
}
prometheus :9253
health __PILLAR__LOCAL__DNS__:8080
}
in-addr.arpa:53 {
errors
cache 30
reload
loop
bind __PILLAR__LOCAL__DNS__ __PILLAR__DNS__SERVER__
forward . __PILLAR__CLUSTER__DNS__ {
force_tcp
}
prometheus :9253
}
ip6.arpa:53 {
errors
cache 30
reload
loop
bind __PILLAR__LOCAL__DNS__ __PILLAR__DNS__SERVER__
forward . __PILLAR__CLUSTER__DNS__ {
force_tcp
}
prometheus :9253
}
.:53 {
errors
cache 30
reload
loop
bind __PILLAR__LOCAL__DNS__ __PILLAR__DNS__SERVER__
forward . __PILLAR__UPSTREAM__SERVERS__
prometheus :9253
}
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-local-dns
namespace: kube-system
labels:
k8s-app: node-local-dns
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
spec:
updateStrategy:
rollingUpdate:
maxUnavailable: 10%
selector:
matchLabels:
k8s-app: node-local-dns
template:
metadata:
labels:
k8s-app: node-local-dns
annotations:
prometheus.io/port: "9253"
prometheus.io/scrape: "true"
spec:
priorityClassName: system-node-critical
serviceAccountName: node-local-dns
hostNetwork: true
dnsPolicy: Default # Don't use cluster DNS.
tolerations:
- key: "CriticalAddonsOnly"
operator: "Exists"
- effect: "NoExecute"
operator: "Exists"
- effect: "NoSchedule"
operator: "Exists"
containers:
- name: node-cache
image: registry.k8s.io/dns/k8s-dns-node-cache:1.23.0
resources:
requests:
cpu: 25m
memory: 5Mi
args: [ "-localip", "__PILLAR__LOCAL__DNS__,__PILLAR__DNS__SERVER__", "-conf", "/etc/Corefile", "-upstreamsvc", "kube-dns-upstream" ]
securityContext:
capabilities:
add:
- NET_ADMIN
ports:
- containerPort: 53
name: dns
protocol: UDP
- containerPort: 53
name: dns-tcp
protocol: TCP
- containerPort: 9253
name: metrics
protocol: TCP
livenessProbe:
httpGet:
host: __PILLAR__LOCAL__DNS__
path: /health
port: 8080
initialDelaySeconds: 60
timeoutSeconds: 5
volumeMounts:
- mountPath: /run/xtables.lock
name: xtables-lock
readOnly: false
- name: config-volume
mountPath: /etc/coredns
- name: kube-dns-config
mountPath: /etc/kube-dns
volumes:
- name: xtables-lock
hostPath:
path: /run/xtables.lock
type: FileOrCreate
- name: kube-dns-config
configMap:
name: kube-dns
optional: true
- name: config-volume
configMap:
name: node-local-dns
items:
- key: Corefile
path: Corefile.base
---
# A headless service is a service with a service IP but instead of load-balancing it will return the IPs of our associated Pods.
# We use this to expose metrics to Prometheus.
apiVersion: v1
kind: Service
metadata:
annotations:
prometheus.io/port: "9253"
prometheus.io/scrape: "true"
labels:
k8s-app: node-local-dns
name: node-local-dns
namespace: kube-system
spec:
clusterIP: None
ports:
- name: metrics
port: 9253
targetPort: 9253
selector:
k8s-app: node-local-dns
파일 명은 nodelocaldns.yaml
로 합니다. (아래쪽의 sed 명령어의 타겟 파일명으로 사용합니다)
kubedns=`kubectl get svc kube-dns -n kube-system -o jsonpath={.spec.clusterIP}`
domain=<cluster-domain>
localdns=<node-local-address>
위와 같은 명령어를 수행해서 올바른 값을 넣습니다. 대부분의 경우
kubedns=10.96.0.10 (서비스 오브젝트의 CIDR 대역에서의 10번째 값)
domain=cluster.local
localdns=169.254.25.10
으로 설정하면 됩니다.
만약 kube-proxy
가 IPTABLES 모드로 실행되고있다면
sed -i "s/__PILLAR__LOCAL__DNS__/$localdns/g; s/__PILLAR__DNS__DOMAIN__/$domain/g; s/__PILLAR__DNS__SERVER__/$kubedns/g" nodelocaldns.yaml
IPVS 모드로 실행되고 있다면
sed -i "s/__PILLAR__LOCAL__DNS__/$localdns/g; s/__PILLAR__DNS__DOMAIN__/$domain/g; s/,__PILLAR__DNS__SERVER__//g; s/__PILLAR__CLUSTER__DNS__/$kubedns/g" nodelocaldns.yaml
$ brew install gnu-sed
를 설치하고. sed 명령어대신 gsed 로 위 명령어를 수행합니다.위 명령어들은 ConfigMap 을 수정하기 위한 명령어입니다.
apiVersion: v1
kind: ConfigMap
metadata:
name: node-local-dns
namespace: kube-system
labels:
addonmanager.kubernetes.io/mode: Reconcile
data:
Corefile: |
__PILLAR__DNS__DOMAIN__:53 {
errors
cache {
success 9984 30
denial 9984 5
}
reload
loop
bind __PILLAR__LOCAL__DNS__ __PILLAR__DNS__SERVER__
forward . __PILLAR__CLUSTER__DNS__ {
force_tcp
}
prometheus :9253
health __PILLAR__LOCAL__DNS__:8080
}
in-addr.arpa:53 {
errors
cache 30
reload
loop
bind __PILLAR__LOCAL__DNS__ __PILLAR__DNS__SERVER__
forward . __PILLAR__CLUSTER__DNS__ {
force_tcp
}
prometheus :9253
}
ip6.arpa:53 {
errors
cache 30
reload
loop
bind __PILLAR__LOCAL__DNS__ __PILLAR__DNS__SERVER__
forward . __PILLAR__CLUSTER__DNS__ {
force_tcp
}
prometheus :9253
}
.:53 {
errors
cache 30
reload
loop
bind __PILLAR__LOCAL__DNS__ __PILLAR__DNS__SERVER__
forward . __PILLAR__UPSTREAM__SERVERS__
prometheus :9253
}
여기서 __PILLAR__LOCAL__DNS__
는 각 노드에서 이 DNS Cache 에이전트가 어떤 IP 로 바인딩 될지를 결정하는 부분이고. 우리는 여기서 169.254.25.10
으로 대치하게 됩니다.
__PILLAR__DNS__SERVER__
의 경우 CoreDNS(kube-dns) 의 ClusterIP 로 대치됩니다.
__PILLAR__CLUSTER__DNS__
및 __PILLAR__UPSTREAM__SERVERS__
는 node-local-dns-cache 데몬셋 파드 내부에서 동적으로 대치하는 영역인데, __PILLAR__CLUSTER__DNS__
는 이 yaml 파일을 사용해서 생성하는 kube-dns-upstream
서비스 오브젝트의 ClusterIP 입니다.
여기서 유의해야하는점은 kube-dns-upstream
서비스 오브젝트의 레이블 셀렉터로 k8s-app=kube-dns
로 되어있기 때문에 CoreDNS 를 수동으로 설치한 경우 레이블에 k8s-app=kube-dns
를 추가해줘야합니다.
(만약 CoreDNS 를 helm 으로 설치했다면 values 에 k8sAppLabelOverride: "kube-dns"
라는 값을 사용하면 됩니다)
__PILLAR__UPSTREAM__SERVERS__
의 경우 __PILLAR__LOCAL__DNS__
값과 CoreDNS(kube-dns) ClusterIP 값으로 평가되어서 들어가게 됩니다.
sed 명령어를 사용하지 않고 직접 수정할 경우 아래 값들을 직접 대치하면 됩니다.
__PILLAR__DNS__DOMAIN__
: cluster.local 등의 클러스터 도메인
__PILLAR__LOCAL__DNS__
: 169.254.25.10 등의 node local dns cache 데몬셋 파드가 Listen 할 IP 지정
__PILLAR__DNS__SERVER__
: CoreDNS(kube-dns) 의 ClusterIP
이렇게 해서 node-local-dns
라는 이름의 데몬셋이 성공적으로 배포되고 나면, 실제 파드들이 node-local-dns
파드로 DNS 쿼리를 하도록 변경해야 합니다.
KubeletConfiguration
의 clusterDNS
정보를 업데이트하고, kubelet 재시작을 함으로서 새로 생성되는 파드들에 대해서 node-local-dns
에 클러스터 도메인에 대한 DNS 쿼리를 하도록 설정 할 수 있습니다.
Wrapping up
쿠버네티스의 Pod 가 어떻게 클러스터 도메인에 대한 DNS Query Resolve 를 하는지 알아보면서, CoreDNS 에 대해서 알아보고, kubeadm 은 그것을 어떻게 설치하는지, 그리고 각 파드별 DNS 설정은 어떻게 하는지, 더 나아가서 NodeLocal DNSCache 까지 다양하게 다루게 되었습니다.
kubadm / kubespray 등의 프로비저닝 툴은 기본적으로 kube-proxy, CoreDNS 등의 애드온을 알아서 설치하고 처리해줍니다. 하지만 Vanilia Kubernetes 클러스터를 구축하는 경우에는 그런것들을 모두 직접 설정해줘야 합니다.
kubeadm 에서 CoreDNS 를 어떻게 설치하고, CoreDNS 의 ClusterIP 는 어떻게 설정된것인지 알아보면서 왜 10.96.0.10
과 같은 IP 주소가 설정되었는지 알 수 있었고. 더 나아가서 kubelet 과도 긴밀히 연계되어있음을 알 수 있었습니다.