← К программе курса

Модуль 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 и некоторых специальных workloads, но не является 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 отдельно от Kubernetes 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.