콘텐츠로 이동

M10. PV·PVC와 stateless 판단

NKS로 옮길 web-app, api-service, ai-service의 Pod는 교체될 수 있으므로 로컬 filesystem에 사용자 업로드나 workflow 상태를 두면 조용한 데이터 손실이 생깁니다. AI가 만든 storage diff를 승인하려면 Pod와 함께 버릴 데이터, PVC로 남길 데이터, 관리형 DB·object storage에 맡길 데이터를 먼저 구분해야 합니다.

개념 정의의 정본은 Kubernetes BasicsDocker Basics입니다. 원본은 각각 content/topics/L5/kubernetes-basics.mdx, content/topics/L5/docker-basics.mdx이며, 이 모듈은 volume 정의를 반복하지 않고 M1의 container writable layer와 M3의 일회용 Pod를 PV·PVC binding과 삭제 경계에 연결합니다.

Pod 안의 경로가 따르는 서로 다른 수명
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 restartPod 재생성PVC 삭제적합한 데이터
container root filesystem사라질 수 있음사라짐해당 없음임시 생성물, 언제든 재생성 가능한 cache
emptyDir같은 Pod면 유지사라짐해당 없음같은 Pod container 간 scratch
PVC가 mount한 persistent data유지재부착 가능reclaim policy·provider 경계 적용명시적 수명과 filesystem이 필요한 데이터
외부 DB·object storagePod와 독립Pod와 독립Kubernetes PVC와 독립공유 상태, 사용자 원본, 업무 정합성 데이터

persistentPod보다 수명이 길다는 뜻이지 backup·replication·multi-zone 복구를 자동으로 뜻하지 않습니다. ReadWriteOnce도 “Pod 하나만 쓴다”가 아니라 단일 Node에서 read-write로 mount할 수 있다는 access mode입니다. 같은 Node의 여러 Pod가 쓰는 상황과 애플리케이션 동시성은 별도로 통제해야 합니다.

PVC가 실제 volume으로 이어지는 요청 흐름
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"]
객체·설정질문이 랩에서 관찰할 값
PVCworkload가 어떤 용량·access mode·class를 요청하는가?16Mi, ReadWriteOnce, local-path
StorageClass누가 언제 어떤 정책으로 storage를 provision하는가?rancher.io/local-path, WaitForFirstConsumer
PV어떤 claim에 실제 volume이 할당됐는가?Bound, 이름은 pvc-<dynamic UID>
reclaimPolicy: Deleteclaim 삭제 뒤 PV와 backend를 어떻게 처리하도록 요청하는가?k3d에서는 PV object 삭제를 실측
kubernetes.io/pvc-protectionPod가 claim을 쓰는 동안 어떤 object 삭제를 미루는가?PVC Terminating 유지
kubernetes.io/pv-protectionclaim에 bound된 PV object 삭제를 무엇이 미루는가?PV가 claim에서 해제될 때까지 보호
CSI deletion-protection finalizerDelete PV와 backend 삭제 순서를 무엇이 맞추는가?NKS CSI는 official-doc-only

삭제 흐름의 finalizer는 같은 역할이 아닙니다.

순서대상기다리는 조건이 모듈의 증거 등급
1PVCPod object가 PVC를 더 이상 사용하지 않을 때까지 kubernetes.io/pvc-protection이 PVC 삭제를 지연executed-local
2PVPVC에 bound된 동안 kubernetes.io/pv-protection이 PV object 삭제를 지연공식 동작 확인; 이 랩은 PV 직접 삭제를 주입하지 않음
3CSI PVDelete 정책에서 backend 삭제가 끝날 때까지 external-provisioner.volume.kubernetes.io/finalizer가 PV object 삭제를 지연NKS official-doc-only
4k3d local-path PVPod 제거 뒤 PVC와 PV object가 없어지는지 확인executed-local; Node directory 삭제 여부는 미실측

따라서 Lab 4에서 관찰한 PVC finalizer를 NKS CSI backend 삭제 완료 신호로 읽으면 안 됩니다. 각 object의 deletionTimestamp, finalizer, controller·provisioner 상태를 따로 봐야 합니다.

예시 3서비스 시스템의 기본 판단

