M6. 스케줄링·리소스·node pool
1. 왜 필요한가
섹션 제목: “1. 왜 필요한가”NKS에서 web-app, api-service, ai-service를 운영할 때 AI가 “고사양 node
pool에 배치하자”거나 “CPU를 2개 보장하자”고 제안해도, Pod가 어떤 Node를 후보로 삼는지와
그 Node에 실제로 들어갈 수 있는지는 별도 판단입니다. label·selector, taint·toleration,
requests·limits의 역할을 분리해야 Pending 원인을 읽고 node pool 변경을 리뷰할 수 있습니다.
개념 정의의 정본은 Kubernetes Basics와
cgroups & Namespace입니다. 원본 경로는 각각
content/topics/L5/kubernetes-basics.mdx, content/topics/L4/cgroups-namespace.mdx이며, 이
모듈은 M3의 Pod template과 M5의 3서비스 설정 위에서 배치 후보 필터링과 resource fit을
실행으로 관찰하는 역할만 맡습니다.
2. 핵심 개념
섹션 제목: “2. 핵심 개념”flowchart LR Pod["새 Pod: 아직 nodeName 없음"] --> Filter["후보 Node 필터"] Labels["node label + nodeSelector/affinity"] --> Filter Taints["node taint + Pod toleration"] --> Filter Filter --> Fit["requests 합계가 allocatable 안에 드는가"] Fit --> Score["남은 후보를 점수화"] Score --> Bind["Pod.spec.nodeName에 bind"] Bind --> Runtime["kubelet: limits를 runtime/cgroup에 전달"]
| 장치 | 붙는 곳 | 스케줄링에서 하는 일 | 하지 않는 일 |
|---|---|---|---|
| Node label | Node | Node의 pool·zone·장비 특성을 기술 | Pod를 스스로 이동시키지 않음 |
nodeSelector | Pod | 모든 key/value가 맞는 Node만 후보로 남김 | 우선순위나 fallback을 표현하지 못함 |
| node affinity | Pod | required 또는 preferred 조건으로 후보·점수를 제어 | taint를 자동 허용하지 않음 |
| taint | Node | 일치하는 toleration이 없는 Pod를 밀어냄 | 특정 Pod를 그 Node로 끌어오지 않음 |
| toleration | Pod | 대응 taint가 있는 Node도 후보가 될 수 있게 허용 | 그 Node 배치를 보장하지 않음 |
requests | Container | scheduler가 Node fit을 계산할 때 더하는 예약 의도 | 현재 사용량이나 성능 보장을 직접 측정하지 않음 |
limits | Container | 실행 중 kubelet/runtime이 적용할 사용 상한 | scheduler의 기본 fit 기준이 아님 |
Node allocatable | Node | 시스템 몫을 제외하고 Pod에 배분 가능한 양을 표시 | 모든 Pod가 그 양을 동시에 실제 사용한다는 뜻이 아님 |
requests는 실제 사용량이 아니라 배치 약속입니다. Node CPU가 지금 5%만 사용 중이어도 이미
배치된 Pod들의 CPU request 합계와 새 Pod의 request가 allocatable을 넘으면 새 Pod는 Pending에
머뭅니다. 반대로 실행 중 container는 여유가 있으면 request보다 더 쓸 수 있습니다.
limits는 실행 단계의 경계입니다. Linux Node에서 CPU limit은 일반적으로 throttling으로,
memory limit은 reclaim으로 해결되지 않을 때 OOM kill 가능성으로 이어집니다. 이 모듈은 값을
API와 QoS class에서 확인하되 의도적으로 부하나 OOM을 만들지 않습니다. runtime 장애 판독은
M11에서 다룹니다. 다만 admission 정책이 별도 기본값을 넣지 않는 환경에서 특정 resource의
limit만 적고 request를 생략하면 Kubernetes가 그 limit 값을 request로 복사할 수 있으므로,
“request를 안 적었으니 scheduler 회계가 0”이라고 판단하면 안 됩니다.
flowchart LR Pool["NKS node pool: 같은 사양의 Node 묶음"] --> PoolLabel["pool 식별 Node label"] Pool --> PoolTaint["선택적 taint"] Spec["Deployment Pod template"] --> Selector["nodeSelector / affinity"] Spec --> Toleration["필요한 toleration"] Spec --> Resources["측정 기반 requests / limits"] PoolLabel --> Scheduler["kube-scheduler"] PoolTaint --> Scheduler Selector --> Scheduler Toleration --> Scheduler Resources --> Scheduler Scheduler --> Placement["적합한 Node에 Pod 배치"]
로컬 k3d의 두 Node에 붙이는 workbook.cs-study/node-pool label은 학습용 fixture입니다.
NKS 공식 문서가 설명하는 실제 node pool 식별 label은 ncloud.com/nks-nodepool이지만, 실제
pool 이름·사양·zone·taint는 승인된 non-production NKS에서 확인해야 합니다. k3d 결과를 NKS
실측으로 해석하지 않습니다. 특히 이 로컬 K3s server Node는 학습용 2노드 대비를 위해 Pod를
받는 general fixture로만 사용합니다. NKS의 managed control plane과 대응하지 않으며, NKS의
general/compute node pool은 모두 사용자 workload가 실행되는 worker Node 묶음입니다.
시점 의존 설명은 2026-07-15에 다음 공식 1차 자료로 확인했습니다.
- Assigning Pods to Nodes:
nodeSelector, required/preferred node affinity와 label 기반 후보 제어 - Taints and Tolerations: taint의 반발 효과와 toleration이 배치를 보장하지 않는 경계
- Resource Management for Pods and Containers: request 기반 scheduler fit, limit enforcement, allocatable과 Pending 관계
- NKS cluster node pool 관리:
같은 사양의 Node 묶음, pool label·taint 편집과
ncloud.com/nks-nodepool식별 label
NKS node pool 생성·사양 변경·taint 편집과 Cluster Autoscaler 활성화는 cloud 비용과 실제
workload 이동을 일으키므로 이 모듈에서는 official-doc-only입니다. 실제 API 호출, console
변경과 production Node 조작은 excluded이며 예상 출력을 제시하지 않습니다. M8에서 Pod
autoscaling과 Node autoscaling의 계층을 분리해 이어갑니다.
3. 직관 비유
섹션 제목: “3. 직관 비유”| 개념 | 비유 | 비유의 경계 |
|---|---|---|
label + nodeSelector | 설비 표식과 그 설비를 반드시 요구하는 작업 지시서 | 표식은 용량이나 출입 허가까지 보장하지 않습니다. |
| taint + toleration | 제한 구역 표지와 출입 허가증 | 허가증이 있어도 그 구역으로 반드시 배정되지는 않습니다. |
| request | 배치 전에 확보한다고 신고한 좌석 수 | 실시간 사용량과 같지 않고 성능 SLO도 자동 보장하지 않습니다. |
| limit | 실행 중 넘지 못하게 둔 사용 상한 | 상한을 지나치게 낮추면 throttling이나 OOM 위험이 생깁니다. |
| node pool | 같은 설비 사양으로 관리되는 작업 구역 | Pod 배치 규칙과 적정 용량 산정은 별도로 필요합니다. |
4. 핸즈온 랩
섹션 제목: “4. 핸즈온 랩”검증 환경
섹션 제목: “검증 환경”| 항목 | 검증값 |
|---|---|
| 기준일 | 2026-07-15 |
| host | macOS arm64 |
| Docker client / engine | 29.6.1 / 29.6.1 |
| k3d | 5.9.0 |
| K3s image / API server | rancher/k3s:v1.36.2-k3s1 / v1.36.2+k3s1 |
| kubectl | 1.36.1 |
| Helm | 4.2.3 — 이 모듈에서는 사용하지 않음 |
| container image | busybox:1.37.0 + Setup의 고정 multi-platform digest |
| evidence 환경 | local-cluster; 고정 context + k8s-wb-m6-<UTC run ID> |
모든 shell 명령 묶음은 executed-local입니다. NKS node pool과 실제 production resource sizing은
각각 official-doc-only, excluded이며 로컬 관측값을 운영 권장값으로 사용하지 않습니다.
사전 조건
섹션 제목: “사전 조건”- 저장소 root에서 실행하며 M0~M5를 완료한 상태여야 합니다.
- Docker Desktop과 M2의
cs-study-workbookcluster가 실행 중이어야 합니다. - cluster는 M2에서 만든 server 1개와 agent 1개, 총 Ready Node 2개여야 합니다.
- 모든 workload 명령은
--context k3d-cs-study-workbook과 고유 namespace를 명시합니다. - Setup은 전용 prefix의 학습 label·taint가 기존에 없는지 확인하고, cleanup에서 모두 제거합니다.
- 예상 소요 시간은 110분입니다.
Setup
섹션 제목: “Setup”두 Node에 학습용 pool label과 taint 만들기
섹션 제목: “두 Node에 학습용 pool label과 taint 만들기”성공 전 중간 오류가 나면 EXIT trap이 namespace와 Node 변경을 되돌립니다. 기존 label이나
taint를 덮어쓰지 않고 전용 key가 비어 있을 때만 시작합니다. 아래 GENERAL_NODE가 가리키는
K3s server는 로컬에서만 schedulable한 두 번째 fixture이며 NKS managed control plane을
모사하지 않습니다.
set -euo pipefailrm -rf /tmp/k8s-wb-m6mkdir -p /tmp/k8s-wb-m6chmod 700 /tmp/k8s-wb-m6node automation/k8s-workbook/preflight.mjs --require-clustertest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NODE_COUNT="$(kubectl --context k3d-cs-study-workbook get nodes --no-headers | wc -l | tr -d ' ')"test "$NODE_COUNT" = "2"GENERAL_NODE="$(kubectl --context k3d-cs-study-workbook get nodes \ -l node-role.kubernetes.io/control-plane -o jsonpath='{.items[0].metadata.name}')"test -n "$GENERAL_NODE"COMPUTE_NODE="$(kubectl --context k3d-cs-study-workbook get nodes -o name \ | sed 's#node/##' | awk -v general="$GENERAL_NODE" '$0 != general {print}')"test -n "$COMPUTE_NODE"test "$(printf '%s\n' "$COMPUTE_NODE" | wc -l | tr -d ' ')" = "1"for NODE in "$GENERAL_NODE" "$COMPUTE_NODE"; do EXISTING_LABEL="$(kubectl --context k3d-cs-study-workbook get node "$NODE" \ -o jsonpath='{.metadata.labels.workbook\.cs-study/node-pool}')" test -z "$EXISTING_LABEL"doneEXISTING_TAINTS="$(kubectl --context k3d-cs-study-workbook get node "$COMPUTE_NODE" \ -o jsonpath='{range .spec.taints[*]}{.key}{"\n"}{end}' \ | awk '$0 == "workbook.cs-study/dedicated" {count++} END {print count+0}')"test "$EXISTING_TAINTS" = "0"NS="k8s-wb-m6-$(date -u +%Y%m%d%H%M%S)"printf '%s' "$NS" > /tmp/k8s-wb-m6/namespaceprintf '%s' "$GENERAL_NODE" > /tmp/k8s-wb-m6/general-nodeprintf '%s' "$COMPUTE_NODE" > /tmp/k8s-wb-m6/compute-nodeif kubectl --context k3d-cs-study-workbook get namespace "$NS" >/dev/null 2>&1; then echo "namespace collision: $NS" >&2 exit 1firollback() { STATUS=$? trap - EXIT set +e ROLLBACK_FAILED=0 kubectl --context k3d-cs-study-workbook delete namespace "$NS" \ --ignore-not-found --wait=true >/dev/null 2>&1 test $? = 0 || ROLLBACK_FAILED=1 for NODE in "$GENERAL_NODE" "$COMPUTE_NODE"; do CURRENT_LABEL="$(kubectl --context k3d-cs-study-workbook get node "$NODE" \ -o jsonpath='{.metadata.labels.workbook\.cs-study/node-pool}' 2>/dev/null)" GET_EXIT=$? if test "$GET_EXIT" != 0; then ROLLBACK_FAILED=1 elif test -n "$CURRENT_LABEL"; then kubectl --context k3d-cs-study-workbook label node "$NODE" \ workbook.cs-study/node-pool- >/dev/null 2>&1 test $? = 0 || ROLLBACK_FAILED=1 fi done CURRENT_TAINTS="$(kubectl --context k3d-cs-study-workbook get node "$COMPUTE_NODE" \ -o jsonpath='{range .spec.taints[*]}{.key}{"\n"}{end}' 2>/dev/null \ | awk '$0 == "workbook.cs-study/dedicated" {count++} END {print count+0}')" GET_EXIT=$? if test "$GET_EXIT" != 0; then ROLLBACK_FAILED=1 elif test "$CURRENT_TAINTS" != 0; then kubectl --context k3d-cs-study-workbook taint node "$COMPUTE_NODE" \ workbook.cs-study/dedicated:NoSchedule- >/dev/null 2>&1 test $? = 0 || ROLLBACK_FAILED=1 fi REMAINING_NAMESPACE="$(kubectl --context k3d-cs-study-workbook get namespace "$NS" \ --ignore-not-found -o name 2>/dev/null)" test $? = 0 || ROLLBACK_FAILED=1 test -z "$REMAINING_NAMESPACE" || ROLLBACK_FAILED=1 REMAINING_LABELS="$(kubectl --context k3d-cs-study-workbook get nodes \ -l workbook.cs-study/node-pool -o name 2>/dev/null | awk 'END {print NR+0}')" test $? = 0 || ROLLBACK_FAILED=1 test "$REMAINING_LABELS" = 0 || ROLLBACK_FAILED=1 REMAINING_TAINTS="$(kubectl --context k3d-cs-study-workbook get nodes \ -o jsonpath='{range .items[*].spec.taints[*]}{.key}{"\n"}{end}' 2>/dev/null \ | awk '$0 == "workbook.cs-study/dedicated" {count++} END {print count+0}')" test $? = 0 || ROLLBACK_FAILED=1 test "$REMAINING_TAINTS" = 0 || ROLLBACK_FAILED=1 if test "$ROLLBACK_FAILED" != 0; then echo 'rollback incomplete: preserve /tmp/k8s-wb-m6 and inspect cluster-scoped state' >&2 exit 70 fi rm -rf /tmp/k8s-wb-m6 exit "$STATUS"}trap rollback EXITkubectl --context k3d-cs-study-workbook create namespace "$NS"kubectl --context k3d-cs-study-workbook label node "$GENERAL_NODE" \ workbook.cs-study/node-pool=general >/dev/nullkubectl --context k3d-cs-study-workbook label node "$COMPUTE_NODE" \ workbook.cs-study/node-pool=compute >/dev/nullkubectl --context k3d-cs-study-workbook taint node "$COMPUTE_NODE" \ workbook.cs-study/dedicated=ai:NoSchedule >/dev/nulltrap - EXITprintf 'setup namespace=%s nodes=2 labels=2 compute_taints=1\n' "$NS"예상 출력:
PASS platform: darwin/arm64PASS architecture: arm64PASS docker-cli: v29.6.1PASS docker-engine: v29.6.1PASS k3d: v5.9.0PASS kubectl-skew: v1.36.1PASS helm: v4.2.3PASS context: k3d-cs-study-workbookPASS kubernetes-server: v1.36.2+k3s1namespace/k8s-wb-m6-<UTC 14자리 run ID> createdsetup namespace=k8s-wb-m6-<UTC 14자리 run ID> nodes=2 labels=2 compute_taints=1host architecture, Docker patch version, namespace run ID와 두 Node 이름은 변동 필드입니다.
Lab 1 — label·selector와 request/limit를 함께 읽기
섹션 제목: “Lab 1 — label·selector와 request/limit를 함께 읽기”web-app fixture를 general label이 있는 Node에만 배치합니다. request와 limit가 서로
다르므로 이 Pod의 QoS class는 Burstable입니다.
set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m6/namespace)"GENERAL_NODE="$(cat /tmp/k8s-wb-m6/general-node)"kubectl --context k3d-cs-study-workbook -n "$NS" apply -f - <<'YAML'apiVersion: apps/v1kind: Deploymentmetadata: name: web-appspec: replicas: 1 selector: matchLabels: app: web-app template: metadata: labels: app: web-app spec: nodeSelector: workbook.cs-study/node-pool: general terminationGracePeriodSeconds: 3 containers: - name: app image: busybox:1.37.0@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028 imagePullPolicy: IfNotPresent command: ["sh", "-c", "sleep 3600"] resources: requests: cpu: 25m memory: 32Mi limits: cpu: 100m memory: 64MiYAMLkubectl --context k3d-cs-study-workbook -n "$NS" \ rollout status deployment/web-app --timeout=90s >/dev/nullPOD="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod -l app=web-app \ -o jsonpath='{.items[0].metadata.name}')"NODE="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod "$POD" \ -o jsonpath='{.spec.nodeName}')"QOS="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod "$POD" \ -o jsonpath='{.status.qosClass}')"REQUESTS="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod "$POD" \ -o jsonpath='{.spec.containers[0].resources.requests.cpu}/{.spec.containers[0].resources.requests.memory}')"LIMITS="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod "$POD" \ -o jsonpath='{.spec.containers[0].resources.limits.cpu}/{.spec.containers[0].resources.limits.memory}')"test "$NODE" = "$GENERAL_NODE"test "$QOS" = "Burstable"test "$REQUESTS" = "25m/32Mi"test "$LIMITS" = "100m/64Mi"printf 'placement app=web-app pool=general qos=%s requests=%s limits=%s\n' \ "$QOS" "$REQUESTS" "$LIMITS"예상 출력:
deployment.apps/web-app createdplacement app=web-app pool=general qos=Burstable requests=25m/32Mi limits=100m/64Mi관찰 포인트는 workload label app=web-app가 Deployment와 Pod를 연결하고, Node label을
읽는 nodeSelector가 별도의 배치 후보를 제한한다는 점입니다.
Lab 2 — toleration이 없어서 Pending이 되는 실패 재현
섹션 제목: “Lab 2 — toleration이 없어서 Pending이 되는 실패 재현”compute Node를 정확히 선택해도 그 Node의 NoSchedule taint를 허용하지 않으면 bind되지
않습니다. 이 묶음은 의도적으로 exit 1을 반환합니다.
set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m6/namespace)"kubectl --context k3d-cs-study-workbook -n "$NS" apply -f - <<'YAML'apiVersion: v1kind: Podmetadata: name: ai-service labels: app: ai-servicespec: nodeSelector: workbook.cs-study/node-pool: compute terminationGracePeriodSeconds: 3 containers: - name: worker image: busybox:1.37.0@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028 imagePullPolicy: IfNotPresent command: ["sh", "-c", "sleep 3600"] resources: requests: cpu: 100m memory: 64Mi limits: cpu: 500m memory: 128MiYAMLREASON=""MESSAGE=""for _ in $(seq 1 30); do REASON="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod ai-service \ -o jsonpath='{.status.conditions[?(@.type=="PodScheduled")].reason}')" MESSAGE="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod ai-service \ -o jsonpath='{.status.conditions[?(@.type=="PodScheduled")].message}')" test "$REASON" = "Unschedulable" && break sleep 1donetest "$REASON" = "Unschedulable"case "$MESSAGE" in *"untolerated taint"*) ;; *) echo "expected untolerated taint evidence" >&2; exit 2 ;;esacset +ekubectl --context k3d-cs-study-workbook -n "$NS" \ wait --for=condition=Ready pod/ai-service --timeout=5s \ > /tmp/k8s-wb-m6/taint-ready.log 2>&1READY_EXIT=$?set -etest "$READY_EXIT" = "1"grep -q 'timed out waiting for the condition' /tmp/k8s-wb-m6/taint-ready.logprintf 'pod=ai-service phase=Pending reason=%s cause=untolerated-taint ready_exit=%s\n' \ "$REASON" "$READY_EXIT"exit 1예상 출력:
pod/ai-service createdpod=ai-service phase=Pending reason=Unschedulable cause=untolerated-taint ready_exit=1Unschedulable 수렴 시간과 원본 event message는 변동 필드입니다. 실패는 image pull이나
application crash보다 앞선 scheduler 단계에서 발생합니다.
Lab 3 — selector와 toleration을 함께 사용해 복구
섹션 제목: “Lab 3 — selector와 toleration을 함께 사용해 복구”Pod의 주요 배치 필드는 실행 중 고쳐 쓰는 대상이 아니므로 실패한 Pod를 삭제하고 올바른
template으로 다시 만듭니다. toleration만 추가하지 않고 compute selector도 유지합니다.
set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m6/namespace)"COMPUTE_NODE="$(cat /tmp/k8s-wb-m6/compute-node)"kubectl --context k3d-cs-study-workbook -n "$NS" delete pod ai-service --wait=true >/dev/nullkubectl --context k3d-cs-study-workbook -n "$NS" apply -f - <<'YAML'apiVersion: v1kind: Podmetadata: name: ai-service labels: app: ai-servicespec: nodeSelector: workbook.cs-study/node-pool: compute tolerations: - key: workbook.cs-study/dedicated operator: Equal value: ai effect: NoSchedule terminationGracePeriodSeconds: 3 containers: - name: worker image: busybox:1.37.0@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028 imagePullPolicy: IfNotPresent command: ["sh", "-c", "sleep 3600"] resources: requests: cpu: 100m memory: 64Mi limits: cpu: 500m memory: 128MiYAMLkubectl --context k3d-cs-study-workbook -n "$NS" \ wait --for=condition=Ready pod/ai-service --timeout=90s >/dev/nullNODE="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod ai-service \ -o jsonpath='{.spec.nodeName}')"SELECTOR="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod ai-service \ -o jsonpath='{.spec.nodeSelector.workbook\.cs-study/node-pool}')"TOLERATION="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod ai-service \ -o jsonpath='{.spec.tolerations[?(@.key=="workbook.cs-study/dedicated")].value}')"test "$NODE" = "$COMPUTE_NODE"test "$SELECTOR" = "compute"test "$TOLERATION" = "ai"printf 'recovery app=ai-service pool=compute selector=%s toleration=%s ready=true\n' \ "$SELECTOR" "$TOLERATION"예상 출력:
pod/ai-service createdrecovery app=ai-service pool=compute selector=compute toleration=ai ready=true관찰 포인트는 selector가 compute를 요구하고 toleration이 그 Node의 taint를 허용해
두 조건을 모두 만족한 뒤에야 배치된다는 점입니다.
Lab 4 — 사용량이 아니라 request 때문에 Pending이 되는 실패 재현
섹션 제목: “Lab 4 — 사용량이 아니라 request 때문에 Pending이 되는 실패 재현”general Node의 실제 CPU 사용량을 읽지 않고, 확실히 allocatable을 넘는 가상 request를
제출합니다. 100000 CPU는 교육용 실패 fixture이며 운영 권장값이 아닙니다.
set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m6/namespace)"kubectl --context k3d-cs-study-workbook -n "$NS" apply -f - <<'YAML'apiVersion: v1kind: Podmetadata: name: api-service labels: app: api-servicespec: nodeSelector: workbook.cs-study/node-pool: general terminationGracePeriodSeconds: 3 containers: - name: api image: busybox:1.37.0@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028 imagePullPolicy: IfNotPresent command: ["sh", "-c", "sleep 3600"] resources: requests: cpu: "100000" memory: 16Mi limits: cpu: "100000" memory: 32MiYAMLREASON=""MESSAGE=""for _ in $(seq 1 30); do REASON="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod api-service \ -o jsonpath='{.status.conditions[?(@.type=="PodScheduled")].reason}')" MESSAGE="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod api-service \ -o jsonpath='{.status.conditions[?(@.type=="PodScheduled")].message}')" test "$REASON" = "Unschedulable" && break sleep 1donetest "$REASON" = "Unschedulable"case "$MESSAGE" in *"Insufficient cpu"*) ;; *) echo "expected Insufficient cpu evidence" >&2; exit 2 ;;esacset +ekubectl --context k3d-cs-study-workbook -n "$NS" \ wait --for=condition=Ready pod/api-service --timeout=5s \ > /tmp/k8s-wb-m6/request-ready.log 2>&1READY_EXIT=$?set -etest "$READY_EXIT" = "1"grep -q 'timed out waiting for the condition' /tmp/k8s-wb-m6/request-ready.logprintf 'pod=api-service phase=Pending reason=%s cause=Insufficient-cpu request=100000 ready_exit=%s\n' \ "$REASON" "$READY_EXIT"exit 1예상 출력:
pod/api-service createdpod=api-service phase=Pending reason=Unschedulable cause=Insufficient-cpu request=100000 ready_exit=1Node allocatable 값, scheduler message와 수렴 시간은 환경에 따른 변동 필드입니다. 고정 증거는
request가 후보 Node의 allocatable보다 커서 Insufficient cpu가 발생했다는 것입니다.
Lab 5 — right-sized fixture로 복구하고 QoS 확인
섹션 제목: “Lab 5 — right-sized fixture로 복구하고 QoS 확인”실제 운영값을 추측하지 않고 로컬 랩에서 들어갈 수 있는 작은 fixture로 교체합니다. production request·limit는 부하 테스트, latency, throttling과 OOM 관측을 근거로 별도 결정해야 합니다.
set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m6/namespace)"GENERAL_NODE="$(cat /tmp/k8s-wb-m6/general-node)"kubectl --context k3d-cs-study-workbook -n "$NS" delete pod api-service --wait=true >/dev/nullkubectl --context k3d-cs-study-workbook -n "$NS" apply -f - <<'YAML'apiVersion: v1kind: Podmetadata: name: api-service labels: app: api-servicespec: nodeSelector: workbook.cs-study/node-pool: general terminationGracePeriodSeconds: 3 containers: - name: api image: busybox:1.37.0@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028 imagePullPolicy: IfNotPresent command: ["sh", "-c", "sleep 3600"] resources: requests: cpu: 50m memory: 32Mi limits: cpu: 200m memory: 64MiYAMLkubectl --context k3d-cs-study-workbook -n "$NS" \ wait --for=condition=Ready pod/api-service --timeout=90s >/dev/nullNODE="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod api-service \ -o jsonpath='{.spec.nodeName}')"QOS="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod api-service \ -o jsonpath='{.status.qosClass}')"REQUESTS="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod api-service \ -o jsonpath='{.spec.containers[0].resources.requests.cpu}/{.spec.containers[0].resources.requests.memory}')"LIMITS="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod api-service \ -o jsonpath='{.spec.containers[0].resources.limits.cpu}/{.spec.containers[0].resources.limits.memory}')"test "$NODE" = "$GENERAL_NODE"test "$QOS" = "Burstable"test "$REQUESTS" = "50m/32Mi"test "$LIMITS" = "200m/64Mi"printf 'recovery app=api-service pool=general qos=%s requests=%s limits=%s ready=true\n' \ "$QOS" "$REQUESTS" "$LIMITS"예상 출력:
pod/api-service createdrecovery app=api-service pool=general qos=Burstable requests=50m/32Mi limits=200m/64Mi ready=trueLab 6 — 세 서비스의 배치 의도를 한 번에 리뷰
섹션 제목: “Lab 6 — 세 서비스의 배치 의도를 한 번에 리뷰”동적 Pod 이름이나 Node 이름 대신 각 Node의 학습용 pool label을 읽어 선언과 결과가 맞는지 검증합니다.
set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m6/namespace)"WEB_NODE="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod \ -l app=web-app -o jsonpath='{.items[0].spec.nodeName}')"AI_SERVICE_NODE="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod ai-service \ -o jsonpath='{.spec.nodeName}')"API_SERVICE_NODE="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod api-service \ -o jsonpath='{.spec.nodeName}')"WEB_POOL="$(kubectl --context k3d-cs-study-workbook get node "$WEB_NODE" \ -o jsonpath='{.metadata.labels.workbook\.cs-study/node-pool}')"AI_SERVICE_POOL="$(kubectl --context k3d-cs-study-workbook get node "$AI_SERVICE_NODE" \ -o jsonpath='{.metadata.labels.workbook\.cs-study/node-pool}')"API_SERVICE_POOL="$(kubectl --context k3d-cs-study-workbook get node "$API_SERVICE_NODE" \ -o jsonpath='{.metadata.labels.workbook\.cs-study/node-pool}')"test "$WEB_POOL" = "general"test "$AI_SERVICE_POOL" = "compute"test "$API_SERVICE_POOL" = "general"printf 'placement web-app=%s ai-service=%s api-service=%s\n' \ "$WEB_POOL" "$AI_SERVICE_POOL" "$API_SERVICE_POOL"printf 'decision selector=eligible-set toleration=taint-permission request=scheduler-fit limit=runtime-boundary\n'예상 출력:
placement web-app=general ai-service=compute api-service=generaldecision selector=eligible-set toleration=taint-permission request=scheduler-fit limit=runtime-boundary이 매핑은 개념 확인용 가상 topology입니다. 실제 예시 서비스가 어느 NKS node pool을 써야 하는지는 workload 측정, 장애 격리 목적, zone, 비용과 autoscaling 정책을 함께 검토해야 합니다.
Cleanup
섹션 제목: “Cleanup”namespace와 Node 변경을 모두 원복합니다.
set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m6/namespace)"GENERAL_NODE="$(cat /tmp/k8s-wb-m6/general-node)"COMPUTE_NODE="$(cat /tmp/k8s-wb-m6/compute-node)"kubectl --context k3d-cs-study-workbook delete namespace "$NS" --wait=false >/dev/nullkubectl --context k3d-cs-study-workbook wait --for=delete \ "namespace/$NS" --timeout=120s >/dev/nullkubectl --context k3d-cs-study-workbook label node "$GENERAL_NODE" \ workbook.cs-study/node-pool- >/dev/nullkubectl --context k3d-cs-study-workbook label node "$COMPUTE_NODE" \ workbook.cs-study/node-pool- >/dev/nullkubectl --context k3d-cs-study-workbook taint node "$COMPUTE_NODE" \ workbook.cs-study/dedicated:NoSchedule- >/dev/nullif kubectl --context k3d-cs-study-workbook get namespace "$NS" >/dev/null 2>&1; then echo "cleanup failed: namespace remains" >&2 exit 1fiLABEL_COUNT="$(kubectl --context k3d-cs-study-workbook get nodes \ -l workbook.cs-study/node-pool -o name | awk 'END {print NR+0}')"TAINT_COUNT="$(kubectl --context k3d-cs-study-workbook get nodes \ -o jsonpath='{range .items[*].spec.taints[*]}{.key}{"\n"}{end}' \ | awk '$0 == "workbook.cs-study/dedicated" {count++} END {print count+0}')"test "$LABEL_COUNT" = "0"test "$TAINT_COUNT" = "0"rm -rf /tmp/k8s-wb-m6test ! -e /tmp/k8s-wb-m6READY_NODES="$(kubectl --context k3d-cs-study-workbook get nodes --no-headers \ | awk '$2 == "Ready" {count++} END {print count+0}')"test "$READY_NODES" = "2"k3d cluster list | awk '$1 == "cs-study-workbook" && $2 == "1/1" && $3 == "1/1" \ {found=1} END {exit found ? 0 : 1}'printf 'cleanup namespace=0 workloads=0 labels=0 taints=0 fixture=0 nodes_ready=%s cluster_retained=true\n' \ "$READY_NODES"예상 출력:
cleanup namespace=0 workloads=0 labels=0 taints=0 fixture=0 nodes_ready=2 cluster_retained=truenamespace 삭제 시간은 변동 필드입니다. 학습용 workload뿐 아니라 cluster-scoped Node label과 taint까지 0인지 확인해야 cleanup이 끝납니다.
5. 관찰 포인트
섹션 제목: “5. 관찰 포인트”| 명령 뒤 | 보게 되는 것 | 운영 판단 |
|---|---|---|
| Setup의 label·taint 적용 | Node 2개가 general/compute로 구분되고 compute만 tainted | node pool 성격과 출입 제한은 서로 다른 설정입니다. |
| Lab 1의 Deployment Ready | nodeSelector=general, QoS Burstable | workload label과 Node label을 혼동하지 않습니다. |
Lab 2의 Ready wait exit 1 | Unschedulable, untolerated taint | image나 app보다 scheduler event를 먼저 봅니다. |
| Lab 3의 재생성 | selector와 toleration을 모두 만족해 compute에 배치 | toleration만으로 routing하지 않습니다. |
Lab 4의 Ready wait exit 1 | Insufficient cpu | 실제 사용률이 아니라 request 합과 allocatable을 봅니다. |
| Lab 5의 right-sized 재생성 | 같은 general 후보에서 Ready로 복구 | 운영값은 fixture 복사가 아니라 측정으로 정합니다. |
| Cleanup | namespace·label·taint·fixture 모두 0 | cluster-scoped 변경도 반드시 원복합니다. |
장애를 볼 때는 다음 순서로 층을 좁힙니다.
PodScheduled=False와reason=Unschedulable인지 봅니다.- message에서 selector/affinity 불일치, untolerated taint,
Insufficient cpu/memory를 구분합니다. - 후보 Node의 label·taint·allocatable과 이미 배치된 request 합을 확인합니다.
- Pod template을 고치고 새 Pod가 bind·Ready가 되는지 검증합니다.
- NKS node pool 증설은 Pod spec 결함을 배제한 뒤 M8의 autoscaling 계층에서 판단합니다.
6. 셀프체크 Q&A
섹션 제목: “6. 셀프체크 Q&A”Q1. AI가 “ai-service에 compute taint toleration만 넣으면 compute pool 전용 배치가 된다”고 제안했습니다. 승인하시겠습니까?
섹션 제목: “Q1. AI가 “ai-service에 compute taint toleration만 넣으면 compute pool 전용 배치가 된다”고 제안했습니다. 승인하시겠습니까?”답: 그대로는 승인하지 않습니다. toleration은 tainted Node를 후보로 허용할 뿐 그 Node로
끌어오지 않습니다. compute pool 전용이어야 한다면 검증된 pool label에 대한 nodeSelector나
required node affinity도 필요합니다. 반대로 전용이 아니라 선호라면 preferred affinity와
fallback 비용을 검토해야 합니다.
Q2. Node CPU 사용률이 낮은데 새 api-service Pod가 Insufficient cpu로 Pending입니다. 무엇을 먼저 비교해야 합니까?
섹션 제목: “Q2. Node CPU 사용률이 낮은데 새 api-service Pod가 Insufficient cpu로 Pending입니다. 무엇을 먼저 비교해야 합니까?”답: 현재 CPU 사용률이 아니라 새 Pod의 CPU request, 후보 Node의 allocatable, 이미 배치된
Pod request 합을 비교합니다. selector·taint 때문에 후보가 한 Node로 좁아졌는지도 함께 봅니다.
Pod request가 잘못 과대 설정됐다면 template을 수정하고, 정당한 수요인데 모든 후보가 차면 그때
node pool 용량이나 M8의 Cluster Autoscaler를 검토합니다.
Q3. AI가 세 서비스의 CPU request와 limit를 모두 같은 값으로 복사해 넣었습니다. 어떤 근거를 요구해야 합니까?
섹션 제목: “Q3. AI가 세 서비스의 CPU request와 limit를 모두 같은 값으로 복사해 넣었습니다. 어떤 근거를 요구해야 합니까?”답: 서비스별 평상·peak 사용량, latency와 queue 지표, CPU throttling, memory working set과 OOM 이력, replica 전략을 요구합니다. request는 배치와 autoscaling 기준에 영향을 주고 limit는 runtime 상한이므로 역할이 다릅니다. 이 랩의 작은 숫자는 재현 fixture일 뿐 NKS 운영값의 근거가 아닙니다.
7. 흔한 함정
섹션 제목: “7. 흔한 함정”함정 1 — toleration을 특정 pool routing으로 이해
섹션 제목: “함정 1 — toleration을 특정 pool routing으로 이해”toleration은 허용만 하므로 다른 untainted Node에도 배치될 수 있습니다. selector/affinity와 함께 의도를 표현합니다.
함정 2 — 실제 사용률만 보고 Pending을 해석
섹션 제목: “함정 2 — 실제 사용률만 보고 Pending을 해석”scheduler는 기본적으로 request 합과 allocatable을 봅니다. PodScheduled event와 후보 Node의
request 회계를 확인합니다.
함정 3 — NKS의 개별 Node에 수동 label만 추가
섹션 제목: “함정 3 — NKS의 개별 Node에 수동 label만 추가”node pool 교체·upgrade 뒤 Node와 설정이 사라질 수 있습니다. provider의 node pool label·taint 설정을 정본으로 관리합니다.
control plane 내부 scheduler plugin 튜닝, CNI topology 구현, Terraform으로 NKS node pool 작성, production NKS 변경은 범위 밖입니다. Pod priority/preemption, topology spread와 admission 기반 default resource 정책은 이 워크북의 심화 항목으로만 남깁니다.
8. 다음 모듈 연결 고리
섹션 제목: “8. 다음 모듈 연결 고리”배치 가능한 Pod template을 만들었으므로 M7에서는 같은 manifest를 Helm chart와 values로
패키징하고 release 단위 upgrade·rollback을 실측합니다.