Published on

EKS NodeLocal DNSCache로 DNS 간헐 실패 잡기

Authors

서버리스나 애플리케이션 로직 문제가 아닌데도 EKS에서 갑자기 Temporary failure in name resolution, i/o timeout, SERVFAIL 같은 DNS 오류가 간헐적으로 튀는 경우가 있습니다. 특히 트래픽이 순간적으로 치솟는 배치/크론, 대량의 외부 API 호출, 서비스 디스커버리가 잦은 마이크로서비스 환경에서 빈도가 높아집니다.

이 글에서는 EKS에서 NodeLocal DNSCache를 적용해 DNS 간헐 실패를 줄이는 방법을, 증상 진단 → 원인 구조 → 적용 절차 → 검증/운영 체크리스트 순으로 정리합니다. CoreDNS 자체가 불안정한 경우라면 먼저 EKS CoreDNS CrashLoopBackOff - upstream 타임아웃 해결도 같이 확인하는 것을 권장합니다.

1) 증상: “가끔만” 실패하는 DNS가 더 위험한 이유

간헐 실패는 재현이 어렵고, 애플리케이션 레벨에서 “외부 API가 불안정하다”로 오인되기 쉽습니다. 실제로는 아래처럼 클러스터 내부 DNS 경로가 병목/드롭을 만들 때 발생합니다.

  • Pod 로그
    • dial tcp: lookup xxx on 10.100.0.10:53: i/o timeout
    • Temporary failure in name resolution
  • 애플리케이션 지표
    • 외부 HTTP 요청 실패율이 특정 시점에만 스파이크
    • 재시도 증가로 P99 지연이 급증
  • 네트워크 관찰
    • CoreDNS Pod는 정상 Running인데도 쿼리 타임아웃 발생

이 문제는 단순히 CoreDNS 리소스를 늘린다고 해결되지 않는 경우가 많습니다. 이유는 DNS 요청이 CoreDNS로 가는 “네트워크 경로” 자체가 흔들릴 수 있기 때문입니다.

2) 원인 구조: EKS에서 DNS 경로가 흔들리는 지점

기본적으로 Pod의 /etc/resolv.confkube-dns(CoreDNS 서비스 ClusterIP)를 네임서버로 가리킵니다.

  1. Pod → (iptables/eBPF) → kube-dns ClusterIP
  2. kube-proxy 규칙에 따라 CoreDNS Pod로 로드밸런싱
  3. CoreDNS → upstream(VPC resolver 또는 지정한 forwarder)

여기서 간헐 실패가 생기는 대표 원인은 다음입니다.

2.1 서비스 VIP 경유 + conntrack/iptables 부하

DNS는 짧고 잦은 UDP 트래픽입니다. 대량 Pod가 동시에 DNS를 치면:

  • kube-proxy의 iptables 규칙 매칭 비용 증가
  • 노드 conntrack 테이블 압박(UDP 엔트리 증가)
  • 패킷 드롭/지연 → 타임아웃

2.2 CoreDNS Pod까지의 네트워크 홉 증가

Pod → Service VIP → CoreDNS Pod로 가는 과정 자체가 노드 내/노드 간을 오가며 비용이 늘어납니다. 특히 노드 간 트래픽이 많아지면 지연이 커지고, UDP 특성상 손실 시 재전송이 애매해 실패로 드러나기 쉽습니다.

2.3 upstream(forward) 지연이 “간헐적으로” 길어짐

VPC DNS(169.254.169.253) 또는 사내 DNS로 forwarding할 때 upstream이 순간적으로 느려지면, CoreDNS가 정상이어도 Pod에서 타임아웃이 납니다. 이 경우 CoreDNS의 forward 설정/타임아웃 튜닝도 필요합니다.

3) 해법: NodeLocal DNSCache가 해결하는 것

NodeLocal DNSCache는 노드마다 DNS 캐시(및 로컬 리졸버)를 두고, Pod가 CoreDNS 서비스 VIP가 아니라 노드 로컬 IP로 DNS를 질의하게 만드는 구조입니다.

  • Pod → NodeLocal DNSCache(노드 로컬) → CoreDNS(필요 시)

효과는 실무에서 꽤 명확합니다.

  • Service VIP 경유 감소 → iptables/kube-proxy 경로 부담 감소
  • 노드 로컬로 DNS 응답 → 지연 감소, 드롭 감소
  • 캐시 히트 시 CoreDNS/업스트림 부하 감소
  • CoreDNS가 일시적으로 느려져도 캐시로 완충

즉, “CoreDNS를 더 키우기”보다 DNS 요청 경로 자체를 단순화하는 접근입니다.

