Kubernetes, CoreDNS

· 7분 읽기

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-dnsCoreDNS
구성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 레코드예시
ClusterIPA/AAAA → ClusterIPmy-svc.default.svc.cluster.local → 10.96.100.50
Headless (ClusterIP: None)A/AAAA → 각 Pod IPmy-svc.default.svc.cluster.local → 10.244.1.5, 10.244.2.3
ExternalNameCNAME → 외부 도메인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를 그대로 사용
NonePod의 dnsConfig에서 직접 설정 (가장 유연)
ClusterFirstWithHostNethostNetwork: true인 Pod에서 ClusterFirst 동작

5초 DNS 지연 문제

Kubernetes에서 가장 악명 높은 DNS 문제다.

conntrack이란

conntrack(connection tracking)은 Linux 커널이 모든 네트워크 연결 상태를 추적하는 모듈이다.

Linux의 패킷 필터링 프레임워크인 iptables와 nftables는 두 가지 핵심 기능을 수행한다.

  1. NAT(Network Address Translation) - 패킷의 출발지/목적지 IP를 변환한다. Kubernetes에서는 ClusterIP를 실제 Pod IP로 바꾸는 DNAT이 대표적이다
  2. 상태 기반 패킷 필터링(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개.

References