본문으로 건너뛰기

11.4 노드 관리

커널 패치, 하드웨어 교체, 용량 조정. 노드를 비우고 빼고 다시 넣는 일은 클러스터 운영에서 가장 자주 반복되는 작업이다. Kubernetes는 이를 위해 강도가 다른 두 도구를 제공한다. 새 Pod의 배치만 막는 cordon과, 이미 실행 중인 Pod까지 내보내는 drain이다. 관리형 서비스의 노드 풀 교체 버튼 뒤에서 동작하는 것도 결국 이 두 동작의 조합이라, 자가 운영이 아니어도 동작을 알아 둘 가치가 있다.

cordon: 스케줄만 차단한다

kubectl cordon node-1

cordon은 Node 오브젝트의 spec.unschedulable을 true로 바꾼다. 스케줄러가 이 노드를 배치 후보에서 제외할 뿐, 이미 실행 중인 Pod에는 아무 일도 일어나지 않는다. kubectl get nodes에서 STATUS가 Ready,SchedulingDisabled로 표시된다.

용도는 두 가지다. 하나는 이상 징후가 보이는 노드를 관찰하는 동안 새 워크로드 유입만 막는 것이고, 다른 하나는 drain의 첫 단계로서다. 되돌리는 명령은 kubectl uncordon node-1이며, cordon 상태를 잊고 방치하면 클러스터의 가용 용량이 조용히 줄어 있게 되므로 작업 후 uncordon까지가 한 절차다.

drain: eviction API를 경유하는 비우기

kubectl drain node-1 --ignore-daemonsets --delete-emptydir-data

drain은 먼저 노드를 cordon하고, 그 위의 Pod 하나하나에 대해 eviction API를 호출한다. 단순 delete가 아니라 eviction이라는 점이 이 명령의 성격을 결정한다. eviction은 PodDisruptionBudget을 검사하는 삭제 요청이라서, 애플리케이션이 정의해 둔 최소 가용 수를 깨는 축출은 API 수준에서 거부된다. 종료 자체도 graceful termination을 따른다.

    flowchart TD
  D["kubectl drain"] --> C["cordon"]
  C --> T{"Pod 분류"}
  T -->|"DaemonSet"| S1["건너뜀<br/>플래그 필요"]
  T -->|"static Pod"| S2["건너뜀"]
  T -->|"일반 Pod"| E["eviction API 호출"]
  E --> B{"PDB 허용?"}
  B -->|"허용"| G["graceful 종료"]
  B -->|"거부"| R["429, 재시도"]
  R --> E
  

플래그 두 개는 각각 하나의 승인이다. --ignore-daemonsets가 필요한 이유는 DaemonSet controller가 unschedulable을 무시하기 때문이다. DaemonSet Pod를 evict해도 즉시 같은 노드에 재생성되므로 내보내는 의미가 없고, drain은 DaemonSet Pod를 발견하면 기본적으로 진행을 거부한다. 이 플래그는 “그 Pod들은 남겨 두고 진행하라"는 지시다. 로그 수집기나 CNI 에이전트처럼 노드가 살아 있는 동안 함께 있어야 하는 워크로드가 DaemonSet이므로, 남는 것이 대개 의도된 동작이기도 하다.

--delete-emptydir-data는 데이터 소실의 승인이다. emptyDir 볼륨은 노드 로컬 저장소라 Pod가 노드를 떠나는 순간 내용이 사라진다. drain은 emptyDir을 쓰는 Pod가 있으면 이 플래그 없이는 거부해서, 데이터 소실을 명시적으로 선택하게 만든다. 비슷하게 --force는 controller가 관리하지 않는 bare Pod의 승인이다. 재생성해 줄 controller가 없으므로 evict되면 그대로 소멸하는데, 그것까지 감수하겠다는 뜻이다. static Pod는 apiserver로 지울 수 없는 존재라 drain이 처음부터 건너뛴다.

PDB 때문에 drain이 멈출 때

drain이 같은 메시지를 반복하며 진행되지 않는 상황을 언젠가 만난다.

error when evicting pods/"web-0" -n "default" (will retry after 5s):
Cannot evict pod as it would violate the pod's disruption budget.

eviction API가 429를 반환하고 kubectl이 재시도하는 중이다. drain은 포기하지 않고 계속 재시도하므로, 원인을 해소하지 않으면 영원히 멈춰 있다. 진단은 PDB의 여유를 보는 것에서 시작한다.

kubectl get pdb -A

ALLOWED DISRUPTIONS가 0인 PDB가 원인이다. 0이 되는 경로는 크게 둘이다. 첫째는 설계 문제로, replicas 2에 minAvailable: 2 같은 구성은 어떤 자발적 축출도 허용하지 않겠다는 선언과 같다. 이 경우 replicas를 일시 증설하거나 PDB 값을 조정한 뒤 drain을 재개한다. 둘째는 상태 문제로, PDB는 healthy Pod 수를 기준으로 계산하므로 CrashLoopBackOff처럼 unhealthy한 Pod가 있으면 정상 구성에서도 여유가 0으로 떨어진다. 이때는 PDB 값을 조정할 일이 아니다. unhealthy Pod의 원인을 해결해야 여유가 생긴다.

--disable-eviction 플래그는 eviction API 대신 일반 delete를 사용해 PDB를 우회한다. 애플리케이션의 가용성 선언을 무시하는 동작이므로, 서비스 영향의 승인을 받은 뒤에만 쓰는 최후 수단으로 남겨 둔다. PDB와 eviction의 배경은 Part VIII. 스케줄링과 자원 관리에서 다뤘다.

노드 교체와 kubectl delete node

노드를 클러스터에서 완전히 빼는 절차는 drain에서 시작해 세 단계로 이어진다.

kubectl drain node-1 --ignore-daemonsets --delete-emptydir-data
kubectl delete node node-1
# 해당 머신에서, 재사용할 경우
sudo kubeadm reset

kubectl delete node가 하는 일은 apiserver에서 Node 오브젝트를 삭제하는 것까지다. 그 노드에 묶여 있던 Pod 오브젝트는 가비지 컬렉션되고, controller가 다른 노드에 대체 Pod를 만든다.

하지 않는 일이 더 길다. 머신의 전원이나 OS에는 아무 영향이 없고, 그 위의 kubelet과 컨테이너는 계속 실행된다. /etc/kubernetes/var/lib/kubelet의 로컬 상태, CNI 설정도 그대로 남는다. 로컬 정리는 kubeadm reset의 몫이며, reset조차 iptables 규칙이나 CNI가 만든 네트워크 설정 일부는 남길 수 있어 공식 문서도 필요 시 수동 정리를 안내한다. 순서도 중요하다. kubelet이 유효한 자격증명으로 계속 실행되고 있으면 Node 오브젝트를 지워도 스스로 다시 등록해 노드가 재생성된다. 머신을 폐기하거나 재설치할 것이 아니라면, delete 전에 kubelet을 중지하거나 reset을 먼저 실행한다. 정리가 끝난 머신은 11.1의 kubeadm join 절차로 새 노드처럼 재합류한다.