Published on

AWS EKS CoreDNS CrashLoopBackOff와 DNS 타임아웃 해결

Authors

서버리스든 마이크로서비스든, 쿠버네티스에서 DNS는 사실상 제1의 데이터플레인입니다. EKS에서 CoreDNS가 CrashLoopBackOff로 빠지면 애플리케이션은 외부 API 호출은 물론, 같은 네임스페이스의 서비스 디스커버리조차 실패하며 i/o timeout, no such host 같은 증상으로 연쇄 장애가 납니다. 특히 장애가 간헐적이면 원인이 더 복잡해 보이는데, 대부분은 CoreDNS 자체 문제(설정/버전/리소스) 또는 **노드/네트워크 경로 문제(iptables, conntrack, SG/NACL, ENI, MTU)**로 수렴합니다.

이 글은 “CoreDNS CrashLoopBackOff + DNS 타임아웃” 조합을 재현 가능한 절차로 진단하고, 가장 자주 먹히는 해결책을 우선순위대로 정리합니다.

> 타임아웃을 다루는 방식은 본질적으로 네트워크/리트라이/관측의 문제입니다. 애플리케이션 계층의 타임아웃 대응 관점이 필요하면 OpenAI Responses API 408 타임아웃 재현과 해결 실전 가이드도 함께 참고하면, “어디서 타임아웃이 나는지”를 쪼개서 보는 감각을 얻을 수 있습니다.

증상 패턴 정리

다음 중 2개 이상이면 CoreDNS/클러스터 DNS 경로를 우선 의심합니다.

  • kubectl -n kube-system get pods에서 coredns-*CrashLoopBackOff 또는 Error.
  • 파드에서 nslookup kubernetes.defaultconnection timed out; no servers could be reached.
  • 애플리케이션 로그에 dial tcp: lookup xxx on 10.100.0.10:53: i/o timeout.
  • 노드 교체/스케일링/업그레이드 직후 발생.
  • 특정 노드에 스케줄된 파드에서만 DNS 실패(노드 로컬 문제 가능성 큼).

0) 현재 상태를 5분 안에 스냅샷 뜨기

장애 대응에서 가장 먼저 할 일은 “지금 무엇이 깨졌는지”를 명령어로 고정하는 것입니다.

# CoreDNS 상태
kubectl -n kube-system get deploy,rs,po -l k8s-app=kube-dns -o wide

# CoreDNS 이벤트(크래시 이유 단서)
kubectl -n kube-system describe pod -l k8s-app=kube-dns | sed -n '/Events:/,$p'

# CoreDNS 로그
kubectl -n kube-system logs -l k8s-app=kube-dns --tail=200

# CoreDNS ConfigMap
kubectl -n kube-system get cm coredns -o yaml

# kube-dns 서비스(ClusterIP가 파드의 nameserver로 들어감)
kubectl -n kube-system get svc kube-dns -o wide

이 스냅샷만으로도 원인이 절반은 좁혀집니다. 예를 들어 로그에 plugin/loop가 보이면 루프, no such host면 업스트림 리졸버, panic이면 버전/플러그인/설정 문제 가능성이 큽니다.

1) CrashLoopBackOff의 1순위: CoreDNS 설정(Corefile) 문제

EKS에서 CoreDNS는 보통 아래와 같은 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
}

1-1) loop 플러그인으로 인한 무한 루프

대표적인 크래시 원인 중 하나가 CoreDNS가 자신(또는 클러스터 DNS)으로 다시 질의를 포워딩하는 루프입니다.

  • forward . /etc/resolv.conf가 가리키는 업스트림이 kube-dns의 ClusterIP(예: 10.100.0.10)로 잡혀있으면 루프가 생깁니다.
  • 노드의 /etc/resolv.conf가 어떤 이유로 클러스터 DNS를 바라보게 되면(잘못된 DHCP/이미지/부트스트랩), CoreDNS가 그 파일을 읽는 순간 루프가 됩니다.

확인:

