지난주에 EKS 1.32 클러스터 노드 그룹 롤링 업그레이드를 돌리다가 새벽 3시에 눈이 번쩍 떠졌다. 알림 소리 때문이 아니라, 오히려 아무 소리도 안 나서. 자동화된 롤링 업그레이드 스크립트가 40분째 같은 노드에서 멈춰 있었다.
drain 로그를 열어보니 이 문구가 계속 반복되고 있었다.
error when evicting pods/"payment-worker-7c9d5f-abx2q" -n payments:
Cannot evict pod as it would violate the pod's disruption budget.
문제는 이 파드가 이미 CrashLoopBackOff 상태였다는 거다. Ready도 아니고, 실제로 트래픽도 받고 있지 않았다. 그런데도 PDB는 "얘 살아있어야 함"이라고 우기고 있었다. 왜.
PDB는 왜 죽은 파드를 살리려고 했나
일단 우리 팀 PDB 스펙은 아주 평범했다.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: payment-worker-pdb
namespace: payments
spec:
minAvailable: 2
selector:
matchLabels:
app: payment-worker
replicas는 3, minAvailable은 2. 정상 상태라면 파드 하나쯤은 언제든지 evict할 수 있어야 한다.
근데 그날 상황이 좀 꼬였다. 노드 3대에 파드가 각각 1개씩 배치돼 있었는데, 그중 하나가 알 수 없는 이유로 계속 크래시 중이었다. 즉 실제 Ready 파드는 2개. minAvailable이 2니까 딱 커트라인이었다. 그 상태에서 크래시 중인 파드가 얹혀있는 노드를 drain하려고 하니, PDB 컨트롤러가 "지금 파드 하나 더 죽이면 Ready가 1개가 되잖아. 안 돼"라고 막고 있었던 거다.
여기서 재미있는 건, drain하려는 대상이 바로 그 크래시 중인 파드였다는 점이다. 이미 죽어 있는 걸 죽이려는데도 PDB가 카운트를 지켜야 한다며 거부한다. 솔직히 처음엔 좀 어이없었다. 이미 트래픽 못 받는 애를 왜 못 죽인다는 거지?
unhealthyPodEvictionPolicy를 몰랐다
이 문제는 사실 K8s 1.27에서 stable로 넘어온 spec.unhealthyPodEvictionPolicy 필드로 해결된다. 기본값이 IfHealthyBudget인데, 이게 "budget이 여유 있어야만 unhealthy 파드도 evict 허용"이라는 뜻이다. 우리 상황처럼 budget이 이미 커트라인이면, unhealthy 파드조차 못 죽인다.
값을 AlwaysAllow로 바꾸면 얘기가 달라진다. Ready 안 된 파드는 언제든지 evict 가능. Ready 상태인 파드에 대해서만 budget을 지킨다.
spec:
minAvailable: 2
selector:
matchLabels:
app: payment-worker
unhealthyPodEvictionPolicy: AlwaysAllow
이 한 줄만 있었으면 새벽에 안 깼을 거다. 근데 나는 이 필드를 그날 처음 알았다. 팀 PDB 템플릿에 아무도 이걸 넣어놓지 않았기 때문에.
임시 조치와 사후 정리
일단 그날은 급했다. 옵션은 셋이었다.
- PDB를 임시로 지운다 → 다른 파드도 무방비 상태 됨. 무섭다.
kubectl drain --disable-eviction(혹은--force)로 우회 → 근데 이건 결국 API server를 통하지 않고 파드를 강제로 죽이는 거라 매너가 아니다.- PDB를 잠깐
minAvailable: 1로 낮춘다 → 그날 상황에선 이게 제일 낫다고 판단.
세 번째로 갔다. drain 끝나고 다시 원복. 이때 팀 슬랙에 "야 잠깐 payment PDB 손댔다, 15분만 참아라" 남기고. 이런 임시 조치는 항상 흔적을 남겨야 다음 사람이 헤매지 않는다.
그 다음날 팀 내부 논의 끝에 다음 세 가지를 정리했다.
첫째, 사내 공통 PDB 템플릿에 unhealthyPodEvictionPolicy: AlwaysAllow를 기본으로 넣기로 했다. 이건 웬만한 스테이트리스 워커에는 다 안전하다. StatefulSet의 경우엔 케이스 바이 케이스로 다시 보기로 했다. 예를 들어 replica 3인 데이터베이스 노드가 하나 이미 죽어있는데 다른 하나까지 강제로 내리면 쿼럼이 깨진다. 이런 건 오히려 막아주는 게 맞다.
둘째, drain 자동화 스크립트에 pre-check 스텝을 추가했다. drain 대상 파드가 속한 PDB의 현재 DisruptionsAllowed 값을 먼저 조회해서, 0이면 왜 0인지 로그를 뽑고, unhealthy 파드가 문제라면 자동으로 알림을 띄우는 흐름이다. 이거 없으면 또 40분 대기하고 새벽에 깨는 일이 반복된다.
kubectl get pdb payment-worker-pdb -n payments \
-o jsonpath='{.status.disruptionsAllowed}'
셋째, PDB 자체를 다시 봤다. 사실 minAvailable을 replicas와 같은 수 (예: replicas 3, minAvailable 3)로 잡아놓은 팀도 있었다. 이건 사실상 drain을 영원히 못 하는 거다. 이런 잘못된 PDB 설정을 찾아내려고 Kyverno 정책 하나 추가해서, minAvailable이 replicas와 같거나 percentage가 100%인 PDB는 CI에서 걸러내기로 했다. 이건 이전에 Kyverno 관련 글에서 다뤘던 검증 정책 확장이라 크게 어렵지 않았다.
마무리
이번 삽질의 진짜 교훈은, PDB는 "정상 상태를 지켜주는 방어벽"이 아니라는 거다. 정상 상태와 비정상 상태를 구분하지 못하고, budget이라는 숫자만 지키려고 한다. 그래서 unhealthyPodEvictionPolicy 같은 옵션이 나온 거고. 기본값이 왜 IfHealthyBudget인지는 안전한 쪽으로 기울인 결정이라고 이해는 하지만, 실무에서는 대부분 AlwaysAllow가 훨씬 편하다.
혹시 요즘 노드 드레인이 이유 없이 오래 걸리는 것 같으면, 한번 PDB 상태 확인해보자. 크래시 중인 파드 하나가 클러스터 전체 유지보수를 발목잡고 있을 수도 있다.
'IT > Kubernets' 카테고리의 다른 글
| HTTP/2 GOAWAY와 Kubernetes 롤링 업데이트 — 5xx가 튀는 진짜 원인 (1) | 2026.09.03 |
|---|---|
| kubectl wait --for=jsonpath, 이거 모르면 손해다 (0) | 2026.09.02 |
| Karpenter NodePool Disruption Budget 실전 적용 가이드 (0) | 2026.09.01 |
| In-place Pod Resize가 사실 내부적으로는 이렇게 돌아간다 (0) | 2026.08.31 |
| kubectl -o kyaml, 이거 모르는 분 꽤 많더라 (0) | 2026.08.30 |