"web"이라고만 쳤는데 왜 다른 namespace로 가버리나 — CoreDNS와 NDots 함정

한 팀이 클러스터 안에서 외부 API(api.weather.com)를 호출하는 코드를 올렸다. 어느 날부터 호출이 이상하게 느려지더니, 가끔 존재하지 않는 도메인(NXDOMAIN) 응답이 왔다. 원인은 Pod의 DNS 검색 도메인 설정 — ndots:5 — 이었다. 외부 도메인을 쳤는데 CoreDNS가 그것을 api.weather.com.default.svc.cluster.local 같은 클러스터 내부 이름으로 먼저 풀려 시도하다 실패하고, 그제야 외부로 나갔다. 매 요청마다 여러 번의 실패한 DNS 질의가 쌓인 것이다.

이 글이 푸는 것은: CoreDNS가 클러스터 안에서 서비스 디스커버리를 어떻게 담당하고, 왜 단일 레이블 이름("web")이 의도치 않게 다른 namespace로 가는가, 그리고 ndots 함정을 어떻게 피하는가다.

클라이언트는 ClusterIP를 어떻게 아나 — 클러스터 안에 DNS가 필요하다

01장에서 Service는 고정된 ClusterIP를 준다고 했다. 그런데 클라이언트 Pod는 그 ClusterIP 숫자를 어떻게 알까? IP를 하드코딩할 수도 있지만, Service가 재생성되면 ClusterIP도 바뀔 수 있다. 이름으로 불러야 한다 — web이라고 치면 그 Service의 IP가 나와야 한다.

일반적인 DNS(8.8.8.8, 회사 DNS)로는 안 된다. 그 DNS들은 web.default.svc.cluster.local 같은 클러스터 내부 이름을 모른다 — 클러스터 안에만 존재하는 Service이니까. 그래서 클러스터 안에는 클러스터 내부 이름을 아는 전용 DNS가 필요하다. 그 역할을 CoreDNS가 한다. (Kubernetes docs - DNS for Services and Pods)

CoreDNS는 kube-dns라는 Service 뒤에서 Deployment로 돈다. 각 Pod는 기본 DNS 설정으로 이 CoreDNS를 이름 풀이기로 쓴다. (CoreDNS - Kubernetes plugin)

flowchart LR
    APP["Pod의 앱"] -->|"DNS 질의: web"| RES["resolv.conf<br/>nameserver: CoreDNS"]
    RES --> CDNS["CoreDNS"]
    CDNS -->|"클러스터 내부 이름"| SVC["Service ClusterIP"]
    CDNS -. 모르는 이름 .-> UP["상위 DNS<br/>(외부 도메인)"]
# Kubernetes 1.36 — CoreDNS 위치 확인
kubectl get svc -n kube-system kube-dns
kubectl get deploy -n kube-system coredns

확인할 것: kube-dns Service가 CoreDNS Pod들을 가리키고, 클러스터 DNS 역할을 한다.

CoreDNS는 쿼리를 두 갈래*로 나눈다. 클러스터 내부 이름(`.svc.cluster.local` 등)은 자기가 풀고, 모르는 이름(외부 도메인)은 상위 DNS로 넘긴다(forward). 한 서버가 두 역할을 하되, 아는 것만 직접 답하고 나머지는 위임하는 구조.

Service DNS 이름 구조 — 그리고 단일 이름의 위험

클라이언트는 Service를 짧은 이름으로 부를 수 있다 — 같은 namespace에선 web만 쳐도 CoreDNS가 검색 도메인(search domain)을 붙여 전체 이름으로 풀어준다. Service의 DNS 이름은 <service>.<namespace>.svc.cluster.local 구조인데, 이 짧은 이름 편의가 위험을 숨기고 있다.

이 단일 이름 편의가 위험을 숨기고 있다. 다른 namespace에도 같은 이름 web Service가 있다면? CoreDNS는 검색 도메인 순서대로 시도하므로 현재 namespaceweb을 먼저 풀지만 — 현재 namespace에 web이 없으면 검색 도메인 구성에 따라 다른 namespace로 갈 수 있다. 이것이 "단일 레이블 이름의 위험"이다.

실무 권장: 항상 완전한 이름(FQDN)이나 최소 <service>.<namespace>로 부른다. web이 아니라 web.default 또는 web.default.svc.cluster.local. 그래야 namespace 혼란이 없다.

ndots 함정 — 왜 외부 도메인이 느려지나

Pod의 /etc/resolv.conf를 보면:

nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

ndots:5의 의미: 이름에 점(dot)이 5개 미만이면, 먼저 검색 도메인을 붙여서 풀어본다. 이 정책은 내부 Service 이름(web, 점 0개)을 자동으로 클러스터 이름으로 확장하기 위해서다. 문제는 외부 도메인이 이 정책에 같이 걸린다는 것. api.weather.com은 점이 2개(< 5)이므로 CoreDNS는 이것을 내부 이름으로 먼저 풀려 한다:

  1. api.weather.com.default.svc.cluster.local → 실패 (NXDOMAIN)
  2. api.weather.com.svc.cluster.local → 실패
  3. api.weather.com.cluster.local → 실패
  4. 그제야 api.weather.com 그대로 → 상위 DNS로 → 성공

매 외부 호출마다 3번의 실패한 내부 질의가 붙는다. 트래픽이 많으면 CoreDNS 부하와 지연이 누적된다. 도입의 사례가 바로 이것이었다.

해결책:

  • 외부 도메인은 끝에 점을 붙여 FQDN으로: api.weather.com.(마지막 점이 "절대 이름"을 나타내어 검색 도메인을 안 붙이게 함).
  • 또는 Pod의 dnsConfigndots를 낮춤(예: ndots:1).
  • 또는 성능에 민감한 외부 호출은 Pod에 dnsPolicy: Default를 줘(클러스터 검색 도메인 없이 노드 DNS만).
# Kubernetes 1.36
spec:
  dnsConfig:
    options: [{name: ndots, value: "1"}]

ndots를 낮추면 반대로 내부 Service 단일 이름 풀이가 느려질 수 있다(점이 적어 절대 이름으로 간주되면 검색 도메인을 안 붙임). trade-off가 있다. 가장 안전한 건 완전한 이름(FQDN)을 쓰는 것이다 — 그러면 ndots 정책에 상관없이 일관되게 동작한다.

headless Service와 Pod DNS — 이름이 곧 Pod

06장에서 headless Service(clusterIP: None)가 각 Pod의 IP를 DNS로 노출한다고 했다. CoreDNS가 그 일을 한다. 차이를 보면 CoreDNS의 역할이 선명해진다:

대상 DNS 질의 결과
일반 Service <svc>.<ns>.svc.cluster.local 단일 ClusterIP (가상 IP)
headless Service <svc>.<ns>.svc.cluster.local Pod IP 목록 (클라이언트가 고름)
StatefulSet Pod <pod-ordinal>.<svc>.<ns>.svc.cluster.local 해당 Pod IP

이것이 StatefulSet의 "안정된 이름"이 실제로 작동하는 기전이다 — CoreDNS가 그 이름을 Pod IP로 풀어준다. 클라이언트는 web-0.nginx.default.svc.cluster.local로 특정 Pod를 직접 찾을 수 있다.

Corefile — CoreDNS 동작을 바꾸려면

CoreDNS의 기본 동작(어느 도메인을 어떻게 풀지, 외부를 어디로 forward할지)을 바꾸려면 Corefile을 고친다. Corefile은 kube-system namespace의 coredns ConfigMap에 들어있고, 플러그인 조합으로 동작을 유연하게 구성한다:

.:53 {                                    # 모든 도메인, 53번 포트
    errors                                 # 에러 로그
    health :8080                           # 헬스 엔드포인트
    kubernetes cluster.local in-addr.arpa ip6.arpa {
        pods insecure                      # Pod DNS 응답 모드
        fallthrough                        # 못 찾으면 다음 플러그인으로
    }
    prometheus :9153                       # 메트릭 노출
    forward . /etc/resolv.conf             # 클러스터 외부는 노드 DNS로
    cache 30                               # 30초 TTL 캐시
    loop                                  # DNS 루프(무한 전달) 감지
    reload                                # Corefile 변경 시 자동 리로드
    loadbalance                           # 응답 IP 순서 회전(라운드로빈)
}

