Модуль 9 / 19
Volumes, PV, PVC, StorageClass і межі single-node storage
Зміст
1. Цілі та результат модуля
Ви навчитеся обирати lifetime даних, зв’язувати PVC з PV, читати StorageClass і діагностувати Pending/ContainerCreating без припущення, що Kubernetes сам надає диск.
- container filesystem не є persistent storage
- emptyDir живе разом із Pod
- PVC виражає запит застосунку
- PV представляє ресурс зберігання
- StorageClass описує спосіб надання volume
2. Теорія: filesystem і lifetime
Writable layer контейнера пов’язаний із конкретним container instance. Volume монтується в контейнер, але lifetime залежить від типу: emptyDir переживає restart контейнера, проте видаляється разом із Pod; persistent volume існує незалежно від Pod.
Volume вирішує доступ до даних, але не робить застосунок stateful-safe: consistency, backup, replication і recovery лишаються обов’язками storage system та застосунку.
3. Теорія: PV, PVC і binding
PV — cluster-scoped об’єкт із capacity, access modes, volumeMode, class, reclaim policy та джерелом storage. PVC — namespaced запит. Control plane зв’язує сумісні об’єкти; Pod посилається на PVC, а не на конкретний PV.
Bound означає збіг control-plane об’єктів, але не гарантує успішний mount, права filesystem, вільне місце чи роботу застосунку.
4. Теорія: StorageClass і provisioning
StorageClass задає provisioner і параметри. Dynamic provisioner/CSI може створити volume за PVC; без нього claim лишається Pending, якщо сумісний PV не створено заздалегідь. На стенді немає CSI, тому використовується чесний static local PV.
WaitForFirstConsumer відкладає binding до появи Pod, щоб scheduler урахував topology. Immediate binding може обрати volume до визначення розміщення workload.
5. Теорія: access modes і volumeMode
ReadWriteOnce означає read-write mount на одному Node, а не рівно одним Pod. ReadOnlyMany і ReadWriteMany потребують підтримки backend; ReadWriteOncePod обмежує доступ одним Pod і залежить від CSI. Ці flags описують можливості підключення, а не POSIX permissions.
Filesystem монтує файлову систему; Block надає raw block device. Режим обирають за вимогами застосунку.
6. Local storage і межі одного вузла
local PV прив’язаний до конкретного Node через nodeAffinity. Якщо Node втрачено, scheduler не перенесе дані на інший вузол. Це корисно для вивчення API, але не є HA storage.
hostPath ще сильніше зв’язує Pod із host path і несе security-ризик; для звичайних застосунків кращий CSI-backed storage з визначеними backup та recovery guarantees.
7. Методичні вказівки
- Спочатку визначте lifetime і consistency, потім тип volume
- Розділяйте Pending PVC, scheduling, mount та application errors
- Перевіряйте StorageClass/provisioner, accessModes, capacity і topology
- Не вважайте RWO синонімом одного Pod
- Не видаляйте PV до перевірки reclaim policy
- Не називайте local PV високодоступним
- Плануйте backup окремо від manifests
8. Лабораторна робота: emptyDir
Два контейнери одного Pod обмінюються файлом. Restart writer не видаляє emptyDir; повторне створення Pod створило б новий volume.
kubectl create namespace k8s-m09
cat > /tmp/k8s-m09-emptydir.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata: {name: shared, namespace: k8s-m09}
spec:
containers:
- name: writer
image: busybox:1.37
command: ["sh", "-c", "while true; do date -Iseconds >> /shared/events; sleep 5; done"]
volumeMounts: [{name: shared, mountPath: /shared}]
- name: reader
image: busybox:1.37
command: ["sh", "-c", "tail -F /shared/events"]
volumeMounts: [{name: shared, mountPath: /shared}]
volumes:
- name: shared
emptyDir: {}
EOF
kubectl apply --dry-run=server -f /tmp/k8s-m09-emptydir.yaml
kubectl apply -f /tmp/k8s-m09-emptydir.yaml
kubectl -n k8s-m09 wait --for=condition=Ready pod/shared --timeout=120s
kubectl -n k8s-m09 logs shared -c reader --tail=5
kubectl -n k8s-m09 exec shared -c writer -- kill 1
kubectl -n k8s-m09 wait --for=jsonpath='{.status.containerStatuses[0].restartCount}'=1 pod/shared --timeout=120s
kubectl -n k8s-m09 exec shared -c reader -- tail -3 /shared/events9. Лабораторна робота: StorageClass, PV і PVC
Створіть host directory, клас без provisioner, static local PV і claim. До consumer PVC може лишатися Pending через WaitForFirstConsumer.
sudo install -d -m 0777 /var/lib/k8s-course/m09
cat > /tmp/k8s-m09-storage.yaml <<'EOF'
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata: {name: manual-local}
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
---
apiVersion: v1
kind: PersistentVolume
metadata: {name: k8s-m09-data}
spec:
capacity: {storage: 1Gi}
volumeMode: Filesystem
accessModes: [ReadWriteOnce]
persistentVolumeReclaimPolicy: Retain
storageClassName: manual-local
local: {path: /var/lib/k8s-course/m09}
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- {key: kubernetes.io/hostname, operator: In, values: [k8s-fundamentals-1]}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: {name: data, namespace: k8s-m09}
spec:
accessModes: [ReadWriteOnce]
storageClassName: manual-local
resources: {requests: {storage: 512Mi}}
EOF
kubectl apply --dry-run=server -f /tmp/k8s-m09-storage.yaml
kubectl apply -f /tmp/k8s-m09-storage.yaml
kubectl get storageclass,pv
kubectl -n k8s-m09 get pvc data
kubectl -n k8s-m09 describe pvc data10. Лабораторна робота: binding і збереження даних
Deployment запускає init container лише для початкового наповнення файла. Після заміни Pod ім’я, записане першим Pod, зберігається: persistent volume пережив Pod lifecycle.
cat > /tmp/k8s-m09-web.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata: {name: web, namespace: k8s-m09}
spec:
replicas: 1
selector: {matchLabels: {app: storage-web}}
template:
metadata: {labels: {app: storage-web}}
spec:
initContainers:
- name: seed
image: busybox:1.37
command: ["sh", "-c", "test -f /data/index.html || printf 'created-by=%s\n' "$HOSTNAME" > /data/index.html"]
volumeMounts: [{name: data, mountPath: /data}]
containers:
- name: nginx
image: nginx:1.29-alpine
ports: [{name: http, containerPort: 80}]
volumeMounts: [{name: data, mountPath: /usr/share/nginx/html}]
volumes:
- name: data
persistentVolumeClaim: {claimName: data}
EOF
kubectl apply --dry-run=server -f /tmp/k8s-m09-web.yaml
kubectl apply -f /tmp/k8s-m09-web.yaml
kubectl -n k8s-m09 rollout status deployment/web --timeout=180s
kubectl get pv k8s-m09-data
kubectl -n k8s-m09 get pvc data
pod=$(kubectl -n k8s-m09 get pod -l app=storage-web -o jsonpath='{.items[0].metadata.name}')
kubectl -n k8s-m09 exec "$pod" -- cat /usr/share/nginx/html/index.html
kubectl -n k8s-m09 delete pod "$pod" --wait=true
kubectl -n k8s-m09 rollout status deployment/web --timeout=180s
new_pod=$(kubectl -n k8s-m09 get pod -l app=storage-web -o jsonpath='{.items[0].metadata.name}')
kubectl -n k8s-m09 exec "$new_pod" -- cat /usr/share/nginx/html/index.html11. Діагностика: Pending PVC і Pod
Перший claim просить неіснуючий class і не може динамічно отримати volume. Другий Pod посилається на відсутній claim. Порівняйте Events: однаковий Pending status має різні причини.
cat > /tmp/k8s-m09-broken.yaml <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata: {name: impossible, namespace: k8s-m09}
spec:
accessModes: [ReadWriteOnce]
storageClassName: absent-provisioner
resources: {requests: {storage: 20Gi}}
---
apiVersion: v1
kind: Pod
metadata: {name: broken-consumer, namespace: k8s-m09}
spec:
containers:
- {name: busybox, image: busybox:1.37, command: ["sh", "-c", "sleep 3600"]}
volumes:
- name: data
persistentVolumeClaim: {claimName: claim-does-not-exist}
EOF
kubectl apply -f /tmp/k8s-m09-broken.yaml
kubectl -n k8s-m09 get pvc,pod
kubectl -n k8s-m09 describe pvc impossible
kubectl -n k8s-m09 describe pod broken-consumer
kubectl -n k8s-m09 get events --sort-by=.metadata.creationTimestamp
kubectl -n k8s-m09 delete pod broken-consumer
kubectl -n k8s-m09 delete pvc impossibleПорядок: PVC status/events → StorageClass/provisioner → доступні PV і сумісність → Pod scheduling events → kubelet mount events → permissions/capacity → application logs.
12. Контроль, reclaim і cleanup
- Що переживає restart контейнера?
- Коли видаляється emptyDir?
- Чому Pod посилається на PVC?
- Що гарантує Bound і чого не гарантує?
- Навіщо WaitForFirstConsumer?
- Чому RWO не означає один Pod?
- Чим local PV відрізняється від HA storage?
- Що робить Retain?
- Де шукати mount failure?
- Чому snapshot не дорівнює backup strategy?
kubectl delete namespace k8s-m09 --wait=true
kubectl get pv k8s-m09-data
sudo cat /var/lib/k8s-course/m09/index.html
kubectl delete pv k8s-m09-data
kubectl delete storageclass manual-local
sudo rm -rf /var/lib/k8s-course/m09
rm -f /tmp/k8s-m09-*.yamlRetain лишає дані після видалення PVC: оператор перевіряє їх і очищає вручну. Модуль засвоєно, якщо ви пояснюєте lifetime, binding і reclaim та знаходите причину Pending за evidence.