쿠버네티스 DNS의 비밀 - CoreDNS가 서비스를 찾는 진짜 방법
쿠버네티스에서 파드들은 어떻게 서로를 찾을까
쿠버네티스 클러스터를 처음 다룰 때 묘한 위화감이 있습니다. 프론트엔드 파드가 백엔드 서비스와 멀쩡히 통신하고, 백엔드는 또 데이터베이스와 자연스럽게 연결되죠. IP 주소를 코드에 박아둔 것도 아닌데 전부 알아서 돌아갑니다. 한동안은 그냥 그러려니 하고 썼습니다.
그런데 한 번쯤 궁금해지는 순간이 옵니다. 파드들이 도대체 어떻게 서로의 위치를 찾아내는 걸까요? 파드는 죽고 살아나기를 반복하면서 IP가 계속 바뀌는데, 그 와중에 통신은 안 끊깁니다.
답은 DNS, 더 정확히는 CoreDNS입니다. 클러스터 안에 조용히 떠 있는 이 일꾼이 사실상 모든 서비스 디스커버리의 중심에 있어요. 동작 원리를 모른 채 쓰다가, 외부 API 호출이 이유 없이 느려지는 장애를 만나고 나서야 제대로 들여다보게 되는 경우가 많습니다. 저도 그랬고요.
오늘은 이 숨은 일꾼이 어떻게 클러스터를 굴리는지, 그리고 가끔 DNS가 느려지는 문제는 어디서 오는지 차근차근 풀어보겠습니다.

CoreDNS, 쿠버네티스의 전화번호부
CoreDNS는 2018년 쿠버네티스 1.13 버전부터 기본 DNS 서버로 자리잡았습니다. 그 전에는 kube-dns를 썼는데, CoreDNS가 더 가볍고 플러그인 구조라 설정이 유연해서 완전히 대체됐죠.
구조를 단순하게 보면 CoreDNS는 클러스터의 전화번호부입니다. 파드가 "내가 연결하려는 서비스가 어디 있지?"라고 물으면, CoreDNS가 "이 IP야"라고 답해주는 구조죠.
보통 이렇게 구성됩니다.
- kube-system 네임스페이스에서 Deployment로 실행
- 고가용성을 위해 보통 2개 복제본으로 운영
- kube-dns라는 이름의 Service로 안정적인 ClusterIP 제공
여기서 한 가지 짚고 갈 점. CoreDNS로 바뀌었는데도 서비스 이름은 여전히 kube-dns입니다. 하위 호환을 위해 이름만 남겨둔 거예요. 운영하다 보면 이런 이름과 실체의 불일치가 디버깅을 헷갈리게 만드는 순간이 종종 있습니다. 그래도 결국 모든 파드는 이 ClusterIP를 DNS 서버로 바라봅니다.
파드 안에서 DNS 조회가 일어나는 과정
파드가 서비스 이름을 찾을 때 내부에서 어떤 일이 벌어지는지 단계별로 보겠습니다.
1단계: DNS 쿼리 발생
파드가 같은 네임스페이스의 my-service에 접근하려고 합니다. 이때 파드 내부의 /etc/resolv.conf에 설정된 DNS 서버로 쿼리를 보냅니다.
2단계: CoreDNS로 전달
kubelet이 파드를 시작할 때 자동으로 /etc/resolv.conf에 kube-dns 서비스 IP를 넣어줍니다. 덕분에 애플리케이션이 따로 신경 쓸 게 없죠. 모든 DNS 쿼리가 자동으로 CoreDNS로 향합니다.
3단계: 내부 vs 외부 판단 CoreDNS가 쿼리를 받으면 이게 클러스터 내부 이름인지, 외부 도메인인지 가릅니다.
- 내부 이름(예:
my-service.default.svc.cluster.local)이면 → Kubernetes API로 서비스 조회 후 ClusterIP 반환 - 외부 도메인(예:
google.com)이면 → 상위 DNS 서버로 전달
4단계: IP 주소 반환 해결된 IP가 파드로 돌아가고, 파드는 그 IP로 연결을 시작합니다. 이 전체 과정이 정상이라면 몇 밀리초 안에 끝나요. 문제는 이 "정상이라면"이라는 전제가 의외로 잘 깨진다는 데 있습니다. 뒤에서 그 얘기를 하겠습니다.
쿠버네티스 DNS 이름 규칙 - 이것만 알면 끝
쿠버네티스 서비스 이름은 체계적인 패턴을 따릅니다.
서비스이름.네임스페이스.svc.cluster.local
각 부분을 뜯어보면 이렇습니다.
- 서비스이름: 직접 만든 Service 리소스의 이름
- 네임스페이스: 서비스가 속한 네임스페이스(default, production 등)
- svc: 이게 서비스 리소스라는 표시
- cluster.local: 클러스터 기본 도메인(거의 바꾸지 않습니다)
예를 들어 production 네임스페이스에 backend-api 서비스가 있다면 전체 이름은 이렇게 됩니다.
backend-api.production.svc.cluster.local
그런데 실무에서는 보통 같은 네임스페이스 안에서 backend-api만 써도 됩니다. 나머지는 쿠버네티스가 알아서 붙여주거든요. 이 "알아서 붙여주는" 메커니즘이 편하긴 한데, 정확히 이 부분이 뒤에 나올 ndots 함정의 출발점이 됩니다. 편의와 함정은 대개 같은 뿌리에서 나옵니다.
/etc/resolv.conf 파일의 비밀
아무 파드에 들어가서 /etc/resolv.conf를 열어보면 이런 내용이 보입니다.
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
각 줄이 하는 일을 보겠습니다.
nameserver 10.96.0.10
- kube-dns 서비스의 ClusterIP
- 모든 DNS 쿼리가 일단 여기로 갑니다
search 도메인들
- 짧은 이름을 입력했을 때 자동으로 붙여줄 접미사 목록
my-service라고 치면my-service.default.svc.cluster.local→my-service.svc.cluster.local순으로 시도- 이게 있어서 같은 네임스페이스에서는 짧은 이름만 써도 되는 겁니다
ndots:5
- 여기서부터 이야기가 복잡해집니다. 다음 섹션에서 따로 다루겠습니다
앞 섹션의 "알아서 붙여준다"는 마법의 정체가 바로 이 search 라인입니다. 마법이 아니라 단순 문자열 결합이죠. 그리고 단순한 만큼, 의도치 않게 쿼리를 잔뜩 만들어내는 부작용도 단순합니다.

