- Published on
EKS에서 CoreDNS 정상인데 DNS가 간헐 실패할 때
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스가 아닌 EKS(EC2 노드)에서 coredns는 Ready이고 로그도 조용한데, 특정 Pod에서만 혹은 트래픽이 몰릴 때만 DNS 조회가 간헐적으로 실패하는 상황이 종종 있습니다. 이때 흔히 “CoreDNS 문제”로만 접근하면 원인을 놓치기 쉽습니다. DNS는 UDP 기반이며, Pod → CoreDNS(ClusterIP) → 노드/ENI → VPC 라우팅/보안 → 업스트림 리졸버까지 경로가 길고, 중간 어디에서든 드랍/지연이 생기면 애플리케이션 관점에서는 “DNS 실패”로 보이기 때문입니다.
이 글은 CoreDNS 자체가 죽거나 CrashLoop인 케이스가 아니라, CoreDNS는 정상처럼 보이는데 DNS만 간헐적으로 실패하는 케이스에 초점을 맞춥니다. CoreDNS가 실제로 불안정한 케이스는 아래 글이 더 직접적입니다.
또한 “DNS는 되는데 HTTPS만 실패”처럼 네트워크 레이어 이슈가 다른 증상으로 나타나는 경우도 많아, 네트워크 경로 디버깅 관점은 아래 글도 함께 보면 좋습니다.
증상 정의: ‘간헐 실패’의 형태를 먼저 분류
DNS 실패라고 해도 형태에 따라 원인 후보가 크게 달라집니다.
1) NXDOMAIN vs timeout vs SERVFAIL
- timeout: UDP 드랍/지연, conntrack 문제, MTU/fragment, NACL/SG, CoreDNS까지 경로 문제 가능성이 큼
- SERVFAIL: 업스트림 리졸버 문제, CoreDNS 캐시/포워딩 문제, DNSSEC/EDNS 관련 이슈 가능
- NXDOMAIN: 진짜로 이름이 없거나, search domain/ndots로 엉뚱한 쿼리를 보내는 설정 문제 가능
2) 특정 노드/특정 AZ/특정 워크로드에서만 발생
- 노드 단위 conntrack 포화, ENI/라우팅, MTU, CNI 이슈 가능
- Pod 수가 많은 노드에서만 발생하면 확률적으로 conntrack/큐잉/CPU 스파이크를 의심
3) 부하가 올라갈 때만 발생
- CoreDNS QPS, CoreDNS CPU throttling, 노드 conntrack 테이블 포화
- 애플리케이션이 DNS를 과도하게 호출(캐시 부재)하는 경우
10분 내 1차 결론을 내는 관측 포인트
핵심은 “어디에서 끊기는지”를 빠르게 나누는 것입니다.
A. Pod 내부에서 에러 형태 확인
아래는 Pod 안에서 dig로 timeout인지, 응답은 오지만 느린지를 확인하는 기본 템플릿입니다.
# 임시 디버그 Pod
kubectl run -it --rm dns-debug \
--image=public.ecr.aws/amazonlinux/amazonlinux:2023 \
--restart=Never -- bash
# 도구 설치
yum -y install bind-utils iproute
# CoreDNS ClusterIP 확인
kubectl -n kube-system get svc kube-dns
# CoreDNS로 직접 질의(ClusterIP)
dig @<KUBE_DNS_CLUSTER_IP> kubernetes.default.svc.cluster.local A +time=1 +tries=1
# 기본 resolv.conf 경유 질의
dig kubernetes.default.svc.cluster.local A +time=1 +tries=1
# 외부 도메인도 테스트
dig amazon.com A +time=1 +tries=1
@kube-dns로 직접 질의는 잘 되는데, 기본 질의가 실패한다면 Pod의 resolv.conf / ndots / search 문제일 수 있습니다.- 둘 다 timeout이면 **Pod→CoreDNS 경로(네트워크/conntrack/MTU)**부터 의심합니다.
B. CoreDNS 메트릭/로그로 “정말 정상인지” 확인
CoreDNS Pod가 Ready여도 CPU throttling이나 QPS 폭증이면 간헐 timeout이 발생할 수 있습니다.
# CoreDNS 리소스/재시작/노드 분산 확인
kubectl -n kube-system get pod -l k8s-app=kube-dns -o wide
kubectl -n kube-system top pod -l k8s-app=kube-dns
# 최근 로그에서 timeout/denied/servfail 힌트 확인
kubectl -n kube-system logs -l k8s-app=kube-dns --tail=200
추가로 Prometheus를 쓴다면 다음 지표가 유용합니다.
coredns_dns_request_count_totalcoredns_dns_response_rcode_count_total{rcode="SERVFAIL"}process_cpu_seconds_total,container_cpu_cfs_throttled_seconds_total
C. 노드 단위 conntrack 상태 확인(가장 흔한 원인 중 하나)
EKS에서 DNS는 대개 UDP/53으로 오가며, 노드의 conntrack 테이블이 포화되면 새 연결/응답 매칭이 깨져서 간헐 timeout이 납니다.
노드에 접속(SSM/SSH) 가능하다면:
# 현재 conntrack 사용량/최대치
sysctl net.netfilter.nf_conntrack_count
sysctl net.netfilter.nf_conntrack_max
# 드랍/실패 카운터(커널 로그/통계)
dmesg | egrep -i "conntrack|nf_conntrack" | tail -n 50
# conntrack 테이블이 급증하는지 관찰
watch -n 1 'cat /proc/sys/net/netfilter/nf_conntrack_count'
증상 패턴:
- 트래픽 증가 시
nf_conntrack_count가nf_conntrack_max에 근접 - 커널 로그에
nf_conntrack: table full, dropping packet가 찍힘
원인 1: 노드 conntrack 테이블 포화(UDP 포함)로 DNS 드랍
왜 CoreDNS는 정상인데 DNS만 실패하나?
CoreDNS는 Pod로 떠 있고 Ready이며, 자기 자신은 멀쩡합니다. 하지만 CoreDNS로 들어오는/나가는 UDP 패킷이 노드에서 드랍되면 CoreDNS는 “요청을 못 받았거나 응답이 못 나간” 상태가 됩니다. 이 경우 CoreDNS 로그에 아무것도 안 남을 수도 있습니다.
해결 방향
- 노드의 conntrack max 상향
- 트래픽 패턴 개선(애플리케이션 DNS 캐시, keepalive, 불필요한 짧은 연결 감소)
- 노드 스케일아웃(파드 밀집도 낮추기)
예: Bottlerocket/AL2에서 user data 또는 DaemonSet으로 sysctl 적용(환경에 맞게 조정 필요)
# 예시 값(노드 스펙/파드 수에 따라 다름)
sysctl -w net.netfilter.nf_conntrack_max=262144
sysctl -w net.netfilter.nf_conntrack_buckets=65536
주의:
- 무작정 크게 하면 메모리 사용량이 늘어납니다.
buckets는 보통max/4근처가 권장되지만 커널/배포판에 따라 제약이 있습니다.
원인 2: MTU/fragmentation 문제로 UDP DNS 응답이 간헐 유실
DNS는 UDP이고, 응답이 커지면(예: TXT 레코드, 여러 A 레코드, DNSSEC, 큰 CNAME 체인) **단편화(fragmentation)**가 발생할 수 있습니다. MTU가 경로 중간에서 맞지 않거나, 단편화된 UDP가 NACL/방화벽/가상 네트워크에서 드랍되면 “간헐”로 보입니다.
체크 방법
- 실패하는 도메인이 특정(응답이 큰 도메인)인지
dig +dnssec,dig +bufsize=4096같은 옵션에서 실패율이 증가하는지
# EDNS 버퍼를 키워 응답이 커질 때 실패하는지 확인
for i in $(seq 1 30); do
dig amazon.com A +time=1 +tries=1 +bufsize=4096 | grep -E "status:|;; MSG SIZE" || echo FAIL
done
해결 방향
- VPC CNI/노드/파드 MTU 정합성 확인
- 캡슐화(예: TGW, VPN, 일부 CNI 설정) 구간이 있다면 MTU를 보수적으로 낮추는 것을 검토
- NodeLocal DNSCache를 쓰면 일부 케이스에서 재시도/캐시로 완화되지만 근본은 MTU 정합성입니다.
원인 3: NodeLocal DNSCache 미사용으로 CoreDNS/네트워크 경로가 길어짐
EKS에서 기본은 Pod가 kube-dns(CoreDNS Service IP)로 질의합니다. 이때 Pod→kube-proxy 규칙→CoreDNS Pod로 가는 경로가 노드/iptables/ipvs/ENI를 타며, 부하가 커지면 지연/드랍이 생길 수 있습니다.
NodeLocal DNSCache를 쓰면 각 노드에 로컬 캐시(DaemonSet)가 떠서 Pod는 노드 로컬 IP로 질의하고, 캐시 히트 시 네트워크 홉이 크게 줄어듭니다. 또한 CoreDNS QPS도 줄어듭니다.
적용 시 기대효과
- 간헐 timeout 감소(특히 부하 시)
- CoreDNS 스케일링 압박 완화
- 외부 도메인에 대한 반복 조회 비용 감소
간단한 검증 포인트
NodeLocal을 켰다면 Pod의 /etc/resolv.conf가 보통 169.254.20.10 같은 로컬 주소를 가리키게 됩니다(클러스터 설정에 따라 다름).
kubectl exec -it <pod> -- cat /etc/resolv.conf
원인 4: ndots/search 설정으로 쿼리가 폭증하거나 엉뚱한 NXDOMAIN이 섞임
Kubernetes 기본 ndots:5는 짧은 호스트명을 질의할 때 search domain을 여러 개 붙여서 시도합니다. 예를 들어 앱이 redis만 조회하면 실제로는 다음처럼 여러 번 질의합니다.
redis.<ns>.svc.cluster.localredis.svc.cluster.localredis.cluster.localredis.<corp-domain>…
트래픽이 크면 이게 DNS QPS 폭증으로 이어지고, 결과적으로 간헐 timeout으로 보일 수 있습니다.
진단
CoreDNS에 query log를 잠깐 켜서(운영 주의) 동일 이름에 대한 반복 질의가 많은지 봅니다.
Corefile 예시(일시적으로만):
.:53 {
log
errors
health
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
}
forward . /etc/resolv.conf
cache 30
}
해결 방향
- 애플리케이션에서 FQDN 사용(
redis.default.svc.cluster.local) - 언어 런타임별 DNS 캐시 활성화(예: JVM
networkaddress.cache.ttl) - 필요 시 Pod
dnsConfig.options로ndots조정(부작용 검토 필수)
원인 5: CoreDNS 리소스 제한/스케줄링/노드 편향
CoreDNS가 2 replicas로만 떠 있고, 둘 다 같은 노드/AZ에 몰리거나 CPU limit이 낮아 throttling이 걸리면 “정상(Ready)”이면서도 간헐 실패가 납니다.
체크
kubectl -n kube-system get deploy coredns -o yaml | yq '.spec.replicas'
kubectl -n kube-system get pod -l k8s-app=kube-dns -o wide
kubectl -n kube-system describe pod -l k8s-app=kube-dns | egrep -i "Limits|Requests|throttle|OOM" -n
해결 방향
- replicas 증가(클러스터 규모/쿼리량에 맞게)
podAntiAffinity로 노드 분산- CPU request/limit 재조정(특히 limit이 너무 낮으면 throttling)
원인 6: 보안장비/NACL/네트워크 정책이 UDP/53을 간헐적으로 막음
대부분은 “아예 안 됨”으로 나타나지만, 다음 조건에서는 간헐처럼 보일 수 있습니다.
- 특정 노드 서브넷에만 적용된 NACL 규칙
- 특정 목적지(업스트림 리졸버 IP)로의 UDP만 제한
- egress 정책이 일부 Pod에만 적용
Kubernetes NetworkPolicy를 쓰는 경우, DNS는 보통 kube-system의 kube-dns로 향하므로 egress 규칙에 UDP/53이 포함되어야 합니다.
간단한 연결성 테스트:
# CoreDNS 서비스 IP로 UDP 53 테스트(도구에 따라)
# nc는 UDP에서 애매하니 dig로 대체하는 편이 정확
dig @<KUBE_DNS_CLUSTER_IP> kubernetes.default.svc.cluster.local A +time=1 +tries=1
네트워크 정책/보안그룹/라우팅 전반 진단 흐름은 아래 글의 “10분 진단” 섹션도 유사한 방식으로 적용할 수 있습니다.
실전 트러블슈팅 플레이북(우선순위)
운영에서 시간을 아끼려면 아래 순서가 효율적입니다.
1) 실패가 “timeout”인지 먼저 확정
dig +time=1 +tries=1로 짧게 여러 번@kube-dns직접 질의 vs 기본 질의 비교
2) 노드 편향 확인
- 실패 Pod가 특정 노드에 몰리는지
- 같은 테스트를 다른 노드의 Pod에서 반복
3) conntrack 포화 지표 확인
nf_conntrack_count/max- 커널 로그의
table full여부
4) MTU/큰 응답에서 실패하는지 확인
+bufsize=4096테스트- 특정 도메인에서만 실패하는지
5) CoreDNS 리소스/스케일/분산 확인
- CPU throttling
- replicas/anti-affinity
6) NodeLocal DNSCache 도입 검토
- 근본 네트워크 드랍이 있는 상태에서도 완화 효과가 큼
마무리: “CoreDNS 정상”은 시작점일 뿐
EKS에서 DNS 간헐 실패는 CoreDNS Pod 상태만으로 판단하기 어렵고, 실제로는 노드 conntrack, MTU/fragment, 경로 상 드랍, ndots로 인한 QPS 폭증, CoreDNS 리소스/분산 같은 요인이 더 자주 원인입니다.
가장 재현 가능하고 빈도가 높은 1순위는 보통 conntrack 포화와 MTU/UDP 단편화입니다. 먼저 “timeout vs NXDOMAIN/SERVFAIL”을 분리하고, @kube-dns 직접 질의/노드 편향/conntrack 카운터를 묶어서 보면 대개 30분 안에 방향이 잡힙니다.
필요하면 다음 단계로, CoreDNS에 일시적으로 query log를 켜서 QPS/패턴을 확인하고(운영 주의), NodeLocal DNSCache로 경로를 단축해 안정성을 끌어올리는 접근이 실전에서 가장 효과적입니다.