섹션 제목: “예시 3서비스 시스템의 기본 판단”
데이터기본 배치이유·승인 질문
HTTP 요청 중 생성한 변환 scratchcontainer root 또는 emptyDir실패해도 원본에서 재생성 가능한가?
사용자 업로드·첨부 원본object storage여러 replica가 공유하고 보존·권한·lifecycle 정책이 필요한가?
비동기 작업·workflow 상태관리형 DB·queuetransaction, 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입니다.

개념비유비유의 경계
container root퇴실할 때 지우는 호텔 객실 메모판container restart의 상세 보존 여부를 메모판 하나로 설명하지 않습니다.
PVC필요한 크기와 조건을 적은 보관함 신청서PVC 자체가 실제 disk나 backup은 아닙니다.
PV신청서에 배정된 보관함물리 위치·복제 방식은 StorageClass와 provider가 결정합니다.
stateless process어느 창구 직원이든 같은 외부 원장을 보고 업무를 처리함연결·cache까지 전혀 상태가 없다는 뜻은 아닙니다.
항목검증값
기준일2026-07-16
hostmacOS arm64
Docker client / engine29.6.1 / 29.6.1
k3d5.9.0
K3s image / API serverrancher/k3s:v1.36.2-k3s1 / v1.36.2+k3s1
kubectl1.36.1
Helm4.2.3 — 이 모듈에서는 사용하지 않음
workload imagebusybox:1.37.0 + 고정 multi-platform digest
local StorageClasslocal-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-workbook cluster가 실행 중이어야 합니다.
  • 모든 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 실패 시 자신이 만든 namespace만 rollback합니다. namespace 삭제까지 실패하면 /tmp의 소유권 표식을 보존하고 exit 70으로 중단하므로 수동 확인 없이 다음 실행을 시작하지 않습니다.

#하는 일중단되면 먼저 볼 대상
1전용 /tmp와 고정 context preflightDocker·k3d·current-context
2고유 namespace 충돌 확인/tmp/k8s-wb-m10/namespace
3namespace 생성 중 실패하면 소유 자원만 rollbacknamespace 잔존 여부, exit 70
Terminal window
set -euo pipefail
# 1) 실행 상태를 모듈 전용 경로에 만들고 고정 환경을 검사한다.
rm -rf /tmp/k8s-wb-m10
mkdir -p /tmp/k8s-wb-m10
chmod 700 /tmp/k8s-wb-m10
node automation/k8s-workbook/preflight.mjs --require-cluster
test "$(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/namespace
if 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 1
fi
# 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/null
trap - EXIT
printf 'setup namespace=%s context_guard=pass\n' "$NS"

예상 출력(exit 0):

PASS platform: darwin/arm64
PASS architecture: arm64
PASS docker-cli: v29.6.1
PASS docker-engine: v29.6.1
PASS k3d: v5.9.0
PASS kubectl-skew: v1.36.1
PASS helm: v4.2.3
PASS context: k3d-cs-study-workbook
PASS kubernetes-server: v1.36.2+k3s1
setup namespace=k8s-wb-m10-<UTC run ID> context_guard=pass

Lab 1 — PVC가 없으면 Pod가 scheduling되지 않는 이유 보기

섹션 제목: “Lab 1 — PVC가 없으면 Pod가 scheduling되지 않는 이유 보기”

먼저 존재하지 않는 app-data claim을 mount하려는 Pod를 만듭니다. timeout은 의도한 실패이며 Pod가 image 실행 단계에 도달하기 전에 claim 계약에서 멈추는지 확인합니다.

Terminal window
set -euo pipefail
test "$(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/null
apiVersion: v1
kind: Pod
metadata:
name: storage-client
labels:
app: storage-client
spec:
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-data
YAML
set +e
kubectl --context k3d-cs-study-workbook -n "$NS" wait \
--for=condition=Ready pod/storage-client --timeout=5s \
>/tmp/k8s-wb-m10/missing-claim.log 2>&1
WAIT_EXIT=$?
set -e
test "$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 ;;
esac
printf '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-found

Lab 2 — PVC 요청이 PV로 binding되는 경로 확인하기

섹션 제목: “Lab 2 — PVC 요청이 PV로 binding되는 경로 확인하기”

같은 이름의 PVC를 만들면 WaitForFirstConsumer가 이미 대기 중인 Pod의 scheduling 조건을 보고 volume을 provision합니다. 동적 PV 이름과 선택 Node는 실행마다 달라집니다.

