K8s Drain·PDB 노드 관리

K8s 노드 관리가 중요한 이유

Kubernetes 클러스터를 운영하면 노드 OS 패치, 커널 업그레이드, 클러스터 버전 업그레이드, 하드웨어 교체 등의 이유로 노드를 안전하게 빼고 넣는 작업이 필수적입니다. 이때 cordon, drain, PodDisruptionBudget(PDB)을 올바르게 사용하지 않으면 서비스 중단이 발생합니다.

이 글에서는 노드 유지보수의 전체 워크플로, PDB 설계 전략, 롤링 업그레이드 자동화, 그리고 실전에서 발생하는 drain 실패 대응까지 심화 분석합니다.

cordon: 스케줄링 차단

kubectl cordon은 노드를 SchedulingDisabled 상태로 만들어 새로운 Pod가 해당 노드에 배치되지 않도록 합니다:

# 노드 스케줄링 비활성화
kubectl cordon worker-node-03

# 확인: STATUS에 SchedulingDisabled 표시
kubectl get nodes
# NAME              STATUS                     ROLES    AGE
# worker-node-01    Ready                      worker   90d
# worker-node-02    Ready                      worker   90d
# worker-node-03    Ready,SchedulingDisabled   worker   90d

# 스케줄링 다시 활성화
kubectl uncordon worker-node-03

핵심: cordon은 기존에 실행 중인 Pod에는 영향을 주지 않습니다. 새 Pod의 스케줄링만 차단합니다. 유지보수 시작 전 첫 번째 단계로 cordon을 실행하여 새 워크로드가 해당 노드로 배치되는 것을 방지합니다.

drain: 안전한 Pod 퇴거

kubectl drain은 노드의 모든 Pod를 안전하게 다른 노드로 이동시킵니다:

# 기본 drain (DaemonSet Pod 무시, 로컬 데이터 Pod 에러)
kubectl drain worker-node-03 --ignore-daemonsets

# 실전용 drain 명령 (모든 옵션 포함)
kubectl drain worker-node-03 
  --ignore-daemonsets               # DaemonSet Pod 무시
  --delete-emptydir-data            # emptyDir 볼륨 Pod도 삭제
  --force                           # ReplicaSet 없는 단독 Pod도 삭제
  --grace-period=60                 # 종료 유예 시간 60초
  --timeout=300s                    # drain 전체 타임아웃 5분
  --pod-selector='app!=critical-db'  # 특정 Pod 제외

drain의 내부 동작 순서:

  • 노드를 cordon 처리 (SchedulingDisabled)
  • 노드의 Pod 목록을 조회하고 PDB를 확인
  • 각 Pod에 Eviction API를 호출 (PDB를 존중)
  • Pod가 SIGTERM을 받고 graceful shutdown 진행
  • ReplicaSet/Deployment 컨트롤러가 다른 노드에 새 Pod 생성
  • 모든 Pod가 퇴거되면 drain 완료

PodDisruptionBudget: 가용성 보장

PDB는 자발적 중단(drain, 업그레이드) 시 최소 가용 Pod 수를 보장하는 핵심 리소스입니다:

# minAvailable 방식: 최소 N개 Pod는 항상 실행
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: web-app-pdb
  namespace: production
spec:
  minAvailable: 2                   # 최소 2개 Pod 유지
  selector:
    matchLabels:
      app: web-app
---
# maxUnavailable 방식: 최대 N개 Pod만 동시에 중단
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-gateway-pdb
  namespace: production
spec:
  maxUnavailable: 1                 # 동시에 1개만 중단 허용
  selector:
    matchLabels:
      app: api-gateway
---
# 퍼센트 기반
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: worker-pdb
  namespace: production
spec:
  maxUnavailable: "25%"             # 전체의 25%만 동시 중단 허용
  selector:
    matchLabels:
      app: worker

minAvailable vs maxUnavailable 선택 기준:

  • minAvailable — 정확한 최소 인스턴스가 필요할 때 (DB 클러스터: minAvailable: 2)
  • maxUnavailable — 스케일 아웃 시에도 유연하게 동작. 권장 옵션. replicas가 변해도 비율이 자동 적용됨
  • 주의: replicas=1인 Deployment에 minAvailable=1 또는 maxUnavailable=0을 설정하면 drain이 영원히 완료되지 않습니다

PDB 설계 실전 패턴

# 패턴 1: Stateless 웹 서비스 — 25% 동시 중단 허용
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: web-pdb
spec:
  maxUnavailable: "25%"
  selector:
    matchLabels:
      app: web-service
---
# 패턴 2: StatefulSet (DB 클러스터) — 1개만 동시 중단
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: postgres-pdb
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app: postgresql
      role: replica
---
# 패턴 3: 싱글톤 서비스 — drain 시 잠시 중단 허용
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: scheduler-pdb
spec:
  maxUnavailable: 1                 # 1개뿐이므로 중단 허용
  selector:
    matchLabels:
      app: task-scheduler
---
# 패턴 4: Kafka Consumer — 파티션 리밸런스 고려
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: kafka-consumer-pdb
spec:
  maxUnavailable: 1                 # 한 번에 1개씩만 재시작
  selector:
    matchLabels:
      app: kafka-consumer

drain 실패 대응

실전에서 drain이 실패하는 흔한 원인과 해결법입니다:

