- Published on
EKS 노드그룹 업그레이드 드레인 멈춤 점검법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
EKS Managed Node Group(이하 MNG) 업그레이드나 AMI 롤링 업데이트를 돌리면, 어느 순간부터 노드가 Draining 상태에서 멈춘 것처럼 보이는 경우가 있습니다. 실제로는 Kubernetes가 안전한 축출(eviction) 을 지키느라 멈춘 것이고, 그 “안전 조건”을 가장 자주 막는 게 PDB, DaemonSet, local PV(노드 로컬 디스크) 입니다.
이 글은 “왜 멈추는지”를 원인별로 분해하고, 무중단을 최대한 유지하면서 드레인을 끝내는 방법을 체크리스트 형태로 정리합니다.
드레인이 멈추는 메커니즘: EKS가 하는 일
MNG 업그레이드 시 EKS는 대체로 다음을 반복합니다.
- 새 노드(새 AMI/새 버전) 생성
- 기존 노드
cordon처리(스케줄링 금지) - 기존 노드
drain수행(파드 축출) - 기존 노드 종료
여기서 핵심은 drain이 단순 강제 종료가 아니라 Eviction API 를 통해 “지금 이 파드를 내보내도 되는가?”를 Kubernetes에 질의한다는 점입니다. 이때 Kubernetes가 PDB나 볼륨 제약 때문에 “안 된다”고 판단하면, 노드는 계속 드레인 중으로 남습니다.
1) 가장 흔한 원인: PDB가 축출을 막는다
증상
- 특정 Deployment/StatefulSet 파드가 계속 노드에 남아 있음
- 이벤트에
Cannot evict pod as it would violate the pod's disruption budget류 메시지
빠른 진단 커맨드
# 드레인 중인 노드에서 남아있는 파드 확인
kubectl get pod -A -o wide --field-selector spec.nodeName=$NODE
# PDB 전체 현황(허용 가능한 축출 수가 0이면 위험 신호)
kubectl get pdb -A
# 특정 PDB 상세
kubectl describe pdb -n $NS $PDB
# 파드 이벤트에서 PDB 관련 에러 확인
kubectl describe pod -n $NS $POD | sed -n '1,200p'
kubectl get pdb 결과에서 특히 봐야 할 컬럼은 다음입니다.
MIN AVAILABLE또는MAX UNAVAILABLEALLOWED DISRUPTIONS(이 값이0이면 eviction이 막힐 가능성이 큼)
왜 이런 일이 생기나
대표 패턴은 아래와 같습니다.
- 레플리카가
1인데 PDB가minAvailable: 1 - 레플리카가
2인데 PDB가minAvailable: 2 - HPA가 축소해버려서 현재 레플리카 수가 줄었는데 PDB는 그대로
즉, PDB가 “항상 이만큼은 살아있어야 한다”고 말하는데, 현재 클러스터 상태가 그 조건을 만족할 여유가 없으면 드레인은 멈춥니다.
안전한 해소 방법(우선순위)
(1) 레플리카를 늘려 ALLOWED DISRUPTIONS 만들기
kubectl -n $NS scale deploy/$DEPLOY --replicas=3
kubectl -n $NS rollout status deploy/$DEPLOY
kubectl -n $NS get pdb
(2) 업그레이드 윈도우 동안만 PDB를 완화
예: minAvailable을 낮추거나 maxUnavailable을 늘립니다.
# 예시: maxUnavailable을 1로 설정(상황에 맞게 조정)
kubectl -n $NS patch pdb $PDB --type=merge -p '{"spec":{"maxUnavailable":1}}'
(3) 최후의 수단: PDB 삭제 후 복구
운영 리스크가 크므로, 반드시 변경 이력과 복구 계획을 함께 가져가야 합니다.
kubectl -n $NS get pdb $PDB -o yaml > /tmp/$PDB.yaml
kubectl -n $NS delete pdb $PDB
# 업그레이드/드레인 종료 후 복구
kubectl apply -f /tmp/$PDB.yaml
2) DaemonSet 파드는 기본적으로 드레인 대상이 아니다
증상
- 드레인 중 노드에 DaemonSet 파드가 남아 있어도 정상처럼 보이기도 함
- 하지만 “남아있는 파드가 있어서 드레인이 끝나지 않는다”고 오해하는 경우가 잦음
Kubernetes의 drain은 기본적으로 DaemonSet 파드를 무시합니다(옵션에 따라 다름). 다만, 다음 케이스는 드레인을 실제로 방해할 수 있습니다.
- DaemonSet 파드가
emptyDir나 특정 마운트로 인해 종료가 지연 terminationGracePeriodSeconds가 과도하게 큼- DaemonSet이 아닌데 DaemonSet처럼 동작하도록 구성된 특수 워크로드
진단
# 해당 노드에서 DaemonSet 파드만 추려보기
kubectl get pod -A -o wide --field-selector spec.nodeName=$NODE \
| grep -E 'daemonset|node-exporter|fluent|cni|aws'
# 파드가 어떤 컨트롤러 소유인지 확인
kubectl -n $NS get pod $POD -o jsonpath='{.metadata.ownerReferences[0].kind}{"\n"}'
해소 포인트
- DaemonSet 자체는 “남아도 되는 파드”인 경우가 많으니, 드레인이 정말 막혔는지 먼저 확인합니다.
- 드레인이 막혔다면, 이벤트/종료 지연 원인을 봐야 합니다.
kubectl -n $NS describe pod $POD | sed -n '1,220p'
그리고 정말로 업그레이드 윈도우 동안 DaemonSet을 내려야 한다면(예: 커스텀 에이전트가 종료를 방해) 다음처럼 nodeSelector나 toleration을 조정해 대상 노드에서만 비활성화하는 방식이 일반적으로 더 안전합니다.
3) LocalPV(노드 로컬 볼륨)가 축출을 “불가능”하게 만든다
증상
- StatefulSet 파드가 특정 노드에 강하게 고정
- 다른 노드로 재스케줄이 안 되면서 eviction이 계속 실패
- EBS 같은 네트워크 볼륨은 이동되는데, local PV는 이동이 안 됨
LocalPV는 말 그대로 “그 노드의 디스크”에 데이터가 있으므로, 파드를 다른 노드로 옮기면 데이터가 사라지거나(혹은 접근 불가) 애플리케이션이 깨집니다. 그래서 드레인 과정에서 매우 자주 병목이 됩니다.
진단: 남아있는 파드가 어떤 PV를 쓰는지 확인
# 파드가 마운트하는 PVC 이름 확인
kubectl -n $NS get pod $POD -o jsonpath='{.spec.volumes[*].persistentVolumeClaim.claimName}{"\n"}'
# PVC가 바인딩된 PV 확인
kubectl -n $NS get pvc $PVC -o wide
# PV의 타입/노드 어피니티 확인(local인지, 특정 노드에 묶였는지)
kubectl get pv $PV -o yaml | sed -n '1,260p'
PV 스펙에서 local: 이 보이거나, nodeAffinity로 특정 노드(또는 특정 라벨)로 강하게 묶여 있으면 LocalPV일 가능성이 큽니다.
해소 전략(데이터/서비스 특성에 따라 선택)
(1) 설계를 바꾸는 것이 정답인 경우
- 데이터가 중요한 상태 저장 서비스라면, LocalPV 기반 단일 노드 고정은 업그레이드 자동화와 충돌합니다.
- 가능하면 EBS CSI 같은 네트워크 스토리지로 전환하거나, 애플리케이션 레벨 복제(예: DB replica)로 노드 교체를 흡수하도록 설계합니다.
(2) 유지보수 윈도우에서 “의도된 다운타임”으로 처리
LocalPV 기반 StatefulSet을 가진 노드그룹은, 업그레이드 시 해당 워크로드를 계획적으로 중단하고 재기동하는 절차가 필요할 수 있습니다.
예시 흐름:
# 1) 트래픽 차단(서비스 라우팅/인그레스/배치 중단 등)
# 2) StatefulSet 스케일 다운
kubectl -n $NS scale statefulset/$STS --replicas=0
# 3) 노드 드레인 재시도(혹은 노드그룹 업그레이드 재개)
# 4) 업그레이드 후 스케일 업
kubectl -n $NS scale statefulset/$STS --replicas=1
(3) 노드그룹을 “분리”해서 업그레이드 blast radius 줄이기
- LocalPV 워크로드 전용 노드그룹을 따로 두면, 다른 stateless 서비스 업그레이드가 LocalPV 때문에 멈추는 상황을 줄일 수 있습니다.
4) 드레인 멈춤을 빠르게 특정하는 실전 체크리스트
아래 순서대로 보면 대부분 원인이 10분 내에 좁혀집니다.
1) 해당 노드에 남아있는 파드 목록부터 확정
kubectl get pod -A -o wide --field-selector spec.nodeName=$NODE
2) 남은 파드가 어떤 컨트롤러 소유인지 분류
kubectl -n $NS get pod $POD -o jsonpath='{.metadata.ownerReferences[0].kind}{"/"}{.metadata.ownerReferences[0].name}{"\n"}'
DaemonSet이면 “원래 남아도 되는지”부터 확인ReplicaSet/StatefulSet이면 PDB/PV 의심
3) 이벤트에서 eviction 실패 이유를 확인
kubectl -n $NS describe pod $POD | sed -n '1,260p'
4) PDB 확인
kubectl get pdb -A
kubectl -n $NS describe pdb $PDB
5) PVC/PV 확인(LocalPV 여부)
kubectl -n $NS get pvc
kubectl get pv
5) 업그레이드 전에 “드레인 멈춤”을 예방하는 설정 팁
PDB를 레플리카 수와 함께 설계하기
- 레플리카가
1인 서비스에minAvailable: 1은 사실상 “축출 금지”입니다. - 무중단이 정말 필요하면 레플리카를 늘리고, 그에 맞춰 PDB를 잡아야 합니다.
예시 PDB:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api-pdb
spec:
maxUnavailable: 1
selector:
matchLabels:
app: api
LocalPV 워크로드는 노드그룹을 분리
- stateless 노드그룹 업그레이드가 LocalPV 때문에 멈추는 연쇄 장애를 줄입니다.
드레인 관찰을 자동화
- 업그레이드 작업 중 “어떤 파드가 마지막까지 남는지”를 자동으로 수집해두면 다음 장애가 크게 줄어듭니다.
# 5초마다 남은 파드 스냅샷
watch -n 5 "kubectl get pod -A -o wide --field-selector spec.nodeName=$NODE"
6) 자주 나오는 오해 3가지
오해 1: 드레인이 멈췄으니 --force로 밀어버리면 된다
강제 삭제는 애플리케이션 데이터 손상, 리더 선출 꼬임, 트래픽 오류를 유발할 수 있습니다. 특히 Stateful 워크로드와 LocalPV는 강제 종료의 대가가 큽니다.
오해 2: DaemonSet 파드가 남아 있으니 드레인이 실패한 것이다
DaemonSet은 남아도 정상인 경우가 많습니다. “실제로 막는 파드”가 무엇인지 이벤트와 PDB로 확인해야 합니다.
오해 3: PDB는 무조건 넣을수록 좋다
PDB는 무중단을 돕지만, 레플리카/오토스케일/업그레이드 전략과 함께 설계되지 않으면 “업그레이드 불능 장치”가 됩니다.
마무리: 드레인이 멈춘 게 아니라, 안전장치가 작동한 것이다
EKS 노드그룹 업그레이드에서 드레인이 멈추는 상황은 대부분 Kubernetes가 PDB와 스토리지 제약을 지키느라 생깁니다. 해결의 핵심은 “강제로 밀기”가 아니라, 어떤 안전조건이 발목을 잡는지 빠르게 특정하고, 그 조건을 일시적으로 완화하거나(=PDB), 구조적으로 제거(=LocalPV 설계 변경/노드그룹 분리) 하는 것입니다.
관련 운영 이슈로 IAM/OIDC 설정이 꼬여 노드 교체 후 권한 문제가 같이 터지는 경우도 있어, IRSA가 의심되면 아래 글도 함께 점검해보면 좋습니다.
업그레이드가 자주 멈추는 워크로드(특정 네임스페이스/특정 StatefulSet)가 있다면, 댓글/후속 글 주제로 “현재 PDB, 레플리카, PV 타입” 조합을 기준으로 더 구체적인 처방전 형태로도 정리할 수 있습니다.