Learning Platform
Глоссарий Troubleshooting
Урок 08.03 · 25 мин
Продвинутый
StorageClassDynamic provisioningvolumeBindingModeWaitForFirstConsumerDefault storage classVolume expansion

StorageClass и dynamic provisioning

В прошлом уроке PV создавался статически — администратор руками регистрировал кусок storage и ждал, пока разработчик создаст подходящий PVC. Это работает в маленьких кластерах с предсказуемой нагрузкой, но в production не масштабируется: сотни команд создают PVC на ходу, и админ не может вручную регистрировать каждый.

Решение — dynamic provisioning через StorageClass. SC описывает, как создавать storage: какой driver, какие параметры, в какой политике. Когда появляется PVC со ссылкой на SC, K8s автоматически создаёт PV через provisioner-driver, bind-ит PVC к нему, и Pod может его использовать.

В этом уроке — что такое SC, как работает dynamic flow, чем отличаются Immediate и WaitForFirstConsumer binding, как настроить default SC и volume expansion.


Docker named volumes: lifecycle, drivers, backup

Анатомия StorageClass

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
  iops: "3000"
  throughput: "125"
  encrypted: "true"
  kmsKeyId: "arn:aws:kms:us-east-1:123456789012:key/abcd1234"
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
mountOptions:
  - debug

Ключевые поля:

  • provisioner — какой driver создаёт PV. Это CSI driver name (ebs.csi.aws.com, disk.csi.azure.com, pd.csi.storage.gke.io) или legacy in-tree (kubernetes.io/aws-ebs — deprecated). Для local PV используется sentinel kubernetes.io/no-provisioner.
  • parameters — параметры, специфичные для provisioner-а: тип диска (gp3/io2/sc1), IOPS, encryption keys, filesystem type, и т.д. Provisioner интерпретирует это сам.
  • reclaimPolicy — что делать с PV после удаления PVC (Delete default для dynamic, Retain если нужна страховка).
  • volumeBindingMode — когда происходит binding (см. ниже).
  • allowVolumeExpansion — можно ли увеличивать PVC после создания.
  • mountOptions — флаги mount-а, передаваемые в OS (например, для NFS: nfsvers=4.2, hard, noatime).

Полный flow: PVC → PV → Pod

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: app-data
spec:
  storageClassName: fast-ssd
  accessModes: [ReadWriteOnce]
  resources:
    requests:
      storage: 50Gi

Что происходит дальше:

Dynamic provisioning flow
1. PVC созданkubectl apply -f pvc.yaml — PVC попадает в API server, watch event приходит в PersistentVolumeController.
PersistentVolumeController читает SC
2. Смотрит storageClassNameЕсли у PVC задан storageClassName — ищет SC с этим именем. Если SC не существует, PVC остаётся Pending с ошибкой 'storageclass not found'. Если storageClassName пустой — берётся default SC (если такой есть).
external-provisioner sidecar
3. Provisioner вызывает CreateVolumeexternal-provisioner (часть CSI driver deployment) подписан на watch PVC events. Делает gRPC CreateVolume к CSI driver, передаёт parameters из SC, capacity из PVC. Driver вызывает AWS API: CreateVolume size=50, type=gp3, iops=3000.
provisioner создаёт PV object
4. PV создан и boundProvisioner создаёт PV в API: spec.csi.driver=ebs.csi.aws.com, volumeHandle=vol-abc123, capacity=50Gi, claimRef ссылается на PVC. PVC переходит в Bound, PV в Bound.
Pod создан с этим PVC
5. Pod scheduled и volume attachedScheduler видит Pod, выбирает node. external-attacher вызывает CSI ControllerPublishVolume — AWS attach volume to EC2 instance. kubelet вызывает NodeStageVolume + NodePublishVolume — mount в /var/lib/kubelet/pods/uid/volumes.

Весь процесс автоматический. Разработчик пишет один YAML, инфраструктура делает остальное.


volumeBindingMode: Immediate vs WaitForFirstConsumer

Самый важный параметр в SC для production. Определяет, когда PV провижионится: сразу при PVC create — или когда Pod, использующий PVC, попадает на конкретную node.

Immediate (default)

volumeBindingMode: Immediate

Как только PVC создан, provisioner создаёт PV. Storage backend выбирает AZ (или другую topology) до того, как известно, где будет Pod.

Проблема: в multi-AZ кластере (AWS, GCP) Pod может быть зашедулен в AZ us-east-1b, а EBS volume создан в us-east-1a. Cross-AZ attach невозможен — volume zonal, не может быть attached к instance в другой AZ. Pod останется в ContainerCreating с ошибкой Multi-Attach error.

kubectl describe pod my-app
# ...
# Events:
#   Warning  FailedAttachVolume  ...
#   AttachVolume.Attach failed for volume "pvc-abcd" : "InvalidVolume.ZoneMismatch"

WaitForFirstConsumer

volumeBindingMode: WaitForFirstConsumer