4) 적용 전 체크: 내 클러스터에 맞는 전제 조건

적용 전에 아래를 확인하면 시행착오가 줄어듭니다.

  • CoreDNS 서비스 IP(보통 kube-system/kube-dns ClusterIP) 확인
  • 클러스터 DNS 도메인(기본 cluster.local) 확인
  • CNI/네트워크 플러그인(Amazon VPC CNI, Calico 등)과의 호환
  • 노드 OS/AMI에서 iptables 모드(nft/legacy) 이슈 여부

확인 명령:

kubectl -n kube-system get svc kube-dns -o wide
kubectl -n kube-system get cm coredns -o yaml
kubectl get nodes -o wide

5) NodeLocal DNSCache 설치 (EKS 실전 YAML)

AWS 문서/쿠버네티스 SIG에서 제공하는 매니페스트를 기반으로 하되, EKS에서 가장 자주 틀리는 값은 아래 3가지입니다.

  • __PILLAR__DNS__SERVER__: kube-dns ClusterIP
  • __PILLAR__LOCAL__DNS__: 노드 로컬 DNS IP(보통 169.254.20.10)
  • __PILLAR__CLUSTER__DNS__: 클러스터 도메인

5.1 kube-dns ClusterIP와 클러스터 도메인 결정

DNS_SVC_IP=$(kubectl -n kube-system get svc kube-dns -o jsonpath='{.spec.clusterIP}')
echo $DNS_SVC_IP

클러스터 도메인은 CoreDNS ConfigMap에서 확인합니다(보통 cluster.local).

5.2 NodeLocal DNSCache DaemonSet 적용

아래는 “개념 이해용” 축약 예시입니다. 실제 운영에서는 Kubernetes 버전에 맞는 공식 매니페스트를 가져와 치환하세요.

apiVersion: v1
kind: ConfigMap
metadata:
  name: nodelocaldns
  namespace: kube-system
data:
  Corefile: |
    cluster.local:53 {
        errors
        cache {
            success 9984 30
            denial 9984 5
        }
        reload
        loop
        bind 169.254.20.10
        forward . 10.100.0.10 {
            force_tcp
        }
        prometheus :9253
        health 169.254.20.10:8080
    }
    .:53 {
        errors
        cache 30
        reload
        loop
        bind 169.254.20.10
        forward . 10.100.0.10 {
            force_tcp
        }
        prometheus :9253
    }
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: nodelocaldns
  namespace: kube-system
spec:
  selector:
    matchLabels:
      k8s-app: nodelocaldns
  template:
    metadata:
      labels:
        k8s-app: nodelocaldns
    spec:
      hostNetwork: true
      dnsPolicy: Default
      containers:
      - name: node-cache
        image: registry.k8s.io/dns/k8s-dns-node-cache:1.22.28
        args:
        - -localip
        - 169.254.20.10
        - -conf
        - /etc/Corefile
        ports:
        - containerPort: 53
          name: dns
          protocol: UDP
        - containerPort: 53
          name: dns-tcp
          protocol: TCP
        securityContext:
          privileged: true
        volumeMounts:
        - name: config-volume
          mountPath: /etc
      volumes:
      - name: config-volume
        configMap:
          name: nodelocaldns

핵심 포인트

  • bind 169.254.20.10: 노드 로컬 IP에 바인딩
  • forward . 10.100.0.10: 여기의 IP를 kube-dns ClusterIP로 치환
  • force_tcp: UDP 손실이 있는 환경에서 안정성에 도움이 되는 경우가 많습니다(다만 성능/상황에 따라 조정).

적용:

kubectl apply -f nodelocaldns.yaml
kubectl -n kube-system rollout status ds/nodelocaldns
kubectl -n kube-system get pods -l k8s-app=nodelocaldns -o wide

6) Pod가 NodeLocal로 질의하도록 바꾸기 (중요)

NodeLocal DNSCache를 깔아도 Pod가 여전히 kube-dns 서비스 IP로 질의하면 효과가 제한적입니다. 일반적으로는 kubelet의 clusterDNS 설정을 NodeLocal IP로 바꿔야 합니다.

EKS Managed Node Group라면 Launch Template/user-data 또는 AMI 부트스트랩 옵션에서 조정합니다.

6.1 (예시) bootstrap에 --dns-cluster-ip 사용

EKS의 /etc/eks/bootstrap.sh--dns-cluster-ip로 kubelet의 --cluster-dns를 설정할 수 있습니다.

/etc/eks/bootstrap.sh my-cluster \
  --dns-cluster-ip 169.254.20.10