# CoreDNS 파드 안에서 resolv.conf 확인
POD=$(kubectl -n kube-system get pod -l k8s-app=kube-dns -o jsonpath='{.items[0].metadata.name}')
kubectl -n kube-system exec -it "$POD" -- cat /etc/resolv.conf

# kube-dns ClusterIP 확인
kubectl -n kube-system get svc kube-dns -o jsonpath='{.spec.clusterIP}'; echo

해결(가장 흔한 처방): 업스트림을 명시적으로 VPC DNS(보통 169.254.169.253)나 사내 리졸버로 고정합니다.

kubectl -n kube-system edit cm coredns

예시(Corefile 일부):

forward . 169.254.169.253 {
    max_concurrent 1000
}

변경 후 롤아웃:

kubectl -n kube-system rollout restart deploy coredns
kubectl -n kube-system rollout status deploy coredns

> 참고: loop 플러그인을 제거하는 방식도 있지만, 루프 탐지를 포기하는 것이므로 근본 원인(업스트림/리졸브 체인)을 먼저 바로잡는 것을 권합니다.

1-2) ndots/검색 도메인 폭증으로 인한 지연(타임아웃처럼 보임)

CrashLoop이 아니라도 DNS 타임아웃의 흔한 원인은 과도한 검색 도메인/ndots 설정입니다. 예: ndots:5 + 긴 search list면, 외부 도메인 하나 조회할 때 수차례 실패 질의가 발생해 지연이 누적됩니다.

파드의 /etc/resolv.conf 확인:

kubectl run -it --rm dns-debug --image=busybox:1.36 --restart=Never -- sh
cat /etc/resolv.conf

완화 방법:

  • 애플리케이션이 외부 FQDN을 자주 조회한다면, 워크로드에 dnsConfig.options.ndots: "1" 적용 고려
  • CoreDNS cache TTL 조정(너무 짧으면 QPS 폭증)

예시(Deployment 일부):

spec:
  template:
    spec:
      dnsConfig:
        options:
        - name: ndots
          value: "1"

2) CrashLoopBackOff의 2순위: 리소스 부족(OOMKilled)과 QPS 폭증

CoreDNS는 작은 리소스로도 돌아가지만, 트래픽이 몰리면 메모리/CPU가 급격히 튀고 OOMKilled → 재시작 → 캐시 소실 → QPS 증가의 악순환이 생깁니다.

확인:

kubectl -n kube-system get pod -l k8s-app=kube-dns -o jsonpath='{range .items[*]}{.metadata.name} {.status.containerStatuses[0].lastState.terminated.reason}{"\n"}{end}'

kubectl -n kube-system top pod -l k8s-app=kube-dns

해결 체크리스트:

  1. CoreDNS replica 수 증가(노드 수/워크로드 QPS에 비례)
  2. requests/limits 상향(특히 memory)
  3. 불필요한 DNS 질의 감소(애플리케이션 캐시, ndots 조정)

예시(리소스 상향 + 레플리카 확장):

kubectl -n kube-system scale deploy coredns --replicas=4

kubectl -n kube-system patch deploy coredns --type='json' -p='[
  {"op":"replace","path":"/spec/template/spec/containers/0/resources","value":{
    "requests":{"cpu":"200m","memory":"256Mi"},
    "limits":{"cpu":"500m","memory":"512Mi"}
  }}
]'

운영 팁:

  • HPA를 CoreDNS에 적용하는 경우도 있지만, DNS는 스파이크에 매우 민감하므로 최소 레플리카를 충분히 두는 것이 중요합니다.
  • CoreDNS 메트릭(:9153)을 Prometheus로 수집해 coredns_dns_requests_total, coredns_cache_hits_total, coredns_forward_request_duration_seconds를 봐야 원인-결과가 연결됩니다.

3) EKS에서 특히 자주 터지는 네트워크 원인 3가지

CoreDNS가 크래시하지 않는데도 타임아웃이 나면, “CoreDNS → 업스트림(예: VPC DNS) → 응답” 경로가 막힌 경우가 많습니다.

3-1) 보안 그룹/네트워크 ACL에서 53/UDP(및 53/TCP) 누락

  • 노드 보안 그룹 또는 NACL에서 UDP 53이 막히면 업스트림 포워딩이 타임아웃.
  • DNS 응답이 큰 경우 TCP로 폴백하므로 TCP 53도 열어야 합니다.

검증(파드에서 직접 업스트림으로 dig):

kubectl run -it --rm dns-tools --image=ghcr.io/infobloxopen/dnstools:latest --restart=Never -- sh

# VPC DNS로 직접 질의(대개 169.254.169.253)
dig @169.254.169.253 amazon.com +time=2 +tries=1

3-2) 노드 conntrack 테이블 포화

DNS는 UDP 기반 단기 연결이 많아 conntrack이 부족하면 드롭/지연이 발생합니다. 증상은 “간헐 타임아웃”으로 나타나기 쉬워서 까다롭습니다.

노드에서 확인(SSM/SSH 필요):

# 현재 conntrack 사용량
sudo conntrack -C
# 최대치
cat /proc/sys/net/netfilter/nf_conntrack_max

# 드롭 카운터(커널 메시지)
dmesg | egrep -i 'conntrack|nf_conntrack|table full' | tail -n 50

해결:

  • nf_conntrack_max 상향
  • 노드 타입/커널 튜닝
  • DNS QPS를 줄이거나 NodeLocal DNSCache 도입(아래 5절)

3-3) MTU/캡슐화 문제로 인한 단편화 드롭

CNI 설정/경로 MTU가 어긋나면 큰 DNS 응답(특히 TXT 레코드, 다수 A 레코드)이 단편화되며 드롭될 수 있습니다. 이 경우도 “일부 도메인만 타임아웃”으로 보입니다.

검증 아이디어:

  • dig +bufsize=4096 vs 기본값 비교
  • 노드/파드 네트워크 MTU 확인(CNI, ENI, 터널 여부)

4) 버전/애드온 불일치: EKS 애드온 CoreDNS 업데이트로 해결되는 케이스

EKS 업그레이드 후 CoreDNS가 크래시하거나 이상 동작하는 경우, 클러스터 버전과 CoreDNS 애드온 버전 호환성을 확인해야 합니다.

확인:

# EKS Add-on으로 관리 중인지 확인(eksctl 또는 aws cli)
aws eks describe-addon --cluster-name <CLUSTER> --addon-name coredns

# CoreDNS 이미지 태그 확인
kubectl -n kube-system get deploy coredns -o jsonpath='{.spec.template.spec.containers[0].image}'; echo

해결:

  • EKS Add-on을 사용한다면, AWS 권장 버전으로 업데이트
  • 수동 매니페스트로 운영 중이면, 릴리즈 노트 보고 버전 업 + Corefile 변경사항 반영

5) 재발 방지에 가장 효과적인 처방: NodeLocal DNSCache

트래픽이 큰 클러스터에서 DNS 안정성을 올리는 정석은 NodeLocal DNSCache입니다.

핵심 효과:

  • 노드마다 로컬 캐시(일반적으로 169.254.20.10)가 떠서 CoreDNS 부하 감소
  • conntrack 압력 감소(노드 내부로 단축)
  • 네트워크 순간 장애에도 캐시로 버팀

적용 개요:

  1. NodeLocal DNSCache DaemonSet 배포
  2. kubelet --cluster-dns 또는 EKS 권장 방식으로 파드의 nameserver가 로컬 IP를 보게 구성

EKS는 공식 매니페스트를 기반으로 적용하는 경우가 많습니다. 적용 후 검증은 간단합니다.

# 파드 resolv.conf의 nameserver가 169.254.20.10(예시)로 바뀌었는지 확인
kubectl run -it --rm dns-debug --image=busybox:1.36 --restart=Never -- sh
cat /etc/resolv.conf

# 연속 질의로 지연/타임아웃 관찰
for i in $(seq 1 20); do nslookup kubernetes.default.svc.cluster.local >/dev/null || echo FAIL $i; done

6) 장애 상황에서 바로 쓰는 “DNS 타임아웃” 트러블슈팅 플레이북

아래 순서대로 하면 헤매는 시간을 줄일 수 있습니다.

6-1) 클러스터 DNS 자체가 살아있는지

kubectl -n kube-system get endpoints kube-dns -o wide
kubectl -n kube-system get pod -l k8s-app=kube-dns -o wide
  • endpoints가 비었으면 CoreDNS 파드가 준비되지 않았거나 라벨/셀렉터 문제

6-2) 파드 → kube-dns ClusterIP:53 경로 확인

kubectl run -it --rm netshoot --image=nicolaka/netshoot --restart=Never -- sh

# kube-dns ClusterIP로 직접 질의
KUBEDNS=$(getent hosts kube-dns.kube-system.svc.cluster.local | awk '{print $1}' | head -n1)
nslookup kubernetes.default "$KUBEDNS"

6-3) CoreDNS → 업스트림 경로 확인

CoreDNS가 forward . /etc/resolv.conf라면, CoreDNS 파드 내부에서 업스트림으로 dig를 날려봅니다.

POD=$(kubectl -n kube-system get pod -l k8s-app=kube-dns -o jsonpath='{.items[0].metadata.name}')

kubectl -n kube-system exec -it "$POD" -- sh -lc 'cat /etc/resolv.conf; echo; nslookup amazon.com 169.254.169.253'

여기서 실패하면 SG/NACL/라우팅/conntrack/MTU 쪽으로 이동합니다.

6-4) 특정 노드에서만 실패하는지(노드 단위 격리)

# 문제가 나는 파드가 올라간 노드 확인
kubectl get pod -A -o wide | egrep -i '(<문제파드이름>|coredns)'

# CoreDNS 파드를 다른 노드로 재스케줄(노드 문제 판별)
kubectl -n kube-system delete pod -l k8s-app=kube-dns

노드가 바뀌면 해결된다면, 해당 노드의 conntrack/iptables/CNI/커널 상태를 집중 점검합니다.

7) (보너스) 애플리케이션 관점의 완충: 타임아웃/재시도/캐시

DNS가 불안정할 때 애플리케이션이 즉시 죽어버리면 장애가 증폭됩니다. DNS는 인프라 문제지만, 호출자도 방어적으로 설계해야 합니다.

  • HTTP 클라이언트에 적절한 connect/read timeout
  • 지수 백오프 재시도(무한 재시도 금지)
  • circuit breaker로 DNS 장애 구간의 연쇄 폭발 방지

이런 “타임아웃을 재현하고, 어디서 끊기는지 분해해서, 재시도/폴백을 설계”하는 접근은 OpenAI Responses API 500·503 대응 재시도 폴백 서킷브레이커에서도 같은 결로 다루고 있으니, 운영 관점에서 같이 읽어두면 좋습니다.

결론: 가장 흔한 원인과 가장 빠른 해결 조합

현장에서 가장 자주 맞는 조합은 아래 3가지입니다.

  1. Corefile의 forward 업스트림이 잘못되어 loop 발생forward . 169.254.169.253로 고정 + 롤아웃
  2. CoreDNS 리소스 부족(OOMKilled) + QPS 폭증 → replicas/메모리 상향 + ndots/캐시 점검
  3. 노드 네트워크(conntrack/SG/NACL/MTU) 문제로 업스트림 타임아웃 → 업스트림 dig로 경로 분리 후 노드 튜닝 또는 NodeLocal DNSCache 도입

장애가 진짜로 끝났는지 확인하려면, “CoreDNS 파드가 안정적으로 Running”만 보지 말고 다음을 함께 체크하세요.

  • 파드에서 nslookup kubernetes.default 20~50회 반복 시 실패 0회
  • 외부 도메인(예: amazon.com) 조회 지연이 안정적으로 낮음
  • CoreDNS 메트릭에서 forward 지연/에러율이 정상 범위

이 과정을 한 번 플레이북으로 만들어두면, 다음번 CoreDNS 이슈는 10분 내 원인 분리까지 가능해집니다.