실제 EKS 클러스터를 운영하다 보면 반복적으로 마주치는 문제 패턴들이 있습니다. 이 글에서는 빈도가 높고, 서비스 장애로 직결되며, 원인-진단-해결 흐름이 단계로 3가지 트러블슈팅 시나리오를 정리해보려 합니다. 명령어와 함께 "왜 이 오류가 발생하는가" 에 집중해서 읽으면 훨씬 도움이 될 것같습니다.
Case 1. Pod CrashLoopBackOff / OOMKilled
1-1. CrashLoopBackOff란 ?
CrashLoopBackOff는 Pod 상태(Status)가 아니라 Kubernetes가 컨테이너를 반복 재시작하는 과정에서
붙이는 이유(Reason) 입니다. 흐름은 아래와 같습니다.
컨테이너 시작 → 비정상 종료(Exit Code != 0) → kubelet이 재시작 시도
→ 또 비정상 종료 → 재시작 간격을 점점 늘림 (Exponential Backoff)
→ 대기 시간이 길어진 상태를 CrashLoopBackOff로 표시
재시작 간격은 10s → 20s → 40s → 80s → 160s → 최대 300s 순으로 증가합니다.
즉, CrashLoopBackOff 자체가 오류 원인이 아니고, 컨테이너가 왜 종료됐는지를 찾아야 합니다.
컨테이너가 비정상 종료되는 원인은 크게 다음 4가지로 정리를 해보았습니다.
원인 설명
OOMKilled
컨테이너에 설정된 메모리 limit을 초과해 커널이 강제 Kill
앱 내부 오류
컨테이너 entrypoint 프로세스가 비정상 종료 (Exit Code 1 등)
Liveness Probe 실패
kubelet이 앱이 죽었다고 판단하여 강제 재시작
ConfigMap / Secret 마운트 실패
의존 리소스가 없어 컨테이너 자체가 시작 불가
OOMKilled: 왜 발생하는가?
📌메모리 Limit과 cgroup의 관계
Kubernetes에서 resources.limits.memory를 설정하면, kubelet은 해당 컨테이너를 위한
Linux cgroup(Control Group) 에 메모리 상한선을 설정합니다. 컨테이너 내부 프로세스가 이 상한을 넘어서는
순간, Linux 커널의 OOM Killer가 해당 프로세스에 SIGKILL(Signal 9)을 보내 강제 종료해요.
이때 Exit Code는 128 + 9 = 137 이 됩니다. (128 + signal number)
JVM Heap 설정 미스: Java 앱은 -Xmx로 Heap을 제한해도 Off-Heap(메타스페이스, 네이티브 메모리, 스레드 스택)이 추가로 사용됩니다. -Xmx512m을 설정해도 실제 프로세스 메모리는 훨씬 클 수 있다.
메모리 누수: 앱 코드의 버그로 메모리가 점점 증가
캐시 설정 미스: Redis 클라이언트, 커넥션 풀 등이 메모리를 과다하게 선점
limits 미설정: limits를 설정하지 않으면 노드 전체 메모리를 쓸 수 있어 노드 전체가 불안정해진다.
Liveness Probe 실패로 인한 재시작: 왜 발생하는가?
Probe는 kubelet이 컨테이너의 상태를 주기적으로 체크하는 메커니즘입니다.
3가지 종류가 있고 각각 역할이 다르며, Probe 종류 역할 실패 시 결과를 표로 정리해보았습니다.
startupProbe
앱이 최초 시작 완료됐는지 확인
실패 시 컨테이너 재시작. 성공 전까지 liveness/readiness 비활성화
livenessProbe
앱이 살아있는지(데드락 감지) 확인
실패 시 컨테이너 재시작
readinessProbe
앱이 트래픽을 받을 준비가 됐는지 확인
실패 시 Service Endpoints에서 제거 (재시작 없음)
CrashLoopBackOff를 유발하는 Probe 오류 패턴
패턴 1: livenessProbe의 initialDelaySeconds가 앱 부팅 시간보다 짧은 경우
Spring Boot 같은 JVM 앱은 부팅에 30~60초가 걸리는 경우가 있습니다.
initialDelaySeconds: 5 로 설정하면, 앱이 아직 시작 중인 5초 시점에 kubelet이 /health 를 찔러요
당연히 404 또는 Connection Refused가 나고, kubelet은 앱이 죽었다고 판단해 재시작합니다.
앱이 부팅될 새도 없이 계속 재시작되는 무한루프가 돌게돼요.
앱 부팅 시작 (0초)
→ livenessProbe 실행 (5초) → 아직 부팅 중 → 실패
→ failureThreshold(3회) 도달
→ kubelet이 컨테이너 Kill → 재시작
→ 반복 → CrashLoopBackOff
패턴 2: livenessProbe에 외부 의존성(DB 등)을 포함한 경우
/health 엔드포인트 구현 시 DB 연결을 체크하는 로직을 넣으면, DB가 잠깐 느려지거나
네트워크 지연이 생길 때 멀쩡한 앱 Pod 전체가 재시작됩니다.
DB 일시적 응답 지연 (3초)
→ livenessProbe timeout (3초) → 실패
→ failureThreshold 도달 → 모든 Pod 재시작
→ 재시작된 Pod들도 DB에 연결 시도 → DB 부하 폭증
→ 더 많은 Pod 재시작 → Cascading Failure
이게 바로 Cascading Failure(연쇄 장애)다. livenessProbe는 반드시
앱 자체의 상태(데드락, 무한루프)만 체크해야 합니다.
1-2 진단: 원인 찾기
# STEP 1: 비정상 Pod 목록 확인
kubectl get pods -n <namespace> | grep -v Running | grep -v Completed
# STEP 2: 이전 컨테이너 종료 원인 확인 (가장 중요)
kubectl describe pod <pod-name> -n <namespace>
# 아래 필드를 집중해서 본다:
# - Last State.Reason: OOMKilled 또는 Error
# - Last State.Exit Code: 137(OOM), 1(앱 오류), 143(SIGTERM)
# - Events: Warning Unhealthy → Probe 실패 메시지
# STEP 3: 이전 크래시 로그 확인
# CrashLoopBackOff 상태에서는 컨테이너가 이미 죽었으므로 --previous 플래그 필수
kubectl logs <pod-name> -n <namespace> --previous
# 멀티 컨테이너 Pod의 경우
kubectl logs <pod-name> -n <namespace> -c <container-name> --previous
# STEP 4: 현재 리소스 사용량 확인
kubectl top pod <pod-name> -n <namespace>
# STEP 5: 설정된 resource limits 확인
kubectl get pod <pod-name> -n <namespace> \
-o jsonpath='{.spec.containers[*].resources}'
Events:
Warning Unhealthy 3m kubelet
Liveness probe failed: HTTP probe failed with statuscode: 503
Warning Killing 3m kubelet
Container app failed liveness probe, will be restarted
1-3. 해결
OOMKilled 해결 : resource limits 조정
# deployment.yaml
containers:
- name: app
resources:
requests:
memory: "256Mi" # 스케줄링 기준 (이 메모리가 있는 노드에 배치)
cpu: "250m"
limits:
memory: "512Mi" # 이 값을 초과하면 OOMKilled 발생
cpu: "500m"
# 파일로 적용
kubectl apply -f deployment.yaml
# 즉시 patch (긴급 대응 시)
kubectl patch deployment <deployment-name> -n <namespace> \
--type='json' \
-p='[{"op":"replace","path":"/spec/template/spec/containers/0/resources/limits/memory","value":"512Mi"}]'
주의: limits를 늘리는 건 임시 조치입니다.
근본 원인(메모리 누수, JVM Heap 설정)은 반드시 앱 레벨에서 분석해야 해요!
JVM 앱이라면 컨테이너 메모리 limit에 맞게 -XX:MaxRAMPercentage=75 옵션 사용을 권장합니다.
Probe 설정 교정 : 3단계 Probe 패턴
containers:
- name: app
image: my-spring-app:latest
# [1단계] startupProbe: 앱이 완전히 기동될 때까지 기다린다
# failureThreshold(30) × periodSeconds(10) = 최대 300초 대기
# 이 Probe가 성공하기 전까지 liveness/readiness는 실행되지 않는다
startupProbe:
httpGet:
path: /actuator/health
port: 8080
failureThreshold: 30
periodSeconds: 10
# [2단계] readinessProbe: 트래픽 수신 준비 여부 확인
# 실패하면 재시작이 아니라 Service Endpoints에서 제거됨
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
# [3단계] livenessProbe: 데드락 감지 전용
# 절대로 DB 연결, 외부 API 체크를 넣지 않는다
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
실행 중인 Pod 내부 디버깅 : Ephemeral Container
컨테이너가 distroless 이미지처럼 쉘이 없거나, CrashLoopBackOff 상태라서 exec가 안 될 때:
# 실행 중인 Pod에 디버깅 컨테이너 붙이기
# nicolaka/netshoot: curl, dig, tcpdump 등 네트워크 도구 모음
kubectl debug <pod-name> -it \
--image=nicolaka/netshoot \
--target=<container-name>
# Pod 복제 후 이미지 교체 (원본 Pod 유지)
kubectl debug <pod-name> \
--copy-to=debug-pod \
--image=ubuntu
# 노드 자체에 접근 (호스트 파일시스템은 /host 에 마운트됨)
kubectl debug node/<node-name> -it --image=ubuntu
📌요약 체크리스트
□ kubectl describe pod → Last State.Reason 확인
□ Exit Code 137 → OOMKilled → memory limits 상향
□ kubectl logs --previous → 이전 컨테이너 로그 확인
□ READY 0/1 이면 readinessProbe 실패 → 재시작 없음, Endpoint에서 제거된 것
□ RESTARTS 증가 + Running → livenessProbe 실패로 재시작 중
□ JVM 앱 → startupProbe 필수, initialDelaySeconds 충분히 확보
□ livenessProbe에 DB 연결 체크 포함 여부 확인 → 즉시 제거
Case 2. Service Endpoint 없음 / Pod 간 통신 불가
📌 Kubernetes Service가 트래픽을 전달하는 원리
Service가 실제로 어떻게 동작하는지 이해해야 Endpoint가 왜 비어있는지 알 수 있습니다.
[클라이언트 Pod]
↓ (1) DNS 조회: web-service.default.svc.cluster.local
[CoreDNS] → ClusterIP 반환 (예: 10.100.50.10)
↓ (2) ClusterIP로 패킷 전송
[노드 커널 iptables / ipvs]
↓ (3) kube-proxy가 설정한 규칙으로 실제 Pod IP로 DNAT
[백엔드 Pod IP] (예: 192.168.1.5)
여기서 핵심은 (3) kube-proxy가 어떤 Pod IP로 DNAT할지를 결정하는 데이터가 바로 Endpoints 라는 것!
Kubernetes의 Endpoints Controller는 다음 조건을 모두 만족하는 Pod만 Endpoints에 등록합니다.
Pod의 label이 Service의 selector와 정확히 일치
Pod이 Ready 상태 (readinessProbe 성공)
Pod이 Running 상태
따라서 Endpoints가 <none> 이면 위 3가지 중 하나 이상이 어긋난 것이다.
2-1. Label Selector 불일치
왜 발생하는가?
Service는 selector 필드에 지정한 label key-value를 모두 가진 Pod에만 트래픽을 보냅니다.
label은 문자열 완전 일치이기 때문에 오타 한 글자, 대소문자 차이, 키 이름 불일치로 Endpoints가 비어버립니다.
자주 발생하는 오타 패턴:
Service selector: version=v1
Pod label: ver=v1 ← 키 이름이 "ver" vs "version"
Service selector: app=web-server
Pod label: app=webserver ← 하이픈 유무
Service selector: env=production
Pod label: env=prod ← 값 축약
이런 불일치는 팀원이 각자 Deployment와 Service를 만들거나,
Helm chart 값을 일부만 오버라이드할 때 자주 발생합니다.
📌 진단
# Service selector 확인
kubectl get svc <service-name> -n <namespace> \
-o jsonpath='{.spec.selector}'
# 출력: {"app":"web","version":"v1"}
# 실제 Pod label 확인 (selector 값과 직접 비교)
kubectl get pods -n <namespace> --show-labels
# NAME READY STATUS LABELS
# web-abc 1/1 Running app=web,ver=v1 ← "version" 아닌 "ver"
# selector 기준으로 매칭되는 Pod이 있는지 직접 확인
kubectl get pods -n <namespace> -l app=web,version=v1
# No resources found ← 없으면 selector 문제
📌 해결
# Service selector를 Pod label에 맞춰 수정
kubectl patch svc <service-name> -n <namespace> \
-p '{"spec":{"selector":{"app":"web","ver":"v1"}}}'
# 또는 Deployment의 label을 Service selector에 맞춰 수정
# spec.template.metadata.labels 를 수정하고 apply
# (이 경우 Rolling Update가 트리거됨)
2-2 port / targetPort 불일치
왜 발생하는가?
Service에는 두 가지 포트 개념이 있습니다.
port: Service가 클라이언트에게 노출하는 포트 (ClusterIP:port)
targetPort: 실제 Pod 컨테이너가 리스닝하는 포트 (트래픽이 최종 도달하는 곳)
targetPort가 실제 컨테이너 포트와 다르면, kube-proxy는 올바른 Pod IP로 패킷을 전달하지만
Pod 내 프로세스가 해당 포트를 듣고 있지 않아 Connection Refused가 발생합니다.
Endpoints는 정상적으로 채워져 있어서 DNS/Service 설정 문제로 오해하기 쉬워요 !
# 현재 Service 포트 설정 확인
kubectl get svc <service-name> -n <namespace> -o yaml | grep -A10 ports
# ports:
# - port: 80
# targetPort: 8080 ← 이게 실제 컨테이너 포트와 다르면 Connection Refused
# Pod이 실제로 리스닝하는 포트 확인
kubectl get pod <pod-name> \
-o jsonpath='{.spec.containers[*].ports[*].containerPort}'
# 9090 ← 실제는 9090인데 Service는 8080으로 설정됨
# 또는 Pod 내부에서 직접 확인
kubectl exec <pod-name> -- ss -tlnp
# LISTEN 0 128 *:9090 *:*
이 설계는 의도적이에요. 아직 초기화 중이거나, 일시적으로 과부하 상태인 Pod에게 트래픽을 보내지 않기 위해서다.
# Endpoints에 등록된 Pod IP 확인
kubectl get endpoints <service-name> -n <namespace>
# NAME ENDPOINTS
# web-service 192.168.1.5:8080, 192.168.2.3:8080 ← 정상
# web-service <none> ← 모든 Pod이 NotReady
# Pod의 READY 상태 확인
kubectl get pods -n <namespace> -o wide
# NAME READY STATUS NODE
# web-abc 0/1 Running ip-10-0-1-1 ← 0/1: Running이지만 NotReady
# readinessProbe 실패 원인 확인
kubectl describe pod <pod-name> -n <namespace> | grep -A5 "Readiness"
# Readiness probe failed: HTTP probe failed with statuscode: 404
2-4. LoadBalancer가 Pending인 경우
왜 발생하는가?
EKS에서 type: LoadBalancer 또는 Ingress를 생성하면 실제 AWS ELB/ALB를 프로비저닝해야 한다.
이 작업은 AWS Load Balancer Controller가 담당합니다.
이 Controller가 없거나, AWS API를 호출할 권한(IRSA)이 부족하면 LoadBalancer는 영원히 Pending 상태다.
자주 발생하는 원인을 아래와 같이 정리해보았습니다.
📌 원인 증상
AWS LB Controller 미설치
EXTERNAL-IP가 <pending>으로 고정
IRSA 권한 부족
LB Controller 로그에 AccessDenied
서브넷 태그 누락
Controller가 어느 서브넷에 LB를 만들지 모름
Annotation 오타
ALB 대신 NLB가 만들어지거나 아무것도 안 됨
# Service 상태 확인
kubectl get svc -n <namespace>
# TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
# LoadBalancer 10.100.50.10 <pending> 80:30001/TCP
# LB Controller Pod 확인
kubectl get pods -n kube-system \
-l app.kubernetes.io/name=aws-load-balancer-controller
# LB Controller 로그에서 오류 확인
kubectl logs -n kube-system \
-l app.kubernetes.io/name=aws-load-balancer-controller \
--tail=100 | grep -i "error\|denied\|failed"
# 서브넷 태그 확인 (외부 LB용 태그)
aws ec2 describe-subnets \
--subnet-ids <subnet-id> \
--query 'Subnets[].Tags[?Key==`kubernetes.io/role/elb`]'
# 값이 없으면 LB Controller가 서브넷을 인식 못 함
# 필요 시 서브넷 태그 추가
aws ec2 create-tags \
--resources <subnet-id> \
--tags Key=kubernetes.io/role/elb,Value=1
2-5. NetworkPolicy가 트래픽 차단
왜 발생하는가?
Kubernetes NetworkPolicy는 Pod 간 트래픽을 L3/L4 레벨에서 제어합니다.
한 번 Default Deny 정책이 적용되면, 명시적으로 허용하지 않은 모든 트래픽은 차단됩니다 !
문제는 AND / OR 로직을 YAML indent로 구분하기 때문에 작성 실수가 잦다는 것이에요.
# 네임스페이스에 적용된 NetworkPolicy 확인
kubectl get networkpolicy -n <namespace>
# 특정 Policy 내용 확인
kubectl describe networkpolicy <policy-name> -n <namespace>
AND vs OR 혼동 — indent 한 칸 차이가 완전히 다른 정책
# [AND 로직]: "alice 네임스페이스" AND "role=client 라벨" 둘 다 만족해야 허용
# namespaceSelector와 podSelector가 같은 블록(-)에 있다
ingress:
- from:
- namespaceSelector:
matchLabels:
user: alice
podSelector: # ← 하이픈(-) 없이 같은 레벨 = AND
matchLabels:
role: client
# [OR 로직]: "alice 네임스페이스" OR "role=client 라벨" 하나만 만족해도 허용
# 각 selector가 별도 블록(-)으로 분리되어 있다
ingress:
- from:
- namespaceSelector:
matchLabels:
user: alice
- podSelector: # ← 하이픈(-) 있음 = OR (별도 항목)
matchLabels:
role: client
의도한 것보다 넓은 OR 정책이 적용되거나, 반대로 좁은 AND 정책으로 인해
트래픽이 차단되는 경우 모두 발생합니다.
# NetworkPolicy 테스트: netshoot으로 연결 직접 확인
kubectl run tmp-shell --rm -i --tty \
--image nicolaka/netshoot -- bash
# 내부에서:
# DNS 해석 확인
dig web-service.default.svc.cluster.local
# HTTP 연결 테스트
curl -v http://web-service.default.svc.cluster.local:80/health
# TCP 포트 연결 확인
nc -zv web-service.default.svc.cluster.local 80
# 패킷 캡처 (특정 Pod IP로의 트래픽)
tcpdump -i any host -n
📌 요약 체크리스트
□ kubectl get endpoints → <none> 확인
□ Service selector vs Pod label 직접 비교 (오타, 키 이름 불일치)
□ kubectl get pods -l <selector> → 매칭 Pod 직접 조회
□ Pod READY 상태 확인 (0/1 이면 Endpoints에서 제외됨)
□ Service targetPort vs 실제 containerPort 비교
□ LoadBalancer Pending → LB Controller 설치 및 로그 확인
□ 서브넷 태그 (kubernetes.io/role/elb: 1) 존재 여부
□ NetworkPolicy AND/OR 로직 indent 확인
□ netshoot으로 내부에서 직접 curl / dig 테스트
Case 3. CoreDNS 장애 / DNS 해석 실패
📌 EKS DNS 아키텍처 이해
DNS가 왜 장애나는지 알려면 EKS에서 DNS가 어떻게 동작하는지 먼저 알아야 한다.
[Pod 내부 앱]
↓ DNS 쿼리 (예: web-service.default.svc.cluster.local)
[Pod의 /etc/resolv.conf]
nameserver: 10.100.0.10 ← kube-dns Service IP (CoreDNS)
↓
[CoreDNS Pod (kube-system)]
- 클러스터 내부 도메인 → 직접 응답
- 외부 도메인 → VPC DNS Resolver로 forward
↓ (외부 도메인인 경우)
[VPC DNS Resolver] (169.254.169.253 또는 VPC+2 주소)
↓
[외부 DNS / Route53]
여기서 장애 포인트는 3곳입니다.
CoreDNS Pod 자체가 죽는 경우 → 클러스터 전체 DNS 해석 불가
ndots:5로 인한 쿼리 폭증 → CoreDNS / VPC DNS 과부하
VPC DNS throttling → ENI 한도 초과로 DNS 응답 지연
3-1. CoreDNS OOMKilled
왜 발생하는가?
CoreDNS는 클러스터 모든 Pod의 DNS 쿼리를 처리합니다. 클러스터 규모가 커질수록
쿼리 수가 비례해서 증가하고, 기본 메모리 limits(170Mi)로는 감당이 안 되는 시점이 오겠죠 ?
특히 아래 상황에서 CoreDNS OOM이 자주 발생한다:
Pod 수가 급격히 증가하는 경우 (오토스케일링, 배포 러시)
ndots:5 설정으로 불필요한 쿼리가 수배 증폭되는 경우
DNS TTL이 너무 짧아 캐싱이 안 되는 경우
CoreDNS가 OOMKilled 되면 잠시 재시작하는 동안 클러스터 전체의 DNS 해석이 실패합니다.
nslookup, curl 이 모두 실패하고, 앱에서 dial tcp: lookup ... no such host 오류가 대량 발생해요.
# CoreDNS 상태 확인
kubectl get pods -n kube-system -l k8s-app=kube-dns
# NAME READY STATUS RESTARTS
# coredns-5d78c9869d-abc12 0/1 OOMKilled 5
# OOMKilled 확인
kubectl describe pods -n kube-system -l k8s-app=kube-dns \
| grep -A5 "Last State"
# Last State: Terminated
# Reason: OOMKilled
# 현재 메모리 사용량 확인
kubectl top pods -n kube-system -l k8s-app=kube-dns
# NAME CPU MEMORY
# coredns-xxx 50m 160Mi ← limits(170Mi)에 근접
# CoreDNS 로그에서 오류 확인
kubectl logs -n kube-system -l k8s-app=kube-dns --tail=50
📌 해결
# CoreDNS 메모리 limits 증가
kubectl set resources deployment coredns -n kube-system \
--limits=memory=300Mi \
--requests=memory=100Mi
# 즉시 Rolling Restart
kubectl rollout restart deployment coredns -n kube-system
# 재시작 완료 확인
kubectl rollout status deployment coredns -n kube-system
# deployment "coredns" successfully rolled out
3-2. ndots:5로 인한 불필요한 DNS 쿼리 폭증
ndots:5는 왜 존재하는가?
ndots:5는 Kubernetes 클러스터 내부 서비스 이름 해석을 편리하게 하기 위해 존재합니다.
web-service 처럼 짧은 이름을 쿼리하면, /etc/resolv.conf의
search domain을 붙여서 FQDN을 만들어 시도해요.
web-service
→ web-service.default.svc.cluster.local ← 성공! (클러스터 내부 서비스)
.이 5개 미만인 도메인은 search domain을 먼저 시도하도록 ndots:5로 설정하면,
web-service.default.svc.cluster.local 처럼 .이 5개인 FQDN도 한 번에 해석할 수 있다.
max_concurrent 1000 → max_concurrent 5000 : 대규모 클러스터에서 동시 외부 쿼리 증가
# ConfigMap 수정
kubectl edit configmap coredns -n kube-system
# 수정 후 CoreDNS 재시작 (reload 플러그인으로 자동 반영되지만 확실하게)
kubectl rollout restart deployment coredns -n kube-system
3-5. DNS 디버깅 명령어 모음
# DNS 기본 테스트 (Pod 내부에서 실행)
kubectl exec <pod-name> -- nslookup kubernetes.default
kubectl exec <pod-name> -- nslookup <service-name>.<namespace>.svc.cluster.local
kubectl exec <pod-name> -- cat /etc/resolv.conf
# netshoot으로 상세 분석
kubectl run dns-debug --rm -i --tty \
--image nicolaka/netshoot -- bash
# 내부에서:
# DNS 응답 시간 및 쿼리 경로 확인
dig kubernetes.default.svc.cluster.local +stats
# search domain 포함 실제 쿼리 순서 확인
dig api.example.com +search +showsearch
# CoreDNS Pod에 직접 쿼리 (CoreDNS IP 확인 후 사용)
kubectl get svc kube-dns -n kube-system
dig @<coredns-clusterip> kubernetes.default.svc.cluster.local
# DNS 쿼리 추적
dig +trace api.example.com
# 소켓 레벨 DNS 확인
ss -tulnp | grep :53
📌 요약 체크리스트
□ kubectl get pods -n kube-system -l k8s-app=kube-dns → Running 여부
□ kubectl top pods → CoreDNS 메모리 limits(기본 170Mi) 근접 여부
□ OOMKilled → memory limits 증가 후 rollout restart
□ 외부 API 호출이 많은 앱 → ndots:2 설정 또는 FQDN trailing dot
□ Pod resolv.conf 확인 → options ndots 값 점검
□ 클러스터 규모 크고 DNS 간헐적 실패 → VPC DNS throttling 의심
→ NodeLocal DNSCache 설치 검토
□ CoreDNS ConfigMap cache TTL / max_concurrent 값 점검
□ netshoot으로 dig +stats / +search +showsearch 테스트
📌 전체 First 5 Minutes 체크리스트
장애 발생 시 가장 먼저 실행할 명령어 세트를 아래와 같이 정리해보았습니다.
# [30초] 클러스터 전체 상태
aws eks describe-cluster --name <cluster-name> \
--query 'cluster.status' --output text
kubectl get nodes
kubectl get pods --all-namespaces | grep -v Running | grep -v Completed
# [2분] 이벤트 스코프 파악
kubectl get events --all-namespaces \
--sort-by='.lastTimestamp' | tail -20
kubectl get pods -n <namespace> --no-headers \
| awk '{print $3}' | sort | uniq -c | sort -rn
# 노드별 비정상 Pod 분포
kubectl get pods --all-namespaces -o wide \
--field-selector=status.phase!=Running \
| awk 'NR>1 {print $8}' | sort | uniq -c | sort -rn
# [5분] 문제 Pod 상세 분석
kubectl describe pod <pod-name> -n <namespace>
kubectl logs <pod-name> -n <namespace> --previous
kubectl top nodes
kubectl top pods -n <namespace> --sort-by=cpu