각 줄이 플러그인이다. kubernetes 플러그인이 Service→IP 해석을 담당(핵심). forward가 외부 도메인을 상위 DNS로 넘긴다. cache가 응답 캐시로 반복 질의를 절약한다. 이 플러그인 구조가 CoreDNS의 유연성 — 필요한 것만 끼워 쓴다.

Corefile 변경은 클러스터 전체 DNS 동작에 영향을 주므로 신중해야 한다. 다행히 reload 플러그인이 Corefile 변경을 감지해 자동 리로드하므로, CoreDNS Pod를 재시작할 필요는 없다. 하지만 잘못된 Corefile은 클러스터 전체의 이름 풀이를 멈춘다.

외부 서비스를 클러스터 이름으로 — 커스텀 영역

Corefile에 별도 영역을 추가하면 외부 서비스를 클러스터 내부 이름처럼 부를 수 있다:

# 회사 내부 DNS를 클러스터에서 풀
corp.local:53 {
    forward . 10.10.0.53      # 회사 내부 DNS로
}

이제 Pod가 db.corp.local을 부르면 회사 내부 DNS가 풀어준다. 클라우드와 온프렘 사설망 서비스를 클러스터에서 자연스럽게 부르는 패턴이다.

NodeLocal DNS Cache — 대규모에서 CoreDNS 병목 풀기

대규모 클러스터에서 모든 Pod가 CoreDNS Pod에 직접 질의하면 CoreDNS가 병목이다. Pod 수천 개가 각자 여러 번 질의하면 CoreDNS Pod 몇 개가 감당해야 한다.

NodeLocal DNS Cache가 이를 푼다 — 각 노드마다 DNS 캐시 컨테이너(DaemonSet)를 띄워, Pod의 DNS 질의를 노드 로컬에서 먼저 처리한다. 캐시 hit면 CoreDNS에 안 가고 즉시 응답, 캐시 miss만 CoreDNS로 간다. (Kubernetes docs - NodeLocal DNSCache)

flowchart LR
    P["Pod DNS 질의"] --> NDC["NodeLocal DNS Cache<br/>(노드 로컬)"]
    NDC -->|캐시 hit| RESP1["즉시 응답 (빠름)"]
    NDC -->|캐시 miss| CDNS["CoreDNS Pod"]
    CDNS --> RESP2["응답 → 캐싱"]

이것이 대규모 클러스터의 DNS 성능 표준이다. CoreDNS 부하가 크게 감소한다. ndots 함정과도 연결된다 — ndots로 외부 도메인이 여러 번 실패 질의할 때, 그 실패 질의도 NodeLocal 캐시가 흡수해 부하를 낮춘다.

DNS 디버깅의 정석 — 3단계

DNS 문제가 의심될 때:

  1. resolv.conf 확인: Pod의 /etc/resolv.conf(nameserver, search, ndots).
  2. 직접 질의: nslookup/dig로 svc-name 풀이 시도. 실패 메시지(NXDOMAIN/SERVFAIL/timeout)로 원인 분류.
  3. CoreDNS 상태: CoreDNS Pod 로그/메트릭. forward 실패, 캐시 동작.

이 3단계가 13장(트러블슈팅)의 DNS 축을 구체화한다. "DNS가 안 된다"를 이 3단계로 좁히면 원인이 보인다.

직접 확인하기

# Kubernetes 1.36 — Pod의 resolv.conf와 ndots 확인
kubectl run t --image=busybox:1.36 --rm -it --restart=Never -- cat /etc/resolv.conf
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

확인할 것: ndots:5와 search 도메인들.