#하는 일중단되면 먼저 볼 대상
1local-path PVC 생성PVC phase와 Events
2PVC Bound와 Pod Ready 대기StorageClass provisioner, Pod scheduling message
3PVC→PV와 StorageClass 정책 검산spec.volumeName, binding mode, reclaim policy
4PVC와 container root에 비교 파일 기록mount 경로와 kubectl exec 결과
Terminal window
set -euo pipefail
test "$(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/null
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 16Mi
YAML
# 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/null
kubectl --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/pv
PROVISIONER="$(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=Delete
bound pvc=app-data phase=Bound pv=<dynamic PV> pod_ready=True node=<dynamic node>
write durable=suite-state-v1 ephemeral=pod-root-v1

Lab 3 — Pod를 교체해 filesystem 수명 비교하기

섹션 제목: “Lab 3 — Pod를 교체해 filesystem 수명 비교하기”

Pod를 삭제하고 같은 spec으로 다시 만듭니다. 새 Pod UID가 발급되고 container root의 파일은 없어지지만 같은 PVC를 mount한 /data의 파일은 남아야 합니다.

Terminal window
set -euo pipefail
test "$(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/null
kubectl --context k3d-cs-study-workbook -n "$NS" apply -f - <<'YAML' >/dev/null
apiVersion: v1
kind: Pod
metadata:
name: storage-client
labels:
app: storage-client
spec:
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-data
YAML
kubectl --context k3d-cs-study-workbook -n "$NS" wait \
--for=condition=Ready pod/storage-client --timeout=60s >/dev/null
NEW_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 2
fi
printf '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=absent

Lab 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 삭제 요청과 짧은 timeoutpvc-delete.log, PVC deletionTimestamp
2PVC in-use protection 판독PVC의 kubernetes.io/pvc-protection, Pod 존재 여부
3PV가 아직 bound인지 검산PV phase와 claimRef
4Pod를 정상 제거해 PVC 보호 조건 해소Pod terminating 상태
5PVC와 local-path PV object 삭제 수렴 확인PVC·PV object 잔존 여부; CSI backend와 혼동 금지
Terminal window
set -euo pipefail
test "$(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/null
set +e
kubectl --context k3d-cs-study-workbook -n "$NS" wait \
--for=delete pvc/app-data --timeout=3s >/tmp/k8s-wb-m10/pvc-delete.log 2>&1
WAIT_EXIT=$?
set -e
test "$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 ;;
esac
test "$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 1
done
test -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 1
done
test -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=Bound
reclaim policy=Delete pod_after=0 pvc_after=0 pv_after=0

공유 k3d cluster와 local-path StorageClass는 유지하고 모듈 namespace와 /tmp fixture만 제거합니다.

Terminal window
set -euo pipefail
test "$(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/null
test -z "$(kubectl --context k3d-cs-study-workbook get namespace "$NS" \
--ignore-not-found -o name)"
rm -rf /tmp/k8s-wb-m10
test ! -e /tmp/k8s-wb-m10
MODULE_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=retained
  • claim이 없을 때 Pod는 Pending이며 PodScheduled=False, Unschedulable입니다.
  • WaitForFirstConsumer StorageClass는 Pod가 존재할 때 PVC를 동적 PV에 binding합니다.
  • 새 Pod는 UID와 container root가 바뀌지만 같은 PVC의 /data/durable.txt를 읽습니다.
  • 사용 중 PVC 삭제 요청은 pvc-protection finalizer 때문에 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와 /tmp fixture는 모두 0입니다.

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/finalizerDelete 정책에서 backend 삭제가 끝나기 전 PV object 삭제를 미룹니다. 원인 확인 없이 어느 finalizer도 강제로 제거하지 않고 workload 분리, backup·reclaim 승인, controller·provisioner 상태를 확인한 뒤 PVC, PV, 실제 storage asset의 삭제 완료를 각각 확인합니다.

함정 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 검증을 따로 요구합니다.

다음 M11에서는 claim 누락처럼 Pod를 멈추게 하는 원인을 포함해 CrashLoopBackOff, ImagePullBackOff, Pending, OOMKilled를 증상→증거→복구 순서의 디버깅 플레이북으로 묶습니다.