PVC создаётся → остаётся в Pending. Provisioner ждёт, пока Pod (использующий этот PVC) будет scheduled. Когда scheduler выбрал node — provisioner создаёт PV в той же AZ. Затем PVC bound, Pod стартует.

Это обязательно для zonal storage (EBS, GCP zonal PD, Azure zonal disk) в multi-AZ кластерах. И для local PV (где storage привязан к конкретной node).

kubectl get pvc app-data
# NAME       STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS
# app-data   Pending                                       fast-ssd

# (создаём Pod с этим PVC)
kubectl apply -f pod.yaml

# Через несколько секунд:
kubectl get pvc app-data
# NAME       STATUS   VOLUME                                     CAPACITY   ACCESS MODES
# app-data   Bound    pvc-abcd1234-...                           50Gi       RWO
Immediate vs WaitForFirstConsumer
ImmediatePV провижионится сразу. AZ выбирается provisioner-ом (часто случайно или round-robin). Если Pod потом шедулится в другую AZ — Multi-Attach error.
WaitForFirstConsumerPVC ждёт первого Pod-а. Scheduler выбирает node с учётом всех constraints (resources, taints, affinity). После этого provisioner создаёт PV в той же AZ, что и node. Zonal storage всегда matched.
Multi-AZ кластерНа AWS / GCP / Azure кластеры обычно растянуты на 3 AZ для HA. Этот сценарий — норма, и Immediate тут не работает.
Single-AZ кластерЕсли все nodes в одной AZ (например, dev environment), Immediate работает. Но для production обычно multi-AZ.
WARNING

Это killer момент production K8s. Default Storage Class в AWS EBS CSI driver — Immediate (в legacy in-tree provisioner; современный default — WaitForFirstConsumer). Если кластер multi-AZ и SC Immediate — Pods будут падать с Multi-Attach error в продакшене. Всегда выставляйте volumeBindingMode: WaitForFirstConsumer для zonal storage. Local PV его требуют обязательно (PV привязан к node).


Default StorageClass

Когда PVC не указывает storageClassName, K8s ищет default SC:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: standard
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
provisioner: ebs.csi.aws.com
...

Annotation storageclass.kubernetes.io/is-default-class: "true" помечает SC как default. PVC без указания SC попадает на default.

# Посмотреть default SC
kubectl get sc | grep "(default)"
# standard (default)   ebs.csi.aws.com   ...

В кластере должен быть только один default SC — если их два, PVC попадёт на тот, что был создан позже (по metadata.creationTimestamp), что недетерминированно.

PVC с пустым storageClassName

Нюанс: storageClassName: "" (пустая строка) — это явное отключение SC, PVC будет искать только статически provisioned PV. А storageClassName отсутствующее — берётся default.

# Использует default SC
spec:
  accessModes: [RWO]
  resources:
    requests:
      storage: 10Gi
# НЕ использует SC, ищет только static PV без SC
spec:
  storageClassName: ""
  accessModes: [RWO]
  resources:
    requests:
      storage: 10Gi

Это subtle разница, которая ловит людей. Если вы хотите явно использовать static PV — storageClassName: "". Если default — оставьте поле пустым.

Сменить default

# Снять default с текущего
kubectl patch sc standard -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'

# Поставить default на новый
kubectl patch sc fast-ssd -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

allowVolumeExpansion

allowVolumeExpansion: true

С этим параметром можно увеличивать размер PVC после создания. Драйвер поддерживает онлайн или offline expansion (обычно онлайн для современных CSI drivers).

Как это работает:

# Текущий PVC 50Gi
kubectl get pvc app-data
# app-data   Bound   pvc-abcd   50Gi   RWO   fast-ssd

# Увеличиваем до 100Gi
kubectl patch pvc app-data -p '{"spec":{"resources":{"requests":{"storage":"100Gi"}}}}'

# Driver делает expand на backend (AWS ModifyVolume)
# Затем external-resizer запускает filesystem resize (ext4: resize2fs; xfs: xfs_growfs)
# Через минуту:
kubectl get pvc app-data
# app-data   Bound   pvc-abcd   100Gi   RWO   fast-ssd

Условия:

  • SC allowVolumeExpansion: true
  • Driver и filesystem поддерживают expansion (ext4, xfs — да; ntfs — нет)
  • Можно только увеличивать, не уменьшать (shrink на большинстве FS опасен)

Если Pod был запущен с PVC размером 50Gi и его увеличили до 100Gi — для Pod-а изменение видно онлайн, без рестарта (для современных CSI drivers).


Provisioner: CSI driver

provisioner — это имя CSI driver, который зарегистрирован в кластере. Например:

CloudProvisioner
AWS EBSebs.csi.aws.com
AWS EFSefs.csi.aws.com
GCP PDpd.csi.storage.gke.io
Azure Diskdisk.csi.azure.com
Azure Filesfile.csi.azure.com
Local PVkubernetes.io/no-provisioner (без dynamic provisioning)
OpenStack Cindercinder.csi.openstack.org
Ceph RBDrbd.csi.ceph.com
Longhorndriver.longhorn.io
OpenEBS Mayastorio.openebs.csi-mayastor

CSI driver обычно деплоится как Helm chart или operator, и регистрируется как CSIDriver объект в кластере. После этого его provisioner name можно использовать в SC.

Параметры зависят от provisioner

parameters интерпретируется самим provisioner-ом. Каждый driver имеет свой набор:

  • EBS CSI: type (gp3/gp2/io2/sc1), iops, throughput, encrypted, kmsKeyId, fsType
  • GCP PD CSI: type (pd-standard/pd-balanced/pd-ssd/pd-extreme), replication-type (regional-pd)
  • Azure Disk CSI: skuName (Standard_LRS/Premium_LRS), cachingMode, diskEncryptionSetID
  • NFS dynamic provisioner: server, path, mountOptions

Документация driver-а — единственный source of truth. Параметры не валидируются K8s’ом — невалидное значение не падает при создании SC, но падает при создании PVC (provisioner возвращает ошибку, PVC в Pending с events).


Local PV: особый случай

Local PV — это PV, чей storage физически на конкретной node (локальный SSD или ephemeral disk на bare metal). Не имеет dynamic provisioner — провижионится статически, но требует особого SC:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer

kubernetes.io/no-provisioner — sentinel, говорит K8s: “не пытайся провижионить, PV буду создавать руками”. WaitForFirstConsumer обязателен — иначе scheduler не сможет учесть, что PV привязан к конкретной node.

apiVersion: v1
kind: PersistentVolume
metadata:
  name: local-pv-node-1
spec:
  capacity:
    storage: 500Gi
  accessModes: [ReadWriteOnce]
  storageClassName: local-storage
  local:
    path: /mnt/disks/ssd
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - node-1

spec.local.path + nodeAffinity — PV привязан к конкретной node. Pod, использующий PVC привязанный к этому PV, будет шедулиться только на node-1.

Use case: высокопроизводительные local SSD для баз данных в bare metal, или ephemeral local disks на cloud nodes (NVMe instance store).


kubectl: операции с SC

# Список SC
kubectl get sc
# NAME              PROVISIONER         RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION
# fast-ssd          ebs.csi.aws.com     Delete          WaitForFirstConsumer   true
# standard (default) ebs.csi.aws.com    Delete          WaitForFirstConsumer   true

# Подробно
kubectl describe sc fast-ssd

# Создать
kubectl apply -f sc.yaml

# Удалить (SC удалится, существующие PV/PVC остаются — но новые PVC на это SC не привяжутся)
kubectl delete sc fast-ssd

# Дополнить параметрами через edit (изменения применяются только к новым PV)
kubectl edit sc fast-ssd
NOTE

Изменения в SC применяются только к новым PV. Существующие PV не модифицируются — они уже созданы с теми параметрами, что были на момент provisioning. Это значит: если вы изменили parameters.type с gp2 на gp3, старые EBS volumes останутся gp2.


Проверка знанийKnowledge check
На AWS EKS кластере с nodes в трёх AZ (us-east-1a, 1b, 1c) администратор создал SC с provisioner ebs.csi.aws.com и volumeBindingMode: Immediate. Команда деплоит Deployment с PVC. Время от времени Pods падают с FailedAttachVolume и Multi-Attach error. В чём причина и как починить?
ОтветAnswer
Причина — race condition между provisioning storage и scheduling Pod-а. С Immediate binding mode provisioner создаёт EBS volume в AZ, выбранной по своему усмотрению (часто round-robin или random) — например, в us-east-1a. Когда Kubernetes scheduler потом выбирает node для Pod-а, он учитывает resource constraints, taints, affinity — и может зашедулить Pod на node в us-east-1b. EBS volumes — zonal storage: они не могут быть attached к instance в другой AZ. При попытке attach EBS API возвращает InvalidVolume.ZoneMismatch, kubelet рапортует FailedAttachVolume / Multi-Attach error, Pod зависает в ContainerCreating. Решение: изменить volumeBindingMode на WaitForFirstConsumer. С этой настройкой PVC остаётся в Pending до момента, пока scheduler не выберет node для Pod-а — после этого provisioner создаёт EBS в той же AZ, что и node. Это гарантирует matching по топологии. То же самое нужно для GCP zonal PD, Azure zonal disks, и local PV. Дополнительный плюс: если кластер использует Cluster Autoscaler, WaitForFirstConsumer позволяет планировать новые nodes с учётом storage constraints — иначе CA может масштабироваться в неподходящую AZ.

Проверьте понимание

Результат: 0 из 0
Аналитический
Вопрос 1 из 5. EKS кластер с nodes в трёх AZ (us-east-1a/b/c), SC ebs.csi.aws.com с volumeBindingMode: Immediate. Pods Deployment-а время от времени застревают в ContainerCreating с FailedAttachVolume (Multi-Attach). Корень причины?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5