M10. PV·PVC와 stateless 판단
1. 왜 필요한가
섹션 제목: “1. 왜 필요한가”NKS로 옮길 web-app, api-service, ai-service의 Pod는 교체될 수 있으므로 로컬
filesystem에 사용자 업로드나 workflow 상태를 두면 조용한 데이터 손실이 생깁니다. AI가 만든
storage diff를 승인하려면 Pod와 함께 버릴 데이터, PVC로 남길 데이터, 관리형 DB·object
storage에 맡길 데이터를 먼저 구분해야 합니다.
개념 정의의 정본은 Kubernetes Basics와
Docker Basics입니다. 원본은 각각
content/topics/L5/kubernetes-basics.mdx, content/topics/L5/docker-basics.mdx이며, 이
모듈은 volume 정의를 반복하지 않고 M1의 container writable layer와 M3의 일회용 Pod를
PV·PVC binding과 삭제 경계에 연결합니다.
2. 핵심 개념
섹션 제목: “2. 핵심 개념”flowchart LR Pod["Pod"] --> Root["container root filesystem container와 함께 교체"] Pod --> Empty["emptyDir Pod와 함께 생성·삭제"] Pod --> Mount["volumeMount /data"] Mount --> PVC["PVC namespace의 storage 요청"] PVC --> PV["PV cluster의 할당 결과"] PV --> Backend["storage backend 수명은 reclaim policy와 provider에 따름"]
| 저장 위치 | container restart | Pod 재생성 | PVC 삭제 | 적합한 데이터 |
|---|---|---|---|---|
| container root filesystem | 사라질 수 있음 | 사라짐 | 해당 없음 | 임시 생성물, 언제든 재생성 가능한 cache |
emptyDir | 같은 Pod면 유지 | 사라짐 | 해당 없음 | 같은 Pod container 간 scratch |
| PVC가 mount한 persistent data | 유지 | 재부착 가능 | reclaim policy·provider 경계 적용 | 명시적 수명과 filesystem이 필요한 데이터 |
| 외부 DB·object storage | Pod와 독립 | Pod와 독립 | Kubernetes PVC와 독립 | 공유 상태, 사용자 원본, 업무 정합성 데이터 |
persistent는 Pod보다 수명이 길다는 뜻이지 backup·replication·multi-zone 복구를 자동으로
뜻하지 않습니다. ReadWriteOnce도 “Pod 하나만 쓴다”가 아니라 단일 Node에서 read-write로
mount할 수 있다는 access mode입니다. 같은 Node의 여러 Pod가 쓰는 상황과 애플리케이션 동시성은
별도로 통제해야 합니다.
flowchart LR App["Pod claimName: app-data"] --> Claim["PVC 16Mi, ReadWriteOnce"] Claim --> Class["StorageClass local-path"] Class --> Provisioner["provisioner rancher.io/local-path"] Provisioner --> Volume["PV pvc-<dynamic UID>"] Volume --> Node["k3d Node의 local path"] Claim -. "NKS에서는 다른 계약" .-> NKS["nks-block-storage blk.csi.ncloud.com"]
| 객체·설정 | 질문 | 이 랩에서 관찰할 값 |
|---|---|---|
| PVC | workload가 어떤 용량·access mode·class를 요청하는가? | 16Mi, ReadWriteOnce, local-path |
| StorageClass | 누가 언제 어떤 정책으로 storage를 provision하는가? | rancher.io/local-path, WaitForFirstConsumer |
| PV | 어떤 claim에 실제 volume이 할당됐는가? | Bound, 이름은 pvc-<dynamic UID> |
reclaimPolicy: Delete | claim 삭제 뒤 PV와 backend를 어떻게 처리하도록 요청하는가? | k3d에서는 PV object 삭제를 실측 |
kubernetes.io/pvc-protection | Pod가 claim을 쓰는 동안 어떤 object 삭제를 미루는가? | PVC Terminating 유지 |
kubernetes.io/pv-protection | claim에 bound된 PV object 삭제를 무엇이 미루는가? | PV가 claim에서 해제될 때까지 보호 |
| CSI deletion-protection finalizer | Delete PV와 backend 삭제 순서를 무엇이 맞추는가? | NKS CSI는 official-doc-only |
삭제 흐름의 finalizer는 같은 역할이 아닙니다.
| 순서 | 대상 | 기다리는 조건 | 이 모듈의 증거 등급 |
|---|---|---|---|
| 1 | PVC | Pod object가 PVC를 더 이상 사용하지 않을 때까지 kubernetes.io/pvc-protection이 PVC 삭제를 지연 | executed-local |
| 2 | PV | PVC에 bound된 동안 kubernetes.io/pv-protection이 PV object 삭제를 지연 | 공식 동작 확인; 이 랩은 PV 직접 삭제를 주입하지 않음 |
| 3 | CSI PV | Delete 정책에서 backend 삭제가 끝날 때까지 external-provisioner.volume.kubernetes.io/finalizer가 PV object 삭제를 지연 | NKS official-doc-only |
| 4 | k3d local-path PV | Pod 제거 뒤 PVC와 PV object가 없어지는지 확인 | executed-local; Node directory 삭제 여부는 미실측 |
따라서 Lab 4에서 관찰한 PVC finalizer를 NKS CSI backend 삭제 완료 신호로 읽으면 안 됩니다.
각 object의 deletionTimestamp, finalizer, controller·provisioner 상태를 따로 봐야 합니다.
예시 3서비스 시스템의 기본 판단
섹션 제목: “예시 3서비스 시스템의 기본 판단”| 데이터 | 기본 배치 | 이유·승인 질문 |
|---|---|---|
| HTTP 요청 중 생성한 변환 scratch | container root 또는 emptyDir | 실패해도 원본에서 재생성 가능한가? |
| 사용자 업로드·첨부 원본 | object storage | 여러 replica가 공유하고 보존·권한·lifecycle 정책이 필요한가? |
| 비동기 작업·workflow 상태 | 관리형 DB·queue | transaction, backup, failover, 동시 접근이 필요한가? |
| 애플리케이션 로그 | stdout 수집 경로 | Pod disk에 쌓지 않고 M0의 stdout 계약을 지키는가? |
| 큰 model·artifact cache | 재생성 가능한 cache 우선 | PVC를 쓰면 zone·Node·동시 mount·warm-up이 scaling을 제한하지 않는가? |
세 서비스의 실제 데이터 저장 구조는 별도 설계 검토 대상입니다. 위 표는 실습용 fixture의 기본 판단 순서이며 production topology를 주장하지 않습니다.
시점 의존 설명은 2026-07-16에 다음 공식 1차 자료로 확인했습니다.
- Volumes: ephemeral volume과 persistent
volume의 Pod 수명 경계,
volumeMounts - Persistent Volumes: PV·PVC binding, access mode, reclaim policy, PVC/PV protection과 CSI deletion-protection finalizer
- Storage Classes: provisioner,
WaitForFirstConsumer,Delete·Retain - Dynamic Volume Provisioning: PVC와 StorageClass를 통한 on-demand provisioning
- NKS Block Storage CSI:
nks-block-storage,blk.csi.ncloud.com,ReadWriteOnce, reclaim policy의 NKS 경계
실제 NKS Block Storage 생성·과금·삭제, NAS CSI, snapshot·backup·restore, volume expansion과
zone 장애 복구는 승인된 non-production 환경이 없어 official-doc-only입니다. k3d
local-path는 Node-local 학습 fixture일 뿐 NKS CSI의 내구성·성능·가용성을 증명하지 않습니다.
StatefulSet 기반 DB 운영, CSI 내부 구현, control plane·CNI, Terraform 작성과 production 데이터
migration은 심화 또는 excluded입니다.
3. 직관 비유
섹션 제목: “3. 직관 비유”| 개념 | 비유 | 비유의 경계 |
|---|---|---|
| container root | 퇴실할 때 지우는 호텔 객실 메모판 | container restart의 상세 보존 여부를 메모판 하나로 설명하지 않습니다. |
| PVC | 필요한 크기와 조건을 적은 보관함 신청서 | PVC 자체가 실제 disk나 backup은 아닙니다. |
| PV | 신청서에 배정된 보관함 | 물리 위치·복제 방식은 StorageClass와 provider가 결정합니다. |
| stateless process | 어느 창구 직원이든 같은 외부 원장을 보고 업무를 처리함 | 연결·cache까지 전혀 상태가 없다는 뜻은 아닙니다. |
4. 핸즈온 랩
섹션 제목: “4. 핸즈온 랩”검증 환경
섹션 제목: “검증 환경”| 항목 | 검증값 |
|---|---|
| 기준일 | 2026-07-16 |
| 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 — 이 모듈에서는 사용하지 않음 |
| workload image | busybox:1.37.0 + 고정 multi-platform digest |
| local StorageClass | local-path, rancher.io/local-path, WaitForFirstConsumer |
| evidence 환경 | local-cluster; 고정 context + k8s-wb-m10-<UTC run ID> |
모든 shell lab은 executed-local입니다. 실제 NKS CSI와 storage asset 변경은
official-doc-only, production 변경은 excluded이며 예상 출력을 작성하지 않습니다.
사전 조건
섹션 제목: “사전 조건”- 저장소 root에서 실행하며 M0~M9를 완료한 상태여야 합니다.
- Docker Desktop과 M2의
cs-study-workbookcluster가 실행 중이어야 합니다. - 모든 namespaced API 명령은
--context k3d-cs-study-workbook과 고유 namespace를 명시합니다. - PV·StorageClass는 cluster-scoped이므로 이름 또는 claim reference로 범위를 좁혀 읽습니다.
- 각 code fence는 별도 Bash process에서 블록 단위로 실행합니다.
exit 1은 의도 실패를 증거로 남기는 블록의 최종 상태이므로 현재 interactive shell에 줄 단위로 붙여 넣지 않습니다. exit 1 블록의 정규화 출력이 예상과 같으면 다음 fence를 새 Bash process에서 계속합니다. - 예상 소요 시간은 85분이며 dynamic provisioning과 삭제 수렴 대기를 포함합니다.
Setup
섹션 제목: “Setup”고유 namespace 만들기
섹션 제목: “고유 namespace 만들기”Setup 실패 시 자신이 만든 namespace만 rollback합니다. namespace 삭제까지 실패하면 /tmp의
소유권 표식을 보존하고 exit 70으로 중단하므로 수동 확인 없이 다음 실행을 시작하지 않습니다.
| # | 하는 일 | 중단되면 먼저 볼 대상 |
|---|---|---|
| 1 | 전용 /tmp와 고정 context preflight | Docker·k3d·current-context |
| 2 | 고유 namespace 충돌 확인 | /tmp/k8s-wb-m10/namespace |
| 3 | namespace 생성 중 실패하면 소유 자원만 rollback | namespace 잔존 여부, exit 70 |
set -euo pipefail# 1) 실행 상태를 모듈 전용 경로에 만들고 고정 환경을 검사한다.rm -rf /tmp/k8s-wb-m10mkdir -p /tmp/k8s-wb-m10chmod 700 /tmp/k8s-wb-m10node automation/k8s-workbook/preflight.mjs --require-clustertest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="k8s-wb-m10-$(date -u +%Y%m%d%H%M%S)"printf '%s' "$NS" >/tmp/k8s-wb-m10/namespaceif kubectl --context k3d-cs-study-workbook get namespace "$NS" >/dev/null 2>&1; then echo "namespace collision: $NS" >&2 rm -rf /tmp/k8s-wb-m10 exit 1fi# 2) 이 실행이 소유한 namespace만 제거하는 rollback을 준비한다.rollback() { STATUS=$? trap - EXIT set +e kubectl --context k3d-cs-study-workbook delete namespace "$NS" \ --ignore-not-found --wait=true >/dev/null 2>&1 REMAINING="$(kubectl --context k3d-cs-study-workbook get namespace "$NS" \ --ignore-not-found -o name 2>/dev/null)" if test $? != 0 || test -n "$REMAINING"; then echo 'rollback incomplete: preserve /tmp/k8s-wb-m10 and inspect namespace' >&2 exit 70 fi rm -rf /tmp/k8s-wb-m10 exit "$STATUS"}trap rollback EXIT# 3) namespace 생성이 성공하면 Setup rollback을 해제한다.kubectl --context k3d-cs-study-workbook create namespace "$NS" >/dev/nulltrap - EXITprintf 'setup namespace=%s context_guard=pass\n' "$NS"예상 출력(exit 0):
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+k3s1setup namespace=k8s-wb-m10-<UTC run ID> context_guard=passLab 1 — PVC가 없으면 Pod가 scheduling되지 않는 이유 보기
섹션 제목: “Lab 1 — PVC가 없으면 Pod가 scheduling되지 않는 이유 보기”먼저 존재하지 않는 app-data claim을 mount하려는 Pod를 만듭니다. timeout은 의도한 실패이며
Pod가 image 실행 단계에 도달하기 전에 claim 계약에서 멈추는지 확인합니다.
set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m10/namespace)"kubectl --context k3d-cs-study-workbook -n "$NS" apply -f - <<'YAML' >/dev/nullapiVersion: v1kind: Podmetadata: name: storage-client labels: app: storage-clientspec: containers: - name: app image: busybox:1.37.0@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028 imagePullPolicy: IfNotPresent command: ["sh", "-c", "sleep 3600"] volumeMounts: - name: data mountPath: /data volumes: - name: data persistentVolumeClaim: claimName: app-dataYAMLset +ekubectl --context k3d-cs-study-workbook -n "$NS" wait \ --for=condition=Ready pod/storage-client --timeout=5s \ >/tmp/k8s-wb-m10/missing-claim.log 2>&1WAIT_EXIT=$?set -etest "$WAIT_EXIT" = "1"PHASE="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod storage-client \ -o jsonpath='{.status.phase}')"REASON="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod storage-client \ -o jsonpath='{.status.conditions[?(@.type=="PodScheduled")].reason}')"MESSAGE="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod storage-client \ -o jsonpath='{.status.conditions[?(@.type=="PodScheduled")].message}')"test "$PHASE" = "Pending"test "$REASON" = "Unschedulable"case "$MESSAGE" in *"persistentvolumeclaim \"app-data\" not found"*) MESSAGE_KIND="pvc-not-found" ;; *) printf 'unexpected scheduling message: %s\n' "$MESSAGE" >&2; exit 2 ;;esacprintf 'missing_claim wait_exit=%s phase=%s reason=%s message=%s\n' \ "$WAIT_EXIT" "$PHASE" "$REASON" "$MESSAGE_KIND"exit 1예상 출력(exit 1, 의도한 timeout을 관찰하고 원인을 판독):
missing_claim wait_exit=1 phase=Pending reason=Unschedulable message=pvc-not-foundLab 2 — PVC 요청이 PV로 binding되는 경로 확인하기
섹션 제목: “Lab 2 — PVC 요청이 PV로 binding되는 경로 확인하기”같은 이름의 PVC를 만들면 WaitForFirstConsumer가 이미 대기 중인 Pod의 scheduling 조건을 보고
volume을 provision합니다. 동적 PV 이름과 선택 Node는 실행마다 달라집니다.
| # | 하는 일 | 중단되면 먼저 볼 대상 |
|---|---|---|
| 1 | local-path PVC 생성 | PVC phase와 Events |
| 2 | PVC Bound와 Pod Ready 대기 | StorageClass provisioner, Pod scheduling message |
| 3 | PVC→PV와 StorageClass 정책 검산 | spec.volumeName, binding mode, reclaim policy |
| 4 | PVC와 container root에 비교 파일 기록 | mount 경로와 kubectl exec 결과 |
set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m10/namespace)"# 1) Pending Pod가 요구한 이름과 같은 PVC를 만든다.kubectl --context k3d-cs-study-workbook -n "$NS" apply -f - <<'YAML' >/dev/nullapiVersion: v1kind: PersistentVolumeClaimmetadata: name: app-dataspec: accessModes: - ReadWriteOnce storageClassName: local-path resources: requests: storage: 16MiYAML# 2) WaitForFirstConsumer provisioning과 Pod scheduling의 수렴을 기다린다.kubectl --context k3d-cs-study-workbook -n "$NS" wait \ --for=jsonpath='{.status.phase}'=Bound pvc/app-data --timeout=60s >/dev/nullkubectl --context k3d-cs-study-workbook -n "$NS" wait \ --for=condition=Ready pod/storage-client --timeout=60s >/dev/null# 3) PVC, PV, StorageClass가 만든 실제 binding 계약을 검산한다.PV="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pvc app-data \ -o jsonpath='{.spec.volumeName}')"printf '%s' "$PV" >/tmp/k8s-wb-m10/pvPROVISIONER="$(kubectl --context k3d-cs-study-workbook get storageclass local-path \ -o jsonpath='{.provisioner}')"BINDING="$(kubectl --context k3d-cs-study-workbook get storageclass local-path \ -o jsonpath='{.volumeBindingMode}')"RECLAIM="$(kubectl --context k3d-cs-study-workbook get pv "$PV" \ -o jsonpath='{.spec.persistentVolumeReclaimPolicy}')"PVC_PHASE="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pvc app-data \ -o jsonpath='{.status.phase}')"POD_READY="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod storage-client \ -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}')"NODE="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod storage-client \ -o jsonpath='{.spec.nodeName}')"test "$PROVISIONER" = "rancher.io/local-path"test "$BINDING" = "WaitForFirstConsumer"test "$RECLAIM" = "Delete"test "$PVC_PHASE" = "Bound"test "$POD_READY" = "True"test -n "$PV"test -n "$NODE"# 4) Pod 교체 뒤 비교할 persistent 파일과 root filesystem 파일을 기록한다.kubectl --context k3d-cs-study-workbook -n "$NS" exec storage-client -- \ sh -c "printf 'suite-state-v1\n' >/data/durable.txt; printf 'pod-root-v1\n' >/tmp/ephemeral.txt"DURABLE="$(kubectl --context k3d-cs-study-workbook -n "$NS" exec storage-client -- \ cat /data/durable.txt)"EPHEMERAL="$(kubectl --context k3d-cs-study-workbook -n "$NS" exec storage-client -- \ cat /tmp/ephemeral.txt)"test "$DURABLE" = "suite-state-v1"test "$EPHEMERAL" = "pod-root-v1"printf 'storageclass name=local-path provisioner=%s binding=%s reclaim=%s\n' \ "$PROVISIONER" "$BINDING" "$RECLAIM"printf 'bound pvc=app-data phase=%s pv=<dynamic PV> pod_ready=%s node=<dynamic node>\n' \ "$PVC_PHASE" "$POD_READY"printf 'write durable=%s ephemeral=%s\n' "$DURABLE" "$EPHEMERAL"예상 출력(exit 0):
storageclass name=local-path provisioner=rancher.io/local-path binding=WaitForFirstConsumer reclaim=Deletebound pvc=app-data phase=Bound pv=<dynamic PV> pod_ready=True node=<dynamic node>write durable=suite-state-v1 ephemeral=pod-root-v1Lab 3 — Pod를 교체해 filesystem 수명 비교하기
섹션 제목: “Lab 3 — Pod를 교체해 filesystem 수명 비교하기”Pod를 삭제하고 같은 spec으로 다시 만듭니다. 새 Pod UID가 발급되고 container root의 파일은
없어지지만 같은 PVC를 mount한 /data의 파일은 남아야 합니다.
set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m10/namespace)"PV="$(cat /tmp/k8s-wb-m10/pv)"OLD_UID="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod storage-client \ -o jsonpath='{.metadata.uid}')"kubectl --context k3d-cs-study-workbook -n "$NS" delete pod storage-client \ --wait=true >/dev/nullkubectl --context k3d-cs-study-workbook -n "$NS" apply -f - <<'YAML' >/dev/nullapiVersion: v1kind: Podmetadata: name: storage-client labels: app: storage-clientspec: containers: - name: app image: busybox:1.37.0@sha256:9532d8c39891ca2ecde4d30d7710e01fb739c87a8b9299685c63704296b16028 imagePullPolicy: IfNotPresent command: ["sh", "-c", "sleep 3600"] volumeMounts: - name: data mountPath: /data volumes: - name: data persistentVolumeClaim: claimName: app-dataYAMLkubectl --context k3d-cs-study-workbook -n "$NS" wait \ --for=condition=Ready pod/storage-client --timeout=60s >/dev/nullNEW_UID="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pod storage-client \ -o jsonpath='{.metadata.uid}')"NEW_PV="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pvc app-data \ -o jsonpath='{.spec.volumeName}')"DURABLE="$(kubectl --context k3d-cs-study-workbook -n "$NS" exec storage-client -- \ cat /data/durable.txt)"test "$OLD_UID" != "$NEW_UID"test "$PV" = "$NEW_PV"test "$DURABLE" = "suite-state-v1"if kubectl --context k3d-cs-study-workbook -n "$NS" exec storage-client -- \ test -e /tmp/ephemeral.txt >/dev/null 2>&1; then echo 'unexpected root file survived Pod replacement' >&2 exit 2fiprintf 'replacement uid_changed=true pv_same=true durable=%s root_file=absent\n' "$DURABLE"예상 출력(exit 0):
replacement uid_changed=true pv_same=true durable=suite-state-v1 root_file=absentLab 4 — 사용 중 PVC 보호와 Delete reclaim 경계 보기
섹션 제목: “Lab 4 — 사용 중 PVC 보호와 Delete reclaim 경계 보기”사용 중인 PVC 삭제를 요청하면 pvc-protection finalizer 때문에 짧은 삭제 대기가 실패해야
합니다. Pod를 먼저 제거한 뒤 PVC와 PV object가 삭제되는 것까지 확인합니다. 이 랩은 PV object
수명을 실측하며 local-path Node directory, NKS CSI backend 삭제, backup 또는 데이터 복구
불가능성을 증명하지 않습니다.
| # | 하는 일 | 중단되면 먼저 볼 대상 |
|---|---|---|
| 1 | 사용 중 PVC 삭제 요청과 짧은 timeout | pvc-delete.log, PVC deletionTimestamp |
| 2 | PVC in-use protection 판독 | PVC의 kubernetes.io/pvc-protection, Pod 존재 여부 |
| 3 | PV가 아직 bound인지 검산 | PV phase와 claimRef |
| 4 | Pod를 정상 제거해 PVC 보호 조건 해소 | Pod terminating 상태 |
| 5 | PVC와 local-path PV object 삭제 수렴 확인 | PVC·PV object 잔존 여부; CSI backend와 혼동 금지 |
set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m10/namespace)"PV="$(cat /tmp/k8s-wb-m10/pv)"# 1) 사용 중 claim 삭제를 요청하고 의도한 timeout exit를 캡처한다.kubectl --context k3d-cs-study-workbook -n "$NS" delete pvc app-data \ --wait=false >/dev/nullset +ekubectl --context k3d-cs-study-workbook -n "$NS" wait \ --for=delete pvc/app-data --timeout=3s >/tmp/k8s-wb-m10/pvc-delete.log 2>&1WAIT_EXIT=$?set -etest "$WAIT_EXIT" = "1"# 2) PVC object에 적용된 in-use protection을 판독한다.DELETING="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pvc app-data \ -o jsonpath='{.metadata.deletionTimestamp}')"FINALIZERS="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pvc app-data \ -o jsonpath='{.metadata.finalizers[*]}')"PV_BEFORE="$(kubectl --context k3d-cs-study-workbook get pv "$PV" \ -o jsonpath='{.status.phase}')"test -n "$DELETING"case "$FINALIZERS" in *kubernetes.io/pvc-protection*) FINALIZER_KIND="present" ;; *) printf 'pvc-protection finalizer missing: %s\n' "$FINALIZERS" >&2; exit 2 ;;esactest "$PV_BEFORE" = "Bound"# 3) Pod를 정상 제거해 PVC protection의 대기 조건을 해소한다.kubectl --context k3d-cs-study-workbook -n "$NS" delete pod storage-client \ --wait=true >/dev/null# 4) PVC object 삭제가 완료되는지 기다린다.for _ in $(seq 1 60); do PVC_LEFT="$(kubectl --context k3d-cs-study-workbook -n "$NS" get pvc app-data \ --ignore-not-found -o name)" test -z "$PVC_LEFT" && break sleep 1donetest -z "$PVC_LEFT"# 5) Delete 정책의 local-path PV object 삭제가 완료되는지 기다린다.for _ in $(seq 1 60); do PV_LEFT="$(kubectl --context k3d-cs-study-workbook get pv "$PV" \ --ignore-not-found -o name)" test -z "$PV_LEFT" && break sleep 1donetest -z "$PV_LEFT"printf 'pvc_protection wait_exit=%s deleting=true finalizer=%s pv_before=%s\n' \ "$WAIT_EXIT" "$FINALIZER_KIND" "$PV_BEFORE"printf 'reclaim policy=Delete pod_after=0 pvc_after=0 pv_after=0\n'exit 1예상 출력(exit 1, 사용 중 삭제를 막는 경계와 안전한 해제 완료):
pvc_protection wait_exit=1 deleting=true finalizer=present pv_before=Boundreclaim policy=Delete pod_after=0 pvc_after=0 pv_after=0Cleanup
섹션 제목: “Cleanup”공유 k3d cluster와 local-path StorageClass는 유지하고 모듈 namespace와 /tmp fixture만
제거합니다.
set -euo pipefailtest "$(kubectl config current-context)" = "k3d-cs-study-workbook"NS="$(cat /tmp/k8s-wb-m10/namespace)"kubectl --context k3d-cs-study-workbook delete namespace "$NS" --wait=true >/dev/nulltest -z "$(kubectl --context k3d-cs-study-workbook get namespace "$NS" \ --ignore-not-found -o name)"rm -rf /tmp/k8s-wb-m10test ! -e /tmp/k8s-wb-m10MODULE_PV="$(kubectl --context k3d-cs-study-workbook get pv \ -o jsonpath='{range .items[?(@.spec.claimRef.namespace=="'"$NS"'")]}{.metadata.name}{"\n"}{end}')"test -z "$MODULE_PV"NODE_READY="$(kubectl --context k3d-cs-study-workbook get nodes --no-headers \ | awk '$2 == "Ready" {count++} END {print count+0}')"test "$NODE_READY" = "2"PROVISIONER="$(kubectl --context k3d-cs-study-workbook get storageclass local-path \ -o jsonpath='{.provisioner}')"test "$PROVISIONER" = "rancher.io/local-path"printf 'cleanup namespace=0 pod=0 pvc=0 pv=0 tmp=0 nodes_ready=%s storageclass=retained cluster=retained\n' \ "$NODE_READY"예상 출력(exit 0):
cleanup namespace=0 pod=0 pvc=0 pv=0 tmp=0 nodes_ready=2 storageclass=retained cluster=retained5. 관찰 포인트
섹션 제목: “5. 관찰 포인트”- claim이 없을 때 Pod는
Pending이며PodScheduled=False,Unschedulable입니다. WaitForFirstConsumerStorageClass는 Pod가 존재할 때 PVC를 동적 PV에 binding합니다.- 새 Pod는 UID와 container root가 바뀌지만 같은 PVC의
/data/durable.txt를 읽습니다. - 사용 중 PVC 삭제 요청은
pvc-protectionfinalizer 때문에Terminating에서 기다립니다. - Pod를 제거하면 PVC 삭제가 완료되고
Delete정책의 local-path PV object도 사라집니다. - NKS CSI에서는 별도 deletion-protection finalizer가 backend 삭제와 PV object 삭제 순서를
맞춥니다. 이 동작은
official-doc-only이며 PVC in-use protection과 다른 단계입니다. - cleanup 뒤 M10 namespace, Pod, PVC, claim에 속한 PV와
/tmpfixture는 모두 0입니다.
6. 셀프체크 Q&A
섹션 제목: “6. 셀프체크 Q&A”Q1. ai-service Pod를 교체했더니 /tmp/result.json은 사라졌지만 PVC의 파일은 남았습니다. 어떤 데이터를 각각 두어야 합니까?
섹션 제목: “Q1. ai-service Pod를 교체했더니 /tmp/result.json은 사라졌지만 PVC의 파일은 남았습니다. 어떤 데이터를 각각 두어야 합니까?”답: /tmp에는 실패해도 입력에서 다시 만들 수 있는 중간 결과만 둡니다. 업무 진행 상태,
사용자 원본, 재처리 불가능한 결과는 Pod-local 경로에 두지 않습니다. PVC가 filesystem 요구에는
맞을 수 있지만 공유·backup·failover·transaction이 필요하면 관리형 DB나 object storage를 먼저
검토합니다.
Q2. AI가 api-service replica 3개가 하나의 ReadWriteOnce PVC를 함께 mount하도록 제안했습니다. 그대로 승인해도 됩니까?
섹션 제목: “Q2. AI가 api-service replica 3개가 하나의 ReadWriteOnce PVC를 함께 mount하도록 제안했습니다. 그대로 승인해도 됩니까?”답: 승인하지 않습니다. ReadWriteOnce는 단일 Pod가 아니라 단일 Node read-write mount
경계이므로 replica가 같은 Node에 몰리거나 다른 Node에서 scheduling·attach가 막힐 수 있고, 여러
process의 파일 동시 쓰기도 해결하지 않습니다. 상태는 관리형 DB·object storage로 분리하고 Pod를
stateless하게 둘 수 있는지 먼저 검토합니다.
Q3. 사용 중 PVC 삭제가 Terminating에서 멈췄고 PV 정책은 Delete입니다. finalizer를 강제로 지워도 됩니까?
섹션 제목: “Q3. 사용 중 PVC 삭제가 Terminating에서 멈췄고 PV 정책은 Delete입니다. finalizer를 강제로 지워도 됩니까?”답: 먼저 finalizer가 붙은 object부터 구분합니다. PVC의 kubernetes.io/pvc-protection은
Pod object가 claim을 사용하는 동안 PVC 삭제를 미룹니다. PV의 kubernetes.io/pv-protection은
bound 관계를 보호하고, CSI의 external-provisioner.volume.kubernetes.io/finalizer는 Delete
정책에서 backend 삭제가 끝나기 전 PV object 삭제를 미룹니다. 원인 확인 없이 어느 finalizer도
강제로 제거하지 않고 workload 분리, backup·reclaim 승인, controller·provisioner 상태를 확인한
뒤 PVC, PV, 실제 storage asset의 삭제 완료를 각각 확인합니다.
7. 흔한 함정
섹션 제목: “7. 흔한 함정”함정 1 — PVC가 있으면 backup과 고가용성도 해결됐다고 보기
섹션 제목: “함정 1 — PVC가 있으면 backup과 고가용성도 해결됐다고 보기”PVC는 storage 요청과 mount 경계를 제공할 뿐 backup, snapshot 주기, replication, restore drill, zone 장애 복구를 자동 보장하지 않습니다. provider별 정책과 복구 실측을 별도 승인합니다.
함정 2 — ReadWriteOnce를 “Pod 하나만 쓰기”로 해석하기
섹션 제목: “함정 2 — ReadWriteOnce를 “Pod 하나만 쓰기”로 해석하기”access mode는 storage attach·mount 능력을 표현합니다. 같은 Node의 여러 Pod나 process가 파일을 어떻게 동기화하는지는 애플리케이션과 filesystem의 별도 문제입니다.
함정 3 — k3d local-path 성공을 NKS CSI 내구성 증거로 쓰기
섹션 제목: “함정 3 — k3d local-path 성공을 NKS CSI 내구성 증거로 쓰기”이 랩은 Node-local provisioner와 축소된 fixture만 검증합니다. NKS의 Block Storage·NAS,
WaitForFirstConsumer, zone topology, detach·reattach, snapshot·비용은 공식 문서와 승인된
non-production 검증을 따로 요구합니다.
8. 다음 모듈 연결 고리
섹션 제목: “8. 다음 모듈 연결 고리”다음 M11에서는 claim 누락처럼 Pod를 멈추게 하는 원인을 포함해 CrashLoopBackOff,
ImagePullBackOff, Pending, OOMKilled를 증상→증거→복구 순서의 디버깅 플레이북으로 묶습니다.