
지난주 화요일 새벽이었다. EKS 클러스터 1.32 → 1.33 업그레이드를 돌리는 중이었는데, 노드 드레인이 4시간째 안 끝나고 있다는 슬랙 알림을 받았다. 새벽 3시였고, 솔직히 처음엔 드레인이 원래 좀 오래 걸리니까 그러려니 했다. 4시간이라는 숫자를 본 순간 멘탈이 한 번 흔들렸다.
평소 같으면 워커 노드 한 대 드레인하는 데 길어야 10분 정도였다. 그런데 이번엔 한 노드에 박혀서 안 빠지는 파드가 있었고, 그 파드 하나가 전체 업그레이드 파이프라인을 막고 있었다. 결국 원인은 PDB(PodDisruptionBudget) 하나였다. 짧게 말하면 그렇고, 길게 말하면 우리 팀의 PDB 관리 방식 전체가 문제였다.
처음 발견한 증상
노드를 cordon하고 drain을 돌렸는데 이런 메시지가 계속 떴다.
evicting pod orders/order-worker-7d4f9b6c8-xqr2p
error when evicting pods/"order-worker-7d4f9b6c8-xqr2p" -n "orders"
(will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
kubectl get pdb -n orders를 쳤더니 이렇게 나왔다.
NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS
order-worker 3 N/A 0
ALLOWED DISRUPTIONS가 0. 이게 모든 것을 설명하고 있었다. minAvailable이 3인데 현재 정상 상태인 파드가 정확히 3개라서, 단 하나도 evict할 수 없는 상태였다.
왜 이런 상황이 됐을까
원래 order-worker 디플로이먼트는 replicas가 5였다. 그리고 PDB도 minAvailable 3으로 설정돼 있었다. 평소엔 5개 중 2개까지 disruption이 허용되니까 드레인이 멀쩡하게 돌아갔다.
그런데 며칠 전에 비용 절감 이슈로 누가 replicas를 5에서 3으로 줄여놨다. PR도 올라왔고 리뷰도 받았는데, 아무도 PDB를 같이 안 봤다. PDB는 그대로 minAvailable 3이었고. 즉, 디플로이먼트가 3개고 PDB도 3개를 항상 유지하라고 하니까 드레인하려는 순간 PDB가 막아버린 거다.
근데 더 황당한 건, 그 3개 파드 중 하나가 이미 CrashLoopBackOff 상태였다는 거다. 이게 무슨 말이냐면, 정상 파드는 사실상 2개뿐인데 PDB는 그걸 모르고 "건강한 파드 수가 3 미만이 되면 안 돼"라고 우기고 있었던 거다. evict하려는 파드는 멀쩡한 파드였고, 정작 죽어 있던 파드는 PDB 입장에서 "이미 죽어 있으니 disruption 카운트에 안 들어감"으로 처리되고 있었다.
이게 꼬이면서 ALLOWED DISRUPTIONS가 영원히 0에 머무는 데드락이 발생했다.
새벽 3시에 시도한 것들
처음엔 단순히 replicas를 늘려서 풀려고 했다.
kubectl scale deploy/order-worker -n orders --replicas=6
그런데 새 파드가 뜨려고 하니 노드가 cordon돼 있어서 스케줄링이 안 되고 있었다. 다른 노드들은 이미 꽉 차 있었고. 그래서 우선 클러스터에 노드를 한 대 더 띄웠다. Karpenter가 NodePool 설정대로 노드를 띄우는 데 또 3-4분 걸렸다. 그동안 멍하니 모니터 보고 있었다.
새 노드가 뜨고 새 파드가 Running으로 올라왔다. 이제 정상 파드가 3개가 됐고, 드레인이 다시 도는가 싶었는데… 여전히 막혔다. 왜냐하면 minAvailable 3 PDB 입장에선 정상 파드가 정확히 3개일 때도 evict가 안 되거든. ALLOWED DISRUPTIONS는 (정상 파드 수 - minAvailable)이라서 3-3=0이다.
결국 replicas를 5로 올렸다. 그제야 ALLOWED DISRUPTIONS가 2가 되면서 드레인이 풀렸다. 새벽 4시 좀 안 돼서 끝났다. 한 노드 드레인하려고 노드 하나 더 띄우고 replicas를 만지고… 좀 어이없는 작업이었다.
진짜 문제: PDB와 디플로이먼트의 결합 부재
다음 날 회고를 했다. 단순히 "replicas 줄일 때 PDB도 같이 보자"로 끝낼 문제가 아니었다. 이런 결합이 우리 클러스터 전체에 깔려 있을 거라는 게 무서웠다.
확인해 보니 비슷한 상태의 워크로드가 7개 더 있었다. minAvailable이 디플로이먼트 replicas와 같거나 너무 가까운 PDB들. 다 시한폭탄이었다.
우리 팀이 적용한 해결책
근본 해결은 두 갈래로 갔다.
첫째, PDB는 minAvailable 절대값 대신 maxUnavailable 비율로 통일. 운영 워크로드에 대해 일괄 마이그레이션 PR을 올렸다.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: order-worker
namespace: orders
spec:
maxUnavailable: 25%
selector:
matchLabels:
app: order-worker
unhealthyPodEvictionPolicy: AlwaysAllow
maxUnavailable: 25%는 replicas를 변경해도 자동으로 비율이 유지된다. 비율로 가면 replicas와 PDB의 결합이 자연스럽게 풀린다.
둘째, unhealthyPodEvictionPolicy: AlwaysAllow 추가. 1.27부터 stable로 들어온 옵션인데, 우린 1.32로 올라오면서도 이걸 안 써왔다. 이게 있으면 ready 안 된 파드(CrashLoopBackOff 같은)는 PDB 카운트에 포함되지 않고 무조건 evict가 허용된다. 우리가 새벽에 겪은 데드락 시나리오가 차단된다.
기본값(IfHealthyBudget)은 보수적이라서 unhealthy pod도 일단 PDB 보호 대상에 넣는다. 그게 이론적으론 맞는데, 운영하면서 보면 "이미 죽어 있는데 왜 PDB가 보호하지?"라는 상황이 너무 자주 생긴다. 우린 다 AlwaysAllow로 통일했다.
그리고 OPA Gatekeeper 정책 하나
이번에 세 번째로 추가한 게 있다. OPA Gatekeeper(우린 Gatekeeper를 쓰지만 Kyverno도 동일)에 PDB 검증 정책을 넣었다. minAvailable이 절대 숫자로 들어오는 PDB와 unhealthyPodEvictionPolicy가 안 들어간 PDB는 admission에서 거부된다.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: pdb-best-practices
spec:
validationFailureAction: Enforce
rules:
- name: require-percentage-and-eviction-policy
match:
any:
- resources:
kinds: ["PodDisruptionBudget"]
validate:
message: "PDB는 maxUnavailable 비율과 unhealthyPodEvictionPolicy: AlwaysAllow를 명시해야 합니다."
pattern:
spec:
unhealthyPodEvictionPolicy: AlwaysAllow
=(maxUnavailable): "?*%"
처음엔 audit 모드로 한 주 돌렸다. 기존에 깔려 있던 PDB 30개 중 18개가 위반이었다. 그걸 다 고친 뒤 enforce로 전환했다.
회고하면서 느낀 것
PDB는 안 보면 잊혀지는 리소스다. 디플로이먼트는 매일 만지는데 PDB는 한 번 깔면 1년을 그대로 둔다. 그러다 어느 날 새벽에 발목을 잡힌다.
우리 팀에서는 이번 일을 계기로 운영 알람에 ALLOWED DISRUPTIONS 메트릭을 넣었다. kube_poddisruptionbudget_status_current_healthy와 kube_poddisruptionbudget_status_desired_healthy 차이가 0이고 디플로이먼트는 ready인 상태가 5분 이상 지속되면 알람이 뜬다. 드레인하기 전에 알 수 있게.
아직 운영 두 주 정도 됐는데 한 번 떴다. 그땐 한가한 시간이었고 미리 손볼 수 있었다. 새벽보다는 훨씬 낫더라.
혹시 비슷한 일 겪으신 분 있으시면 댓글로 어떻게 푸셨는지 공유 좀 부탁드립니다. 우리 팀에서도 아직 이게 최선인지 확신은 없어서.
'IT > Kubernets' 카테고리의 다른 글
| Helm lookup 함수, ArgoCD랑 같이 쓰면 함정 있다 (0) | 2026.05.04 |
|---|---|
| etcd MVCC와 compaction, defrag가 K8s API에 미치는 진짜 영향 (0) | 2026.05.02 |
| kubectl debug --target, 이거 모르는 분 꽤 많더라 (0) | 2026.05.02 |
| ValidatingAdmissionPolicy vs Kyverno, 정책 일부를 옮기고 나서 (0) | 2026.05.01 |
| ingress-nginx EOL 이후, ingress2gateway로 Gateway API 옮기기 (0) | 2026.05.01 |