cordon/uncordon/drain으로 노드를 안전하게 유지보수한다.terminationGracePeriodSeconds와 무중단 배포, PDB의 관계를 이해한다.⚠️ 이 스텝은 노드 상태를 바꿉니다(cordon/drain). 모든 실습은 learn-worker2 하나에만, 짧게 하고 직후 반드시
uncordon합니다. PriorityClass는 전역 리소스라step21-접두사를 붙이고 검증 후 삭제합니다. 다른 학습자의 파드가 노드에 있을 수 있으니 drain은 오래 끌지 마세요.
| 종류 | 예 | 쿠버네티스가 존중하는가 |
|---|---|---|
| 자발적(voluntary) | 노드 drain(유지보수), 배포/롤아웃, kubectl delete pod, 클러스터 오토스케일 축소 | 예 — PDB로 제어 가능 |
| 비자발적(involuntary) | 노드 하드웨어 고장, 커널 패닉, OOM, 네트워크 단절 | 아니오 — 막을 수 없음 |
PDB는 자발적 중단만 제어합니다. 노드가 갑자기 죽는 것(비자발적)은 PDB로 못 막습니다. 그건 replica 수·여러 노드 분산으로 대비합니다.
이 스텝은 4개 replica를 두 워커에 퍼뜨린 web Deployment와 PDB로 실습합니다.
manifests/pdb.yaml (전문은 아래 실습 파일 섹션에 있습니다):
minAvailable: 3은 4개 중 한 번에 최대 1개만 자발적으로 내릴 수 있다는 뜻입니다. maxUnavailable: 1로 써도 같습니다(백분율 maxUnavailable: 25%도 가능).
ALLOWED DISRUPTIONS 1이 핵심 — 지금 이 순간 몇 개까지 evict를 허용하는지입니다. 이 값이 0이면 drain이 아예 진행되지 않습니다.
함정 — 셀렉터 불일치: PDB의
selector가 어떤 파드와도 안 맞으면ALLOWED DISRUPTIONS가 이상해지고 아무것도 보호하지 못합니다.kubectl describe pdb로Selector와 대상 파드 수를 확인하세요.
cordon은 노드에 새 파드 스케줄만 막고, 기존 파드는 건드리지 않습니다.
SchedulingDisabled 상태에서 스케일업하면 새 파드는 다른 노드로만 갑니다:
원복:
drain은 cordon + 기존 파드를 다른 노드로 evict합니다. 노드 유지보수(커널 업데이트 등) 전에 씁니다. 이때 PDB를 존중합니다.
실제 출력 (핵심 부분):
여기가 이 스텝의 핵심 장면입니다. worker2에는 web 파드가 2개 있었는데, PDB(minAvailable=3)가 한 번에 하나만 내리도록 강제했습니다:
Cannot evict pod as it would violate the pod's disruption budget → 거부하고 5초 뒤 재시도PDB가 없었다면 두 파드가 동시에 사라져 순간적으로 가용성이 2개로 떨어졌을 것입니다.
drain 후 web 파드는 전부 learn-worker로 옮겨가 모두 Running 상태가 됩니다. (--ignore-daemonsets는 kindnet/kube-proxy 같은 DaemonSet 파드를 건드리지 않겠다는 뜻 — 이들은 모든 노드에 하나씩 있어야 하므로 evict 대상이 아닙니다.)
노드가 꽉 찼을 때, 중요한 파드가 덜 중요한 파드를 밀어내고(preempt) 자리를 차지하게 할 수 있습니다.
manifests/priorityclass.yaml — 전역 리소스라 step21- 접두사:
실증(manifests/preemption-demo.yaml): 두 파드를 learn-worker2(8코어)에 고정하고 CPU를 크게 요청해 둘이 동시에 못 들어가게 합니다.
실제 결과:
이벤트가 전 과정을 보여줍니다:
important-high는 처음엔 FailedScheduling(자리 없음)이었다가, 스케줄러가 victim-low를 preempt해 자리를 만든 뒤 Scheduled 되었습니다.
주의: preemption은 자발적 중단이지만 PDB를 우회할 수 있습니다(단, 스케줄러는 PDB를 "best effort"로 존중하려 시도). 우선순위가 아주 높은 시스템 파드가 PDB보다 우선할 수 있음을 기억하세요.
system-cluster-critical같은 내장 PriorityClass는 매우 높은 값을 가집니다.
파드가 evict/삭제되면 쿠버네티스는:
Terminating으로 표시하고 Service 엔드포인트에서 제거(새 트래픽 차단)SIGTERM 전송terminationGracePeriodSeconds(기본 30초) 동안 대기 — 앱이 처리 중인 요청을 마무리(graceful shutdown)SIGKILLmanifests/web.yaml은 terminationGracePeriodSeconds: 10으로 설정했습니다. 앱이 이 신호를 받아 커넥션을 정리하면 요청 유실 없이 종료됩니다.
무중단 배포 = 세 가지의 합작:
maxUnavailable/maxSurge: 롤아웃 중 최소 가용 수 유지(Step 05)세 개가 서로 다른 상황(배포 / 노드 유지보수 / 스케줄링)을 커버합니다. 하나만으로는 부족합니다.
SchedulingDisabled로 남으면 그 노드는 영영 새 파드를 못 받습니다. 클러스터 용량이 조용히 줄어듭니다. drain 실습은 항상 uncordon으로 끝내세요.Cannot evict ... disruption budget으로 무한 재시도합니다. drain이 안 끝나면 PDB와 클러스터 여유 용량을 확인하세요.minAvailable을 replica와 같게: replicas: 3인데 minAvailable: 3이면 ALLOWED DISRUPTIONS 0 — drain이 절대 진행 안 됩니다. 최소 1개는 내릴 여유를 두세요.--dry-run으로 drain 미리보기: kubectl drain <node> --dry-run=client로 어떤 파드가 evict될지 먼저 확인.--ignore-daemonsets 필수: 안 주면 drain이 "DaemonSet 파드가 있다"며 멈춥니다. DaemonSet은 노드마다 하나씩 있어야 하므로 옮기지 않는 게 정상입니다.| 명령/필드 | 뜻 |
|---|---|
PodDisruptionBudget minAvailable/maxUnavailable | 자발적 중단 시 보장할 최소 가용/최대 불가용 |
kubectl get pdb → ALLOWED DISRUPTIONS | 지금 evict 허용 가능 수 |
kubectl cordon <node> | 새 스케줄만 차단 (evict 안 함) |
kubectl uncordon <node> | cordon 해제 |
kubectl drain <node> --ignore-daemonsets --delete-emptydir-data | cordon + PDB 존중하며 evict |
PriorityClass value | 스케줄링 우선순위(클수록 높음) |
| preemption | 높은 우선순위가 낮은 파드를 밀어냄 |
terminationGracePeriodSeconds | SIGTERM 후 SIGKILL까지 유예(기본 30s) |
challenge.md의 4개 과제.
→ Step 22 — CRD와 오퍼레이터: CustomResourceDefinition으로 쿠버네티스 API를 확장하고, 오퍼레이터 패턴으로 운영 지식을 코드로 자동화합니다.
이 스텝의 파일은 크게 두 갈래입니다. PDB 실증 갈래(namespace.yaml → web.yaml → pdb.yaml)로 4-replica 워크로드와 예산을 만든 뒤 cordon/drain을 걸어보고, preemption 갈래(priorityclass.yaml → preemption-demo.yaml)로 우선순위가 자리를 뺏는 장면을 재현합니다. commands.sh는 이 두 갈래를 처음부터 정리까지 한 번에 돌려주는 스크립트이며, 노드를 되돌리는 안전장치까지 포함하고 있으니 개별 매니페스트를 손으로 apply 하기 전에 먼저 읽어보길 권합니다.
이 스텝 전체(본문 1~5절)를 순서대로 실행하는 드라이버 스크립트입니다. 가장 중요한 줄은 trap 'echo "[trap] uncordon learn-worker2"; kubectl uncordon learn-worker2 >/dev/null 2>&1 || true' EXIT 입니다 — 스크립트가 중간에 죽거나 Ctrl-C로 끊겨도 EXIT 트랩이 반드시 uncordon을 실행해서, 본문 "함정 1 — drain 후 uncordon을 잊음"에 걸리지 않도록 막아줍니다.
set -uo pipefail에 -e가 일부러 빠져 있습니다. drain이 PDB에 막혀 0이 아닌 종료 코드를 낼 수 있는데, 그것도 학습 대상 출력이므로 스크립트를 중단시키지 않으려는 의도입니다.HERE="$(cd "$(dirname "$0")" && pwd)"로 스크립트 위치를 잡고 모든 apply를 -f "$HERE/manifests/..."로 참조하므로, 어느 디렉터리에서 실행해도 동작합니다.kubectl scale ... --replicas=6 후 sleep 6 → awk '{print $1,$7}'로 파드 이름과 노드만 뽑아 보여줍니다. 신규 2개가 모두 learn-worker에만 뜨는 것이 3절의 관찰 포인트입니다. 곧바로 uncordon + --replicas=4로 원복합니다.kubectl drain learn-worker2 --ignore-daemonsets --delete-emptydir-data --timeout=120s 바로 다음 줄이 kubectl uncordon learn-worker2 입니다. drain 성공/실패와 무관하게 즉시 노드를 되돌리려는 배치입니다.priorityclass.yaml → preemption-demo.yaml 순으로 apply 한 뒤 sleep 8로 스케줄러가 판단할 시간을 주고, kubectl get pods -l app=preempt-demo -o wide와 kubectl get events --sort-by='.lastTimestamp' | grep -i preempt로 결과를 확인합니다. grep 뒤의 || true는 매칭이 없어도(preemption 미발생) 스크립트를 죽이지 않기 위한 것입니다.priorityclass step21-low step21-high를 지우고 namespace step21을 삭제합니다. metrics-server는 다른 스텝이 쓰므로 삭제하지 않는다고 명시(echo)합니다.learn-worker2에 다른 파드가 올라가 있을 수 있으니 확인 후 실행하세요.실습 전용 네임스페이스 step21을 만듭니다. 가장 먼저 apply 해야 하며, 네임스페이스 리소스인 web.yaml·pdb.yaml·preemption-demo.yaml이 모두 namespace: step21을 하드코딩하고 있으므로 이 파일 없이 그것들을 적용하면 실패합니다. (priorityclass.yaml만은 클러스터 스코프라 namespace 필드가 없고, 이 파일과 무관하게 적용됩니다.)
labels의 course: k8s-learn, step: "21"은 교재 전체에서 실습 리소스를 식별하기 위한 표식입니다. "21"을 따옴표로 감싼 이유는 라벨 값이 반드시 문자열이어야 하기 때문입니다 — 따옴표를 빼면 YAML 파서가 정수 21로 읽고, API 서버가 문자열이 아닌 라벨 값을 거부합니다.kubectl delete namespace step21 한 줄이면 되지만, PriorityClass는 네임스페이스에 속하지 않으므로 따로 지워야 합니다(본문 5절 주의사항).PDB의 보호 대상이 될 web Deployment입니다. 본문 1절에서 apply 하며, 4절 drain 실습의 주인공입니다. replicas: 4와 PDB의 minAvailable: 3이 짝을 이뤄 "한 번에 1개만 evict 가능"이라는 상황을 만듭니다.
topologySpreadConstraints의 maxSkew: 1 + topologyKey: kubernetes.io/hostname이 4개 파드를 두 워커에 2:2로 퍼뜨립니다. 이 분산이 없으면 파드가 한 노드에 몰려서 drain 실습이 밋밋해집니다. whenUnsatisfiable: ScheduleAnyway라 강제가 아닌 선호이므로, 노드가 cordon 되어 균등 분산이 불가능해도 파드가 Pending에 빠지지 않습니다(3절에서 신규 파드가 learn-worker로만 가는 이유).terminationGracePeriodSeconds: 10은 기본 30초를 줄인 값으로, 6절의 graceful shutdown 설명과 직결됩니다. SIGTERM 후 최대 10초 기다렸다가 SIGKILL 합니다.requests: cpu: 10m, memory: 16Mi로 아주 작게 잡아서, 4개(또는 6개)를 띄워도 노드 자원이 남습니다. 덕분에 5절 preemption 실습에서 CPU를 크게 요청하는 파드가 자리 경쟁을 벌일 여지가 생깁니다.readinessProbe(httpGet 경로 /, 포트 80, periodSeconds: 5)가 PDB 계산의 기준입니다. PDB는 Ready 상태인 파드 수를 세므로, evict된 파드의 대체 파드가 Ready가 되어야 ALLOWED DISRUPTIONS가 다시 1로 회복됩니다. periodSeconds: 5는 이 probe를 5초마다 수행한다는 뜻이고, 4절 drain 출력의 will retry after 5s는 kubectl drain 자체의 재시도 간격입니다(둘은 우연히 값만 같을 뿐 별개입니다). 두 주기가 맞물려 "거부 → 대체 파드 Ready → 재시도 성공" 흐름이 만들어집니다.nginx:1.27-alpine입니다. nginx는 SIGTERM에 곧바로 종료하므로 grace period 10초를 다 쓰지 않고 빠르게 사라집니다(연습 과제 4의 관찰 포인트).이 스텝의 핵심 리소스입니다. web Deployment 직후에 apply 하고, kubectl get pdb -n step21로 ALLOWED DISRUPTIONS를 확인한 뒤 drain을 겁니다.
minAvailable: 3은 "자발적 중단이 일어나도 Ready 파드가 최소 3개는 남아야 한다"는 뜻입니다. replica가 4이므로 4 - 3 = 1, 즉 동시에 evict 가능한 파드는 1개입니다. 주석대로 maxUnavailable: 1로 써도 같은 의미이며, 둘을 동시에 쓸 수는 없습니다.selector.matchLabels.app: web이 보호 대상을 고릅니다. 이 라벨이 web.yaml의 파드 템플릿 라벨과 정확히 일치해야 하며, 어긋나면 본문 2절 "함정 — 셀렉터 불일치"처럼 아무것도 보호하지 못합니다.minAvailable: 3을 유지하면 ALLOWED DISRUPTIONS가 0이 되어 drain이 영원히 진행되지 않습니다 — 연습 과제 1이 바로 이 상황을 재현하는 문제입니다.본문 5절 preemption 실습의 준비물입니다. 한 파일에 ---로 두 개의 PriorityClass를 담고 있습니다.
step21-low는 value: 100, step21-high는 value: 1000000. 값이 클수록 우선순위가 높고, 노드가 꽉 찼을 때 높은 쪽이 낮은 쪽을 밀어냅니다.globalDefault: false가 양쪽 모두에 지정된 것이 중요합니다. true로 두면 PriorityClass를 지정하지 않은 클러스터 전체의 모든 신규 파드가 이 우선순위를 기본값으로 물려받게 되어, 실습 리소스가 클러스터 전역에 부작용을 일으킵니다.kubectl delete ns step21으로는 지워지지 않습니다. 그래서 이름에 step21- 접두사를 붙여 다른 학습자·시스템 PriorityClass와 충돌하지 않게 했고, commands.sh 마지막에서 kubectl delete priorityclass step21-low step21-high로 별도 삭제합니다.priorityclass.yaml 다음에 apply 합니다(참조하는 PriorityClass가 먼저 존재해야 합니다). 두 개의 Pod로 "자리 뺏기"를 인위적으로 연출한 실습 파일입니다.
nodeSelector: kubernetes.io/hostname: learn-worker2로 같은 노드에 못 박아 자원 경쟁을 강제합니다. 노드가 여러 개면 스케줄러가 그냥 다른 노드에 배치해버려 preemption이 일어나지 않습니다(연습 과제 3의 원인 1).victim-low는 priorityClassName: step21-low + cpu: "6"으로 8코어 노드의 대부분을 예약합니다. important-high는 priorityClassName: step21-high + cpu: "5"를 요청하는데, 6 + 5 = 11 > 8이라 둘이 동시에는 절대 들어갈 수 없습니다. 이 숫자 설계가 실습의 전부입니다.important-high를 넣기 위해 victim-low를 삭제합니다. victim-low는 Deployment가 아닌 베어 Pod이므로 재생성되지 않고 그대로 사라집니다(Error from server (NotFound)). 이것이 preemption의 파괴성을 보여주는 학습 포인트입니다.victim-low에만 terminationGracePeriodSeconds: 5가 붙어 있어, preempt 당했을 때 5초 안에 정리되고 자리가 빨리 비워집니다.commands.sh는 두 파드를 한 번에 apply 하지만(주석: "low 가 먼저면 좋지만 동시 apply 도 스케줄러가 처리"), 순서를 확실히 보고 싶다면 victim-low가 Running이 된 뒤 important-high를 넣으면 FailedScheduling → Preempted → Scheduled 이벤트 흐름이 더 또렷하게 보입니다.