이미 운영 중인 노드 그룹이면 새 노드 그룹을 만들어 점진적으로 교체하는 방식을 권장합니다.

6.2 적용 확인: resolv.conf

임의 Pod에서 확인합니다.

kubectl run -it --rm dns-test --image=busybox:1.36 --restart=Never -- sh
cat /etc/resolv.conf
nslookup kubernetes.default.svc.cluster.local

nameserver 169.254.20.10이 보이면 NodeLocal로 붙은 것입니다.

7) 검증: “간헐 실패”를 수치로 잡는 방법

7.1 반복 쿼리로 실패율 측정

kubectl run -it --rm dns-loop --image=busybox:1.36 --restart=Never -- sh -c '
  i=0; fail=0;
  while [ $i -lt 2000 ]; do
    nslookup kubernetes.default.svc.cluster.local >/dev/null 2>&1 || fail=$((fail+1));
    i=$((i+1));
  done;
  echo "fails=$fail"'

적용 전/후로 비교하면 체감이 아니라 실패율 감소로 확인할 수 있습니다.

7.2 NodeLocal DNSCache 로그/메트릭

kubectl -n kube-system logs -l k8s-app=nodelocaldns --tail=200
kubectl -n kube-system port-forward ds/nodelocaldns 9253:9253

Prometheus를 쓰면 cache_hits, cache_misses, forward_requests 계열을 대시보드로 보는 것이 운영에 도움이 됩니다.

8) 운영 중 자주 만나는 함정과 해결 팁

8.1 로컬 IP 충돌

169.254.20.10은 링크-로컬 대역이라 일반적으로 충돌이 적지만, 조직 표준/보안 정책에 따라 제한될 수 있습니다. 다른 링크-로컬 IP로 바꿀 경우:

  • DaemonSet -localip
  • Corefile bind
  • kubelet clusterDNS

3군데를 반드시 같이 바꿔야 합니다.

8.2 CoreDNS가 원래 불안정한 경우

NodeLocal은 “완충”이지 “치료”가 아닙니다. CoreDNS가 CrashLooping이거나 upstream 타임아웃이 심하면 근본 원인을 먼저 잡아야 합니다. 이 케이스는 위에서 언급한 EKS CoreDNS CrashLoopBackOff - upstream 타임아웃 해결에서 다룬 것처럼 forward 대상, timeout, VPC DNS 경로를 점검하세요.

8.3 네트워크/비용 이슈와 같이 보는 관점

DNS 실패가 외부로 나가는 트래픽 재시도를 유발하면 NAT Gateway 트래픽/비용이 같이 튈 수 있습니다. “DNS가 흔들리던 시간대에 NAT 비용도 튄다”면 VPC NAT Gateway 비용 폭증 10분 진단·절감 관점으로도 상관관계를 확인해보는 게 좋습니다.

8.4 애플리케이션 레벨 방어도 같이 필요

NodeLocal로도 0% 실패가 보장되진 않습니다. 외부 API 의존이 큰 서비스라면 재시도/폴백을 설계해 “DNS/네트워크 순간 장애”를 흡수해야 합니다. 패턴 자체는 OpenAI Responses API 503 멈춤 - 재시도·폴백 설계에서 다룬 방식과 유사합니다(지수 백오프, 최대 재시도 제한, 서킷 브레이커 등).

9) 적용 체크리스트 (요약)

  • kube-dns ClusterIP 확인 후 NodeLocal Corefile forward에 반영
  • NodeLocal IP(예: 169.254.20.10) 결정 및 충돌 여부 확인
  • DaemonSet 정상 배포(모든 노드에 1개 Pod)
  • kubelet clusterDNS를 NodeLocal IP로 변경(새 노드 그룹 교체 권장)
  • Pod /etc/resolv.conf에서 nameserver가 NodeLocal인지 확인
  • 반복 nslookup로 실패율 전/후 비교
  • 메트릭/로그로 cache hit/miss 및 forward 양 관찰

마무리

EKS에서 DNS 간헐 실패는 CoreDNS 자체 문제라기보다, 서비스 VIP 경유/conntrack/iptables/노드 간 홉 같은 “경로 문제”로 발생하는 경우가 많습니다. NodeLocal DNSCache는 이 경로를 노드 로컬로 단축하고 캐시를 제공해, 체감 성능과 안정성을 동시에 끌어올리는 실전적인 해법입니다.

운영 환경에서는 kubelet clusterDNS 변경(노드 교체)까지 포함해야 효과가 확실히 나타납니다. 적용 후에도 실패가 지속된다면 CoreDNS upstream, VPC DNS 경로, 노드 conntrack 한계치까지 함께 점검해보세요.