Kubernetes, CoreDNS
CoreDNS는 Kubernetes 클러스터 내부의 DNS 서버로, Service 이름을 IP 주소로 변환하여 Service Discovery를 가능하게 한다.
Service Discovery란 클러스터 내 서비스들이 서로의 IP를 직접 알지 못해도 이름만으로 찾아갈 수 있게 해주는 메커니즘이다.
Pod에서 my-service.default.svc.cluster.local이라는 이름으로 다른 Service에 접근할 수 있는 것은 CoreDNS 덕분이다.
CoreDNS
CoreDNS는 Go로 작성된 플러그인 기반 DNS 서버이다.
이전에는 kube-dns가 사용되었는데, CoreDNS로 대체된 이유는 다음과 같다:
| 항목 | kube-dns | CoreDNS |
|---|---|---|
| 구성 | 3개 컨테이너 (kubedns, dnsmasq, sidecar) | 단일 컨테이너 |
| 설정 | 복잡한 옵션 | Corefile (단순한 설정 파일) |
| 메모리 | 상대적으로 높음 | 30% 이상 절감 |
CoreDNS는 kube-system 네임스페이스에 Deployment로 배포된다. 보통 2개의 Pod가 실행되며, kube-dns라는 이름의 Service(ClusterIP)를 통해 접근한다.
Service 이름이
kube-dns인 것은 역사적으로 흘러가듯 가기 위함… 하위 호환성을 위해 CoreDNS 전환 이후에도 이름을 유지하고 있다.
DNS 해석 흐름
Pod의 DNS 설정
kubelet은 Pod 생성 시 /etc/resolv.conf를 자동으로 주입한다.
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
- nameserver - CoreDNS Service의 ClusterIP (kube-dns)
- search - DNS 쿼리 시 자동으로 붙이는 도메인 접미사 리스트
- ndots:5 - 도메인에 점(.)이 5개 미만이면 FQDN이 아닌 것으로 간주
FQDN(Fully Qualified Domain Name)은 루트(
.)까지 포함한 완전한 도메인 이름이다. 예를 들어api.example.com.이 FQDN이고,api.example.com은 그렇지 않다. ndots:5 설정에서는 점이 5개 미만인 도메인을 FQDN이 아닌 것으로 간주하여 search 리스트의 접미사를 순서대로 붙여가며 조회한다.
쿼리 흐름
Pod에서 my-service를 조회하면 실제로 어떤 일이 벌어지는가:
sequenceDiagram
participant Pod
participant CoreDNS
participant Upstream as Upstream DNS
Pod->>CoreDNS: my-service.default.svc.cluster.local?
Note over CoreDNS: kubernetes 플러그인에서<br/>Service 조회
CoreDNS-->>Pod: 10.96.100.50 (ClusterIP)
Pod->>CoreDNS: google.com?
Note over CoreDNS: kubernetes 플러그인에서<br/>매치 안됨
CoreDNS->>Upstream: google.com? (forward)
Upstream-->>CoreDNS: 142.250.196.110
CoreDNS-->>Pod: 142.250.196.110
- 클러스터 내부 도메인(
*.svc.cluster.local)- kubernetes 플러그인이 직접 응답
- 클러스터 외부 도메인(
google.com등)- forward 플러그인이 upstream DNS로 전달
ndots 문제
ndots:5는 성능에 심각한 영향을 줄 수 있다.
Pod에서 api.example.com을 조회하면 점이 2개이므로 5개 미만 → FQDN이 아닌 것으로 판단한다:
1. api.example.com.default.svc.cluster.local → NXDOMAIN
2. api.example.com.svc.cluster.local → NXDOMAIN
3. api.example.com.cluster.local → NXDOMAIN
4. api.example.com. → 성공!
하나의 외부 도메인 조회에 최대 4배의 DNS 쿼리가 발생한다. IPv4(A)와 IPv6(AAAA)를 동시에 질의하면 8배까지 증폭된다. 외부 API를 빈번하게 호출하는 서비스라면 이 오버헤드가 꽤 크다.
완화 방법:
- FQDN에 trailing dot 붙이기:
api.example.com.으로 쿼리하면 search domain을 거치지 않는다 - ndots 낮추기: Pod
dnsConfig에서ndots: 1로 설정. 단, 클러스터 내부 Service를 짧은 이름으로 접근할 수 없게 된다 - NodeLocal DNSCache 사용: NXDOMAIN 응답도 캐싱하여 반복 쿼리를 방지한다
DNS 레코드 유형
Service DNS
| Service 유형 | DNS 레코드 | 예시 |
|---|---|---|
| ClusterIP | A/AAAA → ClusterIP | my-svc.default.svc.cluster.local → 10.96.100.50 |
| Headless (ClusterIP: None) | A/AAAA → 각 Pod IP | my-svc.default.svc.cluster.local → 10.244.1.5, 10.244.2.3 |
| ExternalName | CNAME → 외부 도메인 | my-svc.default.svc.cluster.local → api.example.com |
StatefulSet Pod DNS
StatefulSet의 각 Pod는 안정적인 DNS 이름을 갖는다:
<pod-name>.<headless-service>.<namespace>.svc.cluster.local
예시:
mysql-0.mysql-headless.default.svc.cluster.local → 10.244.1.5
mysql-1.mysql-headless.default.svc.cluster.local → 10.244.2.3
mysql-2.mysql-headless.default.svc.cluster.local → 10.244.3.7
Pod가 재시작되어 IP가 변경되어도 DNS 이름은 유지된다. 데이터베이스, 메시지 큐 등 상태가 있는 애플리케이션에서 이 특성이 중요하다.
SRV 레코드
Named Port가 정의된 Service는 SRV 레코드도 제공한다:
_<port-name>._<protocol>.<service>.<namespace>.svc.cluster.local
→ 예: _http._tcp.my-svc.default.svc.cluster.local
커스텀 DNS 설정
Stub Domain: 특정 도메인을 특정 DNS 서버로 전달할 수 있다. 예를 들어 내부 도메인 corp.example.com을 사내 DNS 10.150.0.1로 전달하려면:
corp.example.com:53 {
errors
cache 30
forward . 10.150.0.1
}
Upstream DNS 변경: 기본 upstream을 Google DNS로 변경할 수도 있다:
forward . 8.8.8.8 8.8.4.4
DNS Policy
Pod의 dnsPolicy 필드로 DNS 동작을 제어할 수 있다.
| Policy | 동작 |
|---|---|
| ClusterFirst (기본값) | 클러스터 도메인은 CoreDNS, 나머지는 upstream으로 전달 |
| Default | 노드의 /etc/resolv.conf를 그대로 사용 |
| None | Pod의 dnsConfig에서 직접 설정 (가장 유연) |
| ClusterFirstWithHostNet | hostNetwork: true인 Pod에서 ClusterFirst 동작 |
5초 DNS 지연 문제
Kubernetes에서 가장 악명 높은 DNS 문제다.
conntrack이란
conntrack(connection tracking)은 Linux 커널이 모든 네트워크 연결 상태를 추적하는 모듈이다.
Linux의 패킷 필터링 프레임워크인 iptables와 nftables는 두 가지 핵심 기능을 수행한다.
- NAT(Network Address Translation) - 패킷의 출발지/목적지 IP를 변환한다. Kubernetes에서는 ClusterIP를 실제 Pod IP로 바꾸는 DNAT이 대표적이다
- 상태 기반 패킷 필터링(Stateful Packet Filtering) - 단순히 개별 패킷만 보는 것이 아니라, “이 패킷이 이미 수립된 연결의 일부인지, 새로운 연결인지” 파악하여 처리한다
이 두 기능 모두 “이 패킷이 어떤 연결에 속하는지” 알아야 하는데, 그 정보를 conntrack 테이블에서 관리한다.
Kubernetes에서 Pod가 CoreDNS의 ClusterIP로 DNS 쿼리를 보내면, kube-proxy가 설정한 iptables 규칙에 의해 DNAT(목적지를 ClusterIP → CoreDNS Pod IP로 변환)이 수행되고, conntrack에 이 연결이 기록된다.
Race Condition
문제는 Linux resolver가 A(IPv4)와 AAAA(IPv6) 쿼리를 같은 소켓에서 거의 동시에 전송한다는 데 있다.
sequenceDiagram
participant Pod
participant CT as conntrack
participant CoreDNS
Pod->>CT: A 쿼리 (UDP, src port 12345)
Pod->>CT: AAAA 쿼리 (UDP, src port 12345)
Note over CT: 같은 소켓에서 두 패킷이<br/>동시에 NAT를 요청<br/>→ conntrack 항목 충돌
CT--xPod: 패킷 하나가 드롭됨
Note over Pod: 5초 타임아웃 후 재시도
두 UDP 패킷이 동일한 소스 포트에서 거의 동시에 conntrack 항목을 생성하려 하면, 커널 내부에서 삽입 충돌이 발생한다. 충돌한 패킷은 드롭되고, Linux resolver의 기본 타임아웃인 5초 후에야 재시도한다.
한 줄 요약: DNS 쿼리가 간헐적으로 정확히 5초 지연된다.
NodeLocal DNSCache
대규모 클러스터에서 CoreDNS 부하를 분산하기 위한 솔루션이다.
flowchart LR
subgraph "Node"
Pod["Pod"] --> NLC["NodeLocal DNSCache<br/>(DaemonSet, 169.254.20.10)"]
NLC -->|캐시 미스| CoreDNS["CoreDNS<br/>(kube-dns Service)"]
end
CoreDNS -->|외부 쿼리| Upstream["Upstream DNS"]
각 노드에 DaemonSet으로 배포되며, 링크 로컬 IP 169.254.20.10을 사용한다. kubelet이 Pod의 nameserver를 이 IP로 설정하고, 캐시 히트 시 즉시 응답하며 미스 시 CoreDNS로 전달한다.
핵심은 TCP를 사용하여 CoreDNS에 연결한다는 점이다. 이로 인해 conntrack 관련 DNS 문제를 우회할 수 있다.
- DNS 응답 지연 감소 (로컬 캐시 히트)
- CoreDNS Pod 부하 분산
- 5초 DNS 지연 문제 해결 (conntrack race condition 우회)
- NXDOMAIN 캐싱으로 ndots 쿼리 증폭 완화
오토스케일링
dns-autoscaler가 클러스터 크기에 따라 CoreDNS Pod 수를 자동으로 조정한다.
replicas = max(ceil(nodes × 1/16), ceil(cores × 1/256))
노드 16개 → 1개, 노드 32개 → 2개, 노드 100개 → 7개.