EKS 노드 Drain 핸즈온 — PDB가 없으면 운영 사고난다
노드를 교체할 때 kubectl drain만 실행하면 된다고 생각하기 쉽습니다. 직접 실습하면서 PDB가 없을 때와 있을 때 drain이 어떻게 다르게 동작하는지, 그리고 drain 후 새 노드가 떠도 Pod가 안 옮겨지는 이유까지 정리했습니다.
왜 노드 교체가 필요한가
실제 운영에서 노드를 교체해야 하는 상황은 생각보다 자주 옵니다.
- 커널 패치 / OS 보안 업데이트 적용
- 특정 노드의 디스크 / 메모리 이상 감지
- 인스턴스 타입 변경 (e.g. t3.medium → m5.large)
- EKS 노드 그룹 수동 교체
공통점은 노드 위에 떠 있는 Pod를 먼저 안전하게 다른 노드로 옮긴 다음 노드를 종료해야 한다는 것입니다.
핵심 개념: cordon vs drain
| 명령 | 효과 |
| kubectl cordon <node> | 새 Pod 스케줄링 차단. 기존 Pod는 그대로 유지 |
| kubectl drain <node> | 기존 Pod를 다른 노드로 이동(evict). cordon도 자동으로 포함 |
drain은 단순히 Pod를 삭제하는 게 아닙니다. K8s에 "이 Pod를 정중하게 종료해줘"(Eviction API) 요청을 보내고, terminationGracePeriodSeconds만큼 기다린 후 종료됩니다.
PodDisruptionBudget이 없으면 생기는 일
replicas=3인 Deployment가 있고 모든 Pod가 한 노드에 있다면:
PDB 없음 → drain 시 3개 Pod 동시 evict 요청
→ 순간 서비스 요청 처리 불가
PDB는 "drain 같은 의도적인 중단 시 최소 몇 개는 살아있어야 해" 를 Kubernetes에 알려주는 리소스입니다.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: filedrop-api-pdb
namespace: production
spec:
minAvailable: 2 # 항상 최소 2개 실행 보장
selector:
matchLabels:
app: filedrop-api
kubectl get pdb로 현재 상태를 확인할 수 있습니다:
NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS
filedrop-api-pdb 2 N/A 1
filedrop-worker-pdb N/A 1 1
ALLOWED DISRUPTIONS 가 핵심입니다.
- 1 → 지금 당장 1개 evict 가능
- 0 → drain 시도해도 즉시 중단됨
참고: Specifying a Disruption Budget for your Application — Kubernetes Docs
실습 환경
- EKS 1.32 클러스터 (ap-northeast-2)
- 노드 2개: ip-10-0-2-170, ip-10-0-3-67
- 테스트 워크로드: filedrop-api (3개), filedrop-worker (3개)
Step 1 — 테스트 워크로드 배포
kubectl create namespace production
kubectl create deployment filedrop-api \
--image=nginx --replicas=3 -n production
kubectl create deployment filedrop-worker \
--image=nginx --replicas=3 -n production
kubectl get pods -n production -o wide
NAME READY STATUS NODE
filedrop-api-7f66c4c85-4hgwl 1/1 Running ip-10-0-2-170...
filedrop-api-7f66c4c85-bd5zx 1/1 Running ip-10-0-3-67...
filedrop-api-7f66c4c85-k59fg 1/1 Running ip-10-0-2-170...
filedrop-worker-7b8965bb75-8b9xr 1/1 Running ip-10-0-3-67...
filedrop-worker-7b8965bb75-bscsw 1/1 Running ip-10-0-2-170...
filedrop-worker-7b8965bb75-sfxff 1/1 Running ip-10-0-2-170...
교체할 노드(ip-10-0-2-170)에 filedrop-api 2개, filedrop-worker 2개가 있습니다.
Step 2 — 교체 대상 노드 상태 확인
NODE="ip-10-0-2-170.ap-northeast-2.compute.internal"
kubectl describe node $NODE | grep -A8 "Conditions:"
Conditions:
MemoryPressure False KubeletHasSufficientMemory
DiskPressure False KubeletHasNoDiskPressure
PIDPressure False KubeletHasSufficientPID
Ready True KubeletReady
노드 자체는 정상 상태입니다. (실제 운영에서는 Ready: Unknown이나 이상 Condition을 확인 후 교체 진행)
# 이 노드 위의 Pod 목록
kubectl get pods --all-namespaces \
--field-selector "spec.nodeName=$NODE" -o wide
NAMESPACE NAME NODE
kube-system aws-node-k4lv7 ip-10-0-2-170... ← DaemonSet
kube-system kube-proxy-td6nq ip-10-0-2-170... ← DaemonSet
production filedrop-api-7f66c4c85-4hgwl ip-10-0-2-170...
production filedrop-api-7f66c4c85-k59fg ip-10-0-2-170...
production filedrop-worker-7b8965bb75-bscsw ip-10-0-2-170...
production filedrop-worker-7b8965bb75-sfxff ip-10-0-2-170...
DaemonSet Pod(aws-node, kube-proxy)는 drain 대상에서 제외됩니다.
Step 3 — PDB 적용 (drain 전 필수)
kubectl apply -f pdb.yaml
kubectl get pdb -n production
NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS
filedrop-api-pdb 2 N/A 1
filedrop-worker-pdb N/A 1 1
PDB 없는 Deployment 탐지 스크립트도 drain 전에 실행합니다:
for NS in $(kubectl get namespaces -o jsonpath='{.items[*].metadata.name}'); do
for DEPLOY in $(kubectl get deploy -n $NS -o jsonpath='{.items[*].metadata.name}' 2>/dev/null); do
PDB=$(kubectl get pdb -n $NS \
-o jsonpath="{.items[?(@.spec.selector.matchLabels.app=='$DEPLOY')].metadata.name}" 2>/dev/null)
[ -z "$PDB" ] && echo "WARNING: $NS/$DEPLOY 에 PDB 없음"
done
done
WARNING: kube-system/coredns 에 PDB 없음
coredns는 AWS EKS 관리 애드온으로, 이 실습에서는 무시합니다. 실제 운영에서는 coredns에도 PDB를 설정하는 것이 좋습니다.
Step 4 — cordon
kubectl cordon $NODE
kubectl get node $NODE
NAME STATUS ROLES AGE
ip-10-0-2-170.ap-northeast-2.compute.internal Ready,SchedulingDisabled <none> 38m
SchedulingDisabled가 붙었습니다. 기존 Pod는 그대로 실행 중입니다.
Step 5 — drain
kubectl drain $NODE \
--ignore-daemonsets \
--delete-emptydir-data \
--grace-period=60 \
--timeout=300s
실제 실행 결과:
node/ip-10-0-2-170... already cordoned
Warning: ignoring DaemonSet-managed Pods: kube-system/aws-node-k4lv7, kube-system/kube-proxy-td6nq
evicting pod production/filedrop-api-7f66c4c85-k59fg
evicting pod production/filedrop-worker-7b8965bb75-bscsw
evicting pod production/filedrop-api-7f66c4c85-4hgwl
evicting pod production/filedrop-worker-7b8965bb75-sfxff
error when evicting pods/"filedrop-api-7f66c4c85-k59fg" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
error when evicting pods/"filedrop-worker-7b8965bb75-bscsw" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
pod/filedrop-api-7f66c4c85-4hgwl evicted
pod/filedrop-worker-7b8965bb75-sfxff evicted
evicting pod production/filedrop-api-7f66c4c85-k59fg
evicting pod production/filedrop-worker-7b8965bb75-bscsw
pod/filedrop-api-7f66c4c85-k59fg evicted
pod/filedrop-worker-7b8965bb75-bscsw evicted
node/ip-10-0-2-170... drained
PDB가 실제로 동작하는 순간을 볼 수 있었습니다.
처음에 4개를 동시에 evict 요청하자, PDB 조건을 위반하는 k59fg와 bscsw는 즉시 차단됐습니다. 4hgwl과 sfxff가 먼저 evict되고 다른 노드에서 재생성된 후, 그 다음에 나머지 2개가 evict됐습니다.
PDB가 없었다면 4개가 동시에 종료됐을 것입니다.
Step 6 — EC2 인스턴스 종료
INSTANCE_ID=$(kubectl get node $NODE \
-o jsonpath='{.spec.providerID}' | cut -d'/' -f5)
echo "종료할 인스턴스: $INSTANCE_ID"
# 종료할 인스턴스: i-06e175fde8b010aa5
aws ec2 terminate-instances --instance-ids $INSTANCE_ID
Step 7 — ASG가 새 노드 자동 보충
kubectl wait --for=condition=Ready nodes --all --timeout=300s
node/ip-10-0-2-69.ap-northeast-2.compute.internal condition met
node/ip-10-0-3-67.ap-northeast-2.compute.internal condition met
kubectl get nodes -o wide
NAME STATUS ROLES AGE
ip-10-0-2-69.ap-northeast-2.compute.internal Ready <none> 32s ← 새 노드
ip-10-0-3-67.ap-northeast-2.compute.internal Ready <none> 44m
ASG(Auto Scaling Group)가 terminated 인스턴스를 감지하고 새 인스턴스를 자동으로 추가했습니다.
실습 중 발견한 현상: 새 노드에 Pod가 안 옮겨진다
새 노드(ip-10-0-2-69)가 Ready가 됐는데 Pod가 모두 ip-10-0-3-67에 그대로 있었습니다.
kubectl get pods -n production -o wide
NAME NODE
filedrop-api-7f66c4c85-5vfmm ip-10-0-3-67... ← 새 노드에 안 감
filedrop-api-7f66c4c85-bd5zx ip-10-0-3-67...
filedrop-api-7f66c4c85-fpqfc ip-10-0-3-67...
filedrop-worker-7b8965bb75-4jxdm ip-10-0-3-67...
filedrop-worker-7b8965bb75-8b9xr ip-10-0-3-67...
filedrop-worker-7b8965bb75-jpztn ip-10-0-3-67...
이유: Kubernetes 스케줄러는 실행 중인 Pod를 자동으로 재배치하지 않는다
스케줄러는 Pending 상태의 Pod에만 동작합니다. drain으로 evict된 Pod들이 이미 ip-10-0-3-67에서 Running이 됐으므로, 새 노드가 와도 건드리지 않습니다.
재배치 방법
즉각 재분산: rolling restart
kubectl rollout restart deployment filedrop-api -n production
kubectl rollout restart deployment filedrop-worker -n production
Pod를 순서대로 재생성하면서 스케줄러가 두 노드에 분산 배치합니다.
처음부터 균등하게: topologySpreadConstraints
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: filedrop-api
maxSkew: 1은 노드 간 Pod 수 차이가 최대 1을 넘지 않도록 강제합니다.
자동 재분산 (운영 환경): Descheduler
주기적으로 불균등하게 분포된 Pod를 evict해서 재배치시키는 add-on입니다.
helm install descheduler kubernetes-sigs/descheduler \
--set schedule="*/10 * * * *"
트러블슈팅 요약
| 증상 | 원인 | 해결 |
| drain이 멈추고 retry 반복 | PDB 조건 충족 불가 (ALLOWED DISRUPTIONS=0) | 노드 추가 후 재시도, 또는 replicas 증가 |
| drain 후 Pod가 다른 노드에 안 뜸 | 남은 노드 리소스 부족 | kubectl describe nodes로 Allocated 확인 |
| DaemonSet Pod drain 안 됨 | 정상 동작 (DaemonSet은 이동 불가) | --ignore-daemonsets 옵션 사용 |
| 새 노드에 Pod가 안 옮겨짐 | 스케줄러는 Running Pod를 재배치 안 함 | rollout restart 또는 Descheduler |
면접 포인트
Q. 노드 drain 전에 반드시 해야 하는 것은?
PDB(PodDisruptionBudget) 설정 여부 확인입니다. PDB가 없으면 drain 중 모든 Pod가 동시에 종료될 수 있습니다. drain 전에 PDB가 없는 Deployment를 탐지하는 스크립트를 실행해 누락된 PDB를 먼저 적용합니다.
Q. cordon과 drain의 차이는?
cordon은 새 Pod 스케줄링만 차단하고 기존 Pod는 그대로 둡니다. drain은 cordon 후 기존 Pod를 Eviction API로 정중하게 종료해 다른 노드로 이동시킵니다. 노드 교체 시에는 drain을 사용해야 합니다.
Q. drain 중 PDB가 어떻게 동작하는가?
drain이 Pod eviction을 요청하면 K8s가 PDB 조건을 확인합니다. ALLOWED DISRUPTIONS가 0이면 eviction을 거부하고, drain은 5초 후 재시도합니다. 다른 Pod가 재생성되어 ALLOWED DISRUPTIONS가 1 이상이 되면 다음 Pod를 evict합니다. 이 과정이 자동으로 반복되며, --timeout 내에 완료되지 않으면 drain이 실패합니다.
Q. 실제 운영 사고 사례가 있는가?
PDB가 설정되지 않은 Stateful 워크로드를 drain하다가 데이터 처리 중인 Pod가 강제 종료된 적이 있습니다. Multi-AZ 구성이라 전면 장애는 아니었지만 일부 요청이 실패했습니다. 이후 모든 Deployment에 PDB를 필수 적용하고, drain 전 PDB 설정 여부를 검사하는 스크립트를 CI 파이프라인에 추가했습니다.
'cloud' 카테고리의 다른 글
| ECS Task 생명주기 — 상태 전이, 교체, Graceful Shutdown (0) | 2026.04.05 |
|---|---|
| EKS IAM 핸즈온 — IRSA, Pod Identity, Access Entry (0) | 2026.04.02 |
| EKS 운영 트러블슈팅 핸즈온 — Pending, CrashLoop, NetworkPolicy (0) | 2026.04.01 |
| EKS 클러스터 무중단 버전 업그레이드 핸즈온 (1.32 → 1.33) (0) | 2026.03.31 |
| Terraform으로 EKS + ECS 운영 환경 한 번에 구축하기 (0) | 2026.03.31 |