ndots 문제 - DNS가 느려지는 진짜 이유
이 부분이 핵심인데, 의외로 모르고 지나가는 사람이 많습니다. 저도 외부 결제 API 연동 쪽을 보다가 응답이 미묘하게 늦는 걸 추적하면서 뒤늦게 의식하게 됐어요.
ndots:5 설정은 한 줄로 요약하면 "이름에 점이 5개 미만이면 완전한 도메인으로 보지 말고, search 도메인을 먼저 붙여서 시도해"라는 규칙입니다.
문제는 외부 API를 호출할 때 터집니다. api.stripe.com은 점이 2개뿐이라 5개에 못 미치죠. 그러면 CoreDNS는 이렇게 순서대로 시도합니다.
api.stripe.com.default.svc.cluster.local→ 없음 (NXDOMAIN)api.stripe.com.svc.cluster.local→ 없음 (NXDOMAIN)api.stripe.com.cluster.local→ 없음 (NXDOMAIN)api.stripe.com→ 드디어 성공
외부 API 호출 하나당 불필요한 DNS 쿼리가 3번씩 더 발생하는 구조입니다. 원인과 결과를 분리해서 보면, 원인은 ndots 기본값이고 결과는 쿼리 증폭이에요. 파드가 적고 외부 호출이 드물면 티가 안 납니다. 그런데 파드 수가 늘고 외부 연동이 많아지는 순간, CoreDNS와 상위 DNS로 향하는 트래픽이 몇 배로 부풀면서 병목이 생깁니다. 트래픽이 임계점을 넘기 전까지는 멀쩡하다가, 넘는 순간 갑자기 느려지는 전형적인 패턴이죠.
해결 방법은
방법 1: 도메인 끝에 점 붙이기
// 이렇게 하지 말고
fetch('https://api.stripe.com/charges')
// 이렇게 하세요
fetch('https://api.stripe.com./charges')
마지막 점이 "이미 완전한 도메인 이름이야"라고 명시하는 신호입니다. search 도메인을 건너뛰고 바로 질의하죠. 코드 수정만으로 끝나서 가볍지만, 외부 호출 코드가 흩어져 있으면 빠뜨리는 곳이 생기기 쉽습니다.
방법 2: ndots 값 조정 Pod spec의 dnsConfig로 ndots를 더 낮게 설정할 수 있습니다.
apiVersion: v1
kind: Pod
spec:
dnsConfig:
options:
- name: ndots
value: "2"
이 방식은 파드 단위로 일괄 적용된다는 게 장점입니다. 다만 ndots를 너무 낮추면 반대 리스크가 생깁니다. 같은 네임스페이스의 짧은 서비스 이름을 해석할 때 search 도메인을 못 거치고 곧장 외부로 나가버릴 수 있거든요. 내부 통신이 많은 워크로드와 외부 통신이 많은 워크로드를 같은 값으로 묶으면 한쪽이 손해를 봅니다. 어느 쪽을 우선할지는 그 파드가 무슨 일을 하느냐에 달려 있어요. 정답이 하나가 아니라는 뜻입니다.
CoreDNS 설정 파일 들여다보기
CoreDNS는 Corefile이라는 설정 파일로 동작을 제어합니다. kube-system 네임스페이스의 coredns ConfigMap에 들어 있죠.
kubectl get configmap coredns -n kube-system -o yaml
전형적인 Corefile 내용은 이렇습니다.
.: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: 쿠버네티스 API를 모니터링하며 클러스터 내부 이름 해석
- forward: 클러스터 외부 도메인을 상위 DNS 서버로 전달
- cache: DNS 응답을 30초간 캐싱해서 성능 향상
- health/ready: 쿠버네티스 헬스체크용 엔드포인트
- prometheus: 9153 포트에서 모니터링 메트릭 제공
운영 관점에서 가장 먼저 챙길 건 cache와 prometheus입니다. cache 값은 트래픽을 줄여주는 1차 방어선이고, prometheus 메트릭은 DNS가 느려질 때 "진짜 느린지" 숫자로 확인할 수 있는 유일한 근거니까요. 감으로 "DNS가 이상한 것 같다"고 말하는 것과, 메트릭으로 쿼리 수와 응답 지연을 보여주는 건 장애 회의에서 완전히 다른 무게를 가집니다.
DNS 문제 해결하기 - 실전 명령어
DNS 문제가 터졌을 때 손에 익혀두면 좋은 명령어들을 정리합니다.
1. CoreDNS 파드 상태 확인
kubectl get pods -n kube-system -l k8s-app=kube-dns
2. DNS 테스트용 파드로 직접 확인
kubectl run dnsutils --image=registry.k8s.io/e2e-test-images/jessie-dnsutils:1.3 --restart=Never -- sleep 3600
kubectl exec -it dnsutils -- nslookup kubernetes.default
정상이라면 kubernetes 서비스의 ClusterIP가 나와야 합니다. 타임아웃이나 SERVFAIL이 뜨면 그 자체가 신호예요.
3. CoreDNS 로그 확인
kubectl logs -n kube-system -l k8s-app=kube-dns
REFUSED, SERVFAIL, I/O timeout 같은 에러를 찾아봅니다. 경험상 이 줄에서 갈리는 게 둘 중 하나예요. 상위 DNS 서버 쪽 문제이거나, 네트워크 정책으로 53번 포트가 막힌 경우. 전자는 CoreDNS 바깥의 문제고 후자는 클러스터 안의 문제라, 원인을 어디로 잡느냐에 따라 부르는 담당자가 달라집니다. 그래서 로그로 방향부터 가르는 게 시간을 아끼는 길입니다.

마무리
쿠버네티스에서 네트워킹이 고속도로라면, DNS는 내비게이션입니다. 아무리 잘 깔린 도로가 있어도 길을 못 찾으면 차는 멈춰 서죠.
CoreDNS를 들여다보고 나서 좋은 점은, "왜 가끔 외부 API 호출이 느려지지?" 같은 막연한 의문이 구조의 문제로 또렷해진다는 겁니다. ndots 하나 조정하는 것도 결국 내부 통신과 외부 통신 중 무엇을 우선할지를 정하는 작은 아키텍처 결정이에요. 그래서 DNS는 단순한 인프라 부속이 아니라, 운영 비용과 응답 지연을 동시에 건드리는 지점이라고 봅니다.
눈에 안 보이는 컴포넌트일수록, 잘 돌 때는 존재를 잊고 멈추면 클러스터 전체가 흔들립니다. CoreDNS가 딱 그런 위치에 있어요. 다음에는 쿠버네티스 네트워킹의 다른 층위도 같은 방식으로 뜯어보겠습니다.