# 외부 도메인 풀이
kubectl run t --image=busybox:1.36 --rm -it --restart=Never -- nslookup api.weather.com
# 끝 점을 붙인 FQDN — 검색 도메인을 안 붙임
kubectl run t --image=busybox:1.36 --rm -it --restart=Never -- nslookup api.weather.com.

확인할 것: 둘 다 결국 풀리지만, ndots 영향으로 질의 단계가 다를 수 있다. (정확한 측정은 CoreDNS 로그/메트릭.)

흔히 묻는 것, 흔히 틀리는 것

오해 정정
"Service 이름은 항상 같은 namespace에서만 풀린다" 단일 이름은 현재 namespace 우선이지만, 없으면 다른 namespace 시도 가능(구성 따라). FQDN이 안전
"ndots는 성능과 무관하다" 깊은 관련. 외부 도메인이 여러 번 실패 질의. 지연/부하 원인
"CoreDNS가 외부까지 직접 푼다" 외부는 상위 DNS로 forward. CoreDNS는 클러스터 내부 전담 + 외부 위임
"dnsPolicy는 신경 안 써도 된다" ClusterFirst(기본)/Default/None이 외부 호출 성능에 영향
"headless Service는 DNS에 안 나온다" 반대. Pod IP 목록이 DNS로 나옴
"Corefile은 고정이다" ConfigMap이라 편집 가능. 단 전체 영향. reload 플러그인이 자동 반영

요약 — 이 글의 결론

  • 클러스터 안에는 전용 DNS가 필요하다 — 외부 DNS는 클러스터 내부 Service 이름을 모른다. CoreDNS가 그 역할. kube-dns Service 뒤에서 돈다.
  • Service DNS 이름 구조: <service>.<namespace>.svc.cluster.local. 같은 namespace에선 단일 이름도 풀리지만, namespace 혼란을 피하려면 <service>.<namespace> 이상으로 부를 것.
  • ndots:5 함정: 점이 5개 미만인 이름(대부분의 외부 도메인)이 검색 도메인을 먼저 붙여 여러 번 실패 질의. 외부 호출 지연/부하의 흔한 원인. 끝 점(FQDN) 또는 dnsConfig ndots 조정으로 해결.
  • headless/StatefulSet DNS도 CoreDNS가 담당 — 일반 Service는 단일 ClusterIP, headless는 Pod IP 목록, StatefulSet Pod는 개별 Pod IP.
  • Corefile(ConfigMap)이 CoreDNS 동작 정의. 플러그인 구조로 유연. 편집으로 클러스터 DNS 정책 커스터마이즈 가능(전체 영향 주의).
  • NodeLocal DNS Cache가 대규모에서 CoreDNS 병목을 푼다 — 노드 로컬 캐싱.

생각해 볼 문제

  1. Pod에서 web을 질의했는데 다른 namespace의 web Service로 갔다. 이유는? 어떻게 막나?
  2. 외부 API 호출이 느리다. ndots 함정인지 확인하는 방법은? (resolv.conf, CoreDNS 로그)
  3. ndots:1로 낮췄더니 내부 Service 단일 이름이 안 풀린다. 왜? 어떻게 둘 다 만족시키나?
  4. CoreDNS Pod가 죽었다. 클러스터에선 무슨 일이 벌어지나? (DNS 의존성)
  5. kube-dns Service가 ClusterIP인데, 클라이언트는 어떻게 CoreDNS Pod를 찾나?
  6. 외부 도메인을 끝 점 없이 api.weather.com로 부를 때와 api.weather.com.로 부를 때, CoreDNS의 질의 경로 차이를 설명하라.
  7. NodeLocal DNS Cache 없이 Pod 1만 개 클러스터를 돌리면 CoreDNS에 무슨 일이 나나?

참고

'Tech Artifacts > K8s Networking' 카테고리의 다른 글

K8s Networking - 06. network-policy  (0) 2026.07.14
K8s Networking - 05. cilium ebpf  (0) 2026.07.14
K8s Networking - 04. calico  (1) 2026.07.14
K8s Networking - 03. cni model  (0) 2026.07.14
K8s Networking - 01. service kube-proxy  (0) 2026.07.13