5.3 DNS와 서비스 디스커버리
Service가 고정된 가상 IP를 제공해도, 그 IP를 클라이언트가 어떻게 알아내는가 하는 문제는 남는다. 매니페스트마다 ClusterIP를 하드코딩하면 Service를 다시 만들 때마다 전부 고쳐야 하므로 Pod IP를 하드코딩하던 시절과 다를 것이 없다. Kubernetes의 답은 이름이다. 클러스터 안에 DNS 서버를 두고 모든 Service에 규칙적인 도메인 이름을 부여해서, 애플리케이션은 web이나 db.storage 같은 이름만 알면 되게 한다.
CoreDNS 구조와 Corefile
클러스터 DNS의 표준 구현은 CoreDNS다. kube-system namespace에서 Deployment로 동작하고, 그 앞에 kube-dns라는 이름의 ClusterIP Service가 있다. 이름이 kube-dns인 것은 이전 세대 구현의 이름을 호환성 때문에 그대로 물려받은 결과다. kubelet은 Pod를 만들 때 이 Service의 IP를 컨테이너의 /etc/resolv.conf에 nameserver로 기록하고, 그 결과 Pod 안의 모든 이름 조회가 CoreDNS로 향한다.
CoreDNS의 동작은 플러그인 체인 설정인 Corefile이 정의한다. kubeadm 기본 구성은 대략 이런 모양이다.
.:53 {
errors
health
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}중심은 두 플러그인이다. kubernetes 플러그인은 apiserver에서 Service와 EndpointSlice를 감시하면서 cluster.local 도메인의 레코드를 메모리에서 직접 응답한다. forward 플러그인은 클러스터 도메인이 아닌 질의를 상위 DNS(기본은 CoreDNS Pod가 뜬 노드의 resolv.conf)로 넘긴다. 클러스터 내부 이름은 즉답하고 외부 이름은 중계하는 구조다. Corefile은 ConfigMap으로 관리되므로 특정 도메인만 사내 DNS로 보내는 조건부 포워딩 같은 변경을 kubectl -n kube-system edit configmap coredns로 적용한다.
이름 규칙
Service가 만들어지면 다음 형태의 A 레코드(IPv6이면 AAAA)가 생긴다.
<service>.<namespace>.svc.cluster.local일반 Service는 이 이름이 ClusterIP 하나로 풀리고, headless Service는 대상 Pod들의 IP 목록으로 풀린다. StatefulSet과 headless Service를 조합하면 <pod>.<service>.<namespace>.svc.cluster.local 형태로 Pod 개별 레코드까지 생긴다. named port를 선언했다면 _<port>._<protocol>.<service>... 형태의 SRV 레코드로 포트 번호까지 조회된다.
전체 이름을 항상 쓸 필요는 없다. 같은 namespace의 Service는 web처럼 이름만으로, 다른 namespace의 Service는 db.storage처럼 두 단계로 조회한다. 이 축약이 동작하는 원리는 resolv.conf의 search 도메인이다.
Pod 자체에도 172-17-0-3.default.pod.cluster.local처럼 IP를 대시로 바꾼 레코드가 있지만, IP를 알아야 이름을 만들 수 있는 구조라 실용성은 낮다.
dnsPolicy
Pod가 어느 DNS 서버를 보게 되는지는 Pod spec의 dnsPolicy가 정한다.
| 값 | resolv.conf 내용 |
|---|---|
| ClusterFirst | CoreDNS를 nameserver로 지정 (기본값) |
| Default | 노드의 resolv.conf를 그대로 상속 |
| ClusterFirstWithHostNet | hostNetwork Pod에서 ClusterFirst와 동일 효과 |
| None | 비우고 dnsConfig로 전부 수동 지정 |
주의할 점은 이름과 실제의 어긋남이다. 기본값은 Default가 아니라 ClusterFirst다. hostNetwork: true인 Pod는 자동으로 Default처럼 동작해 클러스터 DNS를 보지 못하므로, 그런 Pod가 Service 이름을 조회해야 한다면 ClusterFirstWithHostNet을 명시해야 한다. None은 dnsConfig 필드와 조합해 nameserver, search, options를 전부 직접 지정하는 값이다.
ndots:5와 외부 도메인 조회 폭증
ClusterFirst Pod의 resolv.conf를 실제로 열어 보면 다음과 같다 (default namespace 기준).
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5search는 web 한 단어를 web.default.svc.cluster.local로 완성해 주는 접미사 목록이다. ndots는 그 적용 기준으로, 질의한 이름의 점 개수가 이 값보다 적으면 절대 이름 조회보다 search 접미사를 붙인 조회를 먼저 시도한다. 클러스터 내부 축약 이름이 동작하는 것은 이 두 줄 덕분이다.
문제는 외부 도메인이다. api.example.com은 점이 2개라 ndots:5 기준에 미달하므로, resolver는 절대 이름을 조회하기 전에 search 목록을 전부 순회한다.
search 접미사 시도
api.example.com.default.svc.cluster.local, api.example.com.svc.cluster.local, api.example.com.cluster.local을 차례로 조회하고 전부 NXDOMAIN을 받는다.
절대 이름 조회
마지막에야 api.example.com을 조회해 답을 얻는다.
A와 AAAA를 함께 조회하는 일반적인 환경에서는 외부 도메인 하나를 푸는 데 DNS 질의가 8회 발생한다. 외부 API를 초당 수백 번 호출하는 워크로드라면 CoreDNS에 실제 필요의 몇 배가 되는 질의가 몰리고, 그중 대부분이 의미 없는 NXDOMAIN이다.
완화 방법은 조회를 절대 이름으로 만들거나 기준을 낮추는 것이다. 도메인 끝에 점을 붙인 api.example.com.은 FQDN으로 간주되어 search 순회 없이 즉시 조회된다. 애플리케이션 설정에서 끝점을 지원한다면 가장 값싼 해결책이다. 아니면 Pod 단위로 ndots를 낮춘다.
spec:
dnsConfig:
options:
- name: ndots
value: "2"ndots:2면 점 2개 이상인 외부 도메인이 곧바로 절대 이름으로 조회된다. web이나 db.storage 같은 내부 축약 이름은 여전히 search를 통해 풀린다. 이와 별개로 각 노드에 DNS 캐시를 두는 NodeLocal DNSCache 애드온으로 CoreDNS까지 가는 질의량 자체를 줄이는 방법도 있다.
svc.cluster.local 접미사를 생략한 서너 단계짜리 내부 이름을 쓰는 애플리케이션이 있는지 확인한 뒤 적용한다.정리
- CoreDNS가 Service와 EndpointSlice를 감시해
cluster.local레코드를 즉답하고, 외부 질의는 상위 DNS로 forward한다. 동작 정의는 Corefile(ConfigMap)에 있다. - Service 이름은
<service>.<namespace>.svc.cluster.local규칙을 따르고, 같은 namespace에서는 Service 이름만으로 조회된다. - dnsPolicy 기본값은 ClusterFirst이며, hostNetwork Pod에는 ClusterFirstWithHostNet을 명시해야 클러스터 DNS가 보인다.
- ndots:5와 search 도메인의 조합은 외부 도메인 조회를 여러 배로 부풀린다. FQDN 끝점 또는 dnsConfig의 ndots 조정으로 완화한다.