# 문제 1: PDB가 drain을 차단
# 확인
kubectl get pdb -A
# NAME          MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS   AGE
# web-pdb       2               N/A               0                     30d
# → ALLOWED DISRUPTIONS = 0이면 drain 불가

# 원인: replicas=2, minAvailable=2 → 여유가 없음
# 해결: replicas를 먼저 늘리거나 PDB 조정
kubectl scale deployment web-app --replicas=3
# 이제 ALLOWED DISRUPTIONS = 1

# 문제 2: Pod가 Terminating 상태에서 멈춤
kubectl get pods -o wide | grep Terminating
# → terminationGracePeriodSeconds 동안 종료 안 됨

# 강제 삭제 (최후의 수단)
kubectl delete pod stuck-pod-xxx --grace-period=0 --force

# 문제 3: 로컬 스토리지(emptyDir) Pod 거부
# 해결: --delete-emptydir-data 플래그 추가

# 문제 4: DaemonSet Pod 거부
# 해결: --ignore-daemonsets 플래그 (DaemonSet Pod는 모든 노드에 있으므로 무시)

# drain 상태 모니터링
kubectl get events --sort-by=.metadata.creationTimestamp | grep -i evict

클러스터 롤링 업그레이드 자동화

전체 클러스터 노드를 순차적으로 업그레이드하는 스크립트입니다:

#!/bin/bash
# rolling-upgrade.sh — 노드 순차 업그레이드

NODES=$(kubectl get nodes -l role=worker -o jsonpath='{.items[*].metadata.name}')
DRAIN_TIMEOUT=600
GRACE_PERIOD=60

for NODE in $NODES; do
  echo "=== Upgrading $NODE ==="

  # 1. 스케줄링 비활성화
  kubectl cordon "$NODE"

  # 2. 안전하게 Pod 퇴거
  echo "Draining $NODE..."
  kubectl drain "$NODE" 
    --ignore-daemonsets 
    --delete-emptydir-data 
    --grace-period=$GRACE_PERIOD 
    --timeout=${DRAIN_TIMEOUT}s

  if [ $? -ne 0 ]; then
    echo "ERROR: drain failed for $NODE"
    kubectl uncordon "$NODE"
    exit 1
  fi

  # 3. 노드 업그레이드 작업 (예: kubelet 업그레이드)
  echo "Upgrading kubelet on $NODE..."
  ssh "$NODE" "apt-get update && apt-get install -y kubelet=1.29.0-00"
  ssh "$NODE" "systemctl daemon-reload && systemctl restart kubelet"

  # 4. 노드 Ready 대기
  echo "Waiting for $NODE to be Ready..."
  kubectl wait --for=condition=Ready "node/$NODE" --timeout=120s

  # 5. 스케줄링 재활성화
  kubectl uncordon "$NODE"

  # 6. Pod 재배치 안정화 대기
  echo "Waiting for pod stabilization..."
  sleep 30

  # 7. 모든 Pod Running 확인
  NOT_READY=$(kubectl get pods -A --field-selector spec.nodeName="$NODE" 
    -o jsonpath='{.items[?(@.status.phase!="Running")].metadata.name}')

  if [ -n "$NOT_READY" ]; then
    echo "WARNING: Some pods not ready on $NODE: $NOT_READY"
  fi

  echo "=== $NODE upgrade complete ==="
  echo ""
done

echo "All nodes upgraded successfully!"

Graceful Shutdown과 preStop Hook

drain이 안전하게 동작하려면 애플리케이션의 graceful shutdown이 필수입니다:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
spec:
  replicas: 3
  template:
    spec:
      terminationGracePeriodSeconds: 60   # SIGTERM 후 최대 대기
      containers:
      - name: api
        image: api-server:v2
        lifecycle:
          preStop:
            exec:
              command:
              - /bin/sh
              - -c
              - |
                # 1. 헬스체크 실패시켜 LB에서 제거
                touch /tmp/shutdown
                # 2. 진행 중인 요청 완료 대기 (15초)
                sleep 15
        readinessProbe:
          exec:
            command:
            - /bin/sh
            - -c
            - "test ! -f /tmp/shutdown"
          periodSeconds: 5

drain 시 Pod 종료 타임라인:

  • 0s: Eviction API 호출 → Pod Terminating 상태
  • 0s: Endpoints에서 Pod IP 제거 시작 (비동기)
  • 0s: preStop hook 실행
  • 15s: preStop 완료 → SIGTERM 전송
  • 15~55s: 앱이 graceful shutdown 진행
  • 60s: terminationGracePeriodSeconds 초과 시 SIGKILL

관련 글: K8s StatefulSet 완벽 가이드에서 StatefulSet의 순서 보장 종료를, NestJS Health Check·Graceful Shutdown에서 앱 레벨 graceful shutdown 구현을 함께 확인하세요.

마무리

K8s 노드 관리는 클러스터 운영의 기본이자 핵심입니다. cordon으로 스케줄링 차단, drain으로 안전한 Pod 퇴거, PDB로 가용성 보장 — 이 세 가지를 올바르게 조합하면 무중단 노드 유지보수가 가능합니다. 모든 프로덕션 Deployment에는 적절한 PDB를 반드시 설정하고, 앱의 graceful shutdown과 preStop hook을 함께 구현하세요.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux