← До програми курсу

Модуль 9 / 19

Volumes, PV, PVC, StorageClass і межі single-node storage

KUBERNETES

Зміст
01

1. Цілі та результат модуля

Ви навчитеся обирати lifetime даних, зв’язувати PVC з PV, читати StorageClass і діагностувати Pending/ContainerCreating без припущення, що Kubernetes сам надає диск.

  • container filesystem не є persistent storage
  • emptyDir живе разом із Pod
  • PVC виражає запит застосунку
  • PV представляє ресурс зберігання
  • StorageClass описує спосіб надання volume
02

2. Теорія: filesystem і lifetime

Writable layer контейнера пов’язаний із конкретним container instance. Volume монтується в контейнер, але lifetime залежить від типу: emptyDir переживає restart контейнера, проте видаляється разом із Pod; persistent volume існує незалежно від Pod.

Volume вирішує доступ до даних, але не робить застосунок stateful-safe: consistency, backup, replication і recovery лишаються обов’язками storage system та застосунку.

03

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, вільне місце чи роботу застосунку.

04

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.

05

5. Теорія: access modes і volumeMode

ReadWriteOnce означає read-write mount на одному Node, а не рівно одним Pod. ReadOnlyMany і ReadWriteMany потребують підтримки backend; ReadWriteOncePod обмежує доступ одним Pod і залежить від CSI. Ці flags описують можливості підключення, а не POSIX permissions.

Filesystem монтує файлову систему; Block надає raw block device. Режим обирають за вимогами застосунку.

06

6. Local storage і межі одного вузла

local PV прив’язаний до конкретного Node через nodeAffinity. Якщо Node втрачено, scheduler не перенесе дані на інший вузол. Це корисно для вивчення API, але не є HA storage.

hostPath ще сильніше зв’язує Pod із host path і несе security-ризик; для звичайних застосунків кращий CSI-backed storage з визначеними backup та recovery guarantees.

07

7. Методичні вказівки

  • Спочатку визначте lifetime і consistency, потім тип volume
  • Розділяйте Pending PVC, scheduling, mount та application errors
  • Перевіряйте StorageClass/provisioner, accessModes, capacity і topology
  • Не вважайте RWO синонімом одного Pod
  • Не видаляйте PV до перевірки reclaim policy
  • Не називайте local PV високодоступним
  • Плануйте backup окремо від manifests
08

8. Лабораторна робота: emptyDir

Два контейнери одного Pod обмінюються файлом. Restart writer не видаляє emptyDir; повторне створення Pod створило б новий volume.

terminal
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/events
09

9. Лабораторна робота: StorageClass, PV і PVC

Створіть host directory, клас без provisioner, static local PV і claim. До consumer PVC може лишатися Pending через WaitForFirstConsumer.

terminal
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 data
10

10. Лабораторна робота: binding і збереження даних

Deployment запускає init container лише для початкового наповнення файла. Після заміни Pod ім’я, записане першим Pod, зберігається: persistent volume пережив Pod lifecycle.

terminal
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.html
11

11. Діагностика: Pending PVC і Pod

Перший claim просить неіснуючий class і не може динамічно отримати volume. Другий Pod посилається на відсутній claim. Порівняйте Events: однаковий Pending status має різні причини.

terminal
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

12. Контроль, reclaim і cleanup

  • Що переживає restart контейнера?
  • Коли видаляється emptyDir?
  • Чому Pod посилається на PVC?
  • Що гарантує Bound і чого не гарантує?
  • Навіщо WaitForFirstConsumer?
  • Чому RWO не означає один Pod?
  • Чим local PV відрізняється від HA storage?
  • Що робить Retain?
  • Де шукати mount failure?
  • Чому snapshot не дорівнює backup strategy?
terminal
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-*.yaml

Retain лишає дані після видалення PVC: оператор перевіряє їх і очищає вручну. Модуль засвоєно, якщо ви пояснюєте lifetime, binding і reclaim та знаходите причину Pending за evidence.