EKS 노드 Drain 핸즈온 — PDB가 없으면 운영 사고난다

2026. 4. 1. 20:59·cloud

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만큼 기다린 후 종료됩니다.

참고: Safely Drain a Node — Kubernetes Docs


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 * * * *"

참고: Descheduler for Kubernetes — GitHub


트러블슈팅 요약

증상 원인 해결
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
'cloud' 카테고리의 다른 글
  • EKS IAM 핸즈온 — IRSA, Pod Identity, Access Entry
  • EKS 운영 트러블슈팅 핸즈온 — Pending, CrashLoop, NetworkPolicy
  • EKS 클러스터 무중단 버전 업그레이드 핸즈온 (1.32 → 1.33)
  • Terraform으로 EKS + ECS 운영 환경 한 번에 구축하기
howokace
howokace
howokace 님의 블로그 입니다.
  • howokace
    howokace 님의 블로그
    howokace
  • 전체
    오늘
    어제
    • 분류 전체보기 (15)
      • cloud (7)
      • system (8)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
howokace
EKS 노드 Drain 핸즈온 — PDB가 없으면 운영 사고난다
상단으로

티스토리툴바