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 используется sentinelkubernetes.io/no-provisioner.parameters— параметры, специфичные для provisioner-а: тип диска (gp3/io2/sc1), IOPS, encryption keys, filesystem type, и т.д. Provisioner интерпретирует это сам.reclaimPolicy— что делать с PV после удаления PVC (Deletedefault для 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
Что происходит дальше:
Весь процесс автоматический. Разработчик пишет один 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
Это 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, который зарегистрирован в кластере. Например:
| Cloud | Provisioner |
|---|---|
| AWS EBS | ebs.csi.aws.com |
| AWS EFS | efs.csi.aws.com |
| GCP PD | pd.csi.storage.gke.io |
| Azure Disk | disk.csi.azure.com |
| Azure Files | file.csi.azure.com |
| Local PV | kubernetes.io/no-provisioner (без dynamic provisioning) |
| OpenStack Cinder | cinder.csi.openstack.org |
| Ceph RBD | rbd.csi.ceph.com |
| Longhorn | driver.longhorn.io |
| OpenEBS Mayastor | io.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
Изменения в SC применяются только к новым PV. Существующие PV не модифицируются — они уже созданы с теми параметрами, что были на момент provisioning. Это значит: если вы изменили parameters.type с gp2 на gp3, старые EBS volumes останутся gp2.