Подключение хранилища к Kubernetes сводится к трём действиям: развернуть CSI-драйвер или провижионер, создать StorageClass с нужными параметрами и запросить том через PersistentVolumeClaim (PVC). Под монтирует полученный том как обычный volume, но данные переживают перезапуск контейнера и перенос пода на другой узел.
Рабочий минимум для динамического выделения: StorageClass с полем provisioner, PVC на 1 Gi и Deployment, который монтирует этот PVC в каталог /data. Схема работает на Kubernetes 1.24 и новее при условии, что установлен совместимый драйвер: в 1.24 встроенные плагины хранилищ убрали из дерева проекта, и внешние СХД подключаются только через Container Storage Interface. Исключение - NFS, hostPath и local, эти плагины остаются в ядре Kubernetes.
Дальше: устройство PV, PVC и StorageClass, сравнение Rook/Ceph, Longhorn и NFS, пошаговые манифесты, разбор типовых ошибок и команды диагностики.
Как Kubernetes работает с хранилищем: ключевые абстракции
Файловая система контейнера исчезает вместе с ним. Каталог emptyDir живёт в пределах пода: данные сохраняются при рестарте контейнера, но пропадают, когда под удаляют или пересоздают. hostPath цепляет каталог узла и привязывает под к конкретной машине, что ломает планирование и переносимость. Для баз данных, очередей и любых stateful-нагрузок нужен слой, который отделяет запрос приложения от физического носителя.
Этот слой состоит из четырёх объектов. PersistentVolume (PV) - участок хранилища в кластере. PersistentVolumeClaim (PVC) - заявка приложения на этот участок. StorageClass описывает, как такой участок создать автоматически. CSI-драйвер выполняет фактические операции с СХД: создание тома, подключение к узлу, монтирование.
Поток данных выглядит так: разработчик создаёт PVC, контроллер StorageClass находит подходящий провижионер, провижионер через CSI-драйвер создаёт том в СХД, Kubernetes формирует объект PV и связывает его с PVC в статусе Bound, kubelet на узле монтирует том в каталог пода. Один PVC соответствует одному тому в хранилище.
PersistentVolume и PersistentVolumeClaim: как это работает
PV существует на уровне кластера и не привязан к namespace. PVC создаётся в namespace приложения. Связывание идёт по трём условиям: достаточный объём, совместимый режим доступа и совпадающий storageClassName.
Режимы доступа описывают, сколько узлов могут одновременно работать с томом:
- ReadWriteOnce (RWO) - том монтируется для чтения и записи только на одном узле. Ограничение действует на узел, а не на под, поэтому несколько подов на одной машине могут делить один RWO-том.
- ReadOnlyMany (ROX) - чтение со многих узлов.
- ReadWriteMany (RWX) - чтение и запись со многих узлов. Требует файлового протокола: CephFS, NFS, SMB.
- ReadWriteOncePod (RWOP) - том доступен ровно одному поду в кластере. Режим стабилен с Kubernetes 1.29, в 1.27 и 1.28 он был бета-функцией.
Блочные СХД (iSCSI, RBD, локальные LVM-диски) отдают RWO и ROX. RWX для них либо не поддерживается, либо реализуется надстройкой вроде share-manager у Longhorn. Политика persistentVolumeReclaimPolicy управляет судьбой тома после удаления PVC: Delete стирает том в СХД, Retain оставляет его вместе с данными.
Пример статического связывания для NFS-сервера:
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-pv-app-data
spec:
capacity:
storage: 50Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: manual
nfs:
server: 10.0.0.20
path: /export/k8s/app-data
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data
namespace: production
spec:
accessModes:
- ReadWriteMany
storageClassName: manual
resources:
requests:
storage: 50Gi
Если подходящего PV нет, PVC останется в статусе Pending, а под зависнет в ContainerCreating. Причину показывает kubectl describe pvc app-data -n production: в секции Events будет указано, чего не хватает. Статическое и динамическое связывание с примерами для локальных дисков и облачных томов разобрано в руководстве по настройке PV и PVC.
StorageClass и динамическое выделение
StorageClass избавляет от ручного создания PV. В поле provisioner указывают имя драйвера, в parameters передают настройки хранилища, а Kubernetes сам вызывает создание тома в момент появления PVC.
- reclaimPolicy: Delete по умолчанию, Retain для критичных данных.
- volumeBindingMode: Immediate создаёт том сразу, WaitForFirstConsumer откладывает создание до планирования пода. Второй вариант нужен для локальных дисков и зон доступности: том появится в той же зоне, где запустится под.
- allowVolumeExpansion: 'true' разрешает увеличивать PVC через kubectl edit pvc без пересоздания пода.
- mountOptions: параметры монтирования, например nfsvers=4.1 или noatime.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: longhorn-fast
provisioner: driver.longhorn.io
allowVolumeExpansion: true
reclaimPolicy: Delete
volumeBindingMode: Immediate
parameters:
numberOfReplicas: '3'
staleReplicaTimeout: '30'
fsType: ext4
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-data
namespace: production
spec:
storageClassName: longhorn-fast
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi
После kubectl apply -f pvc.yaml появляется PVC в статусе Bound и объект PV с именем вида pvc- и идентификатором тома. Один StorageClass можно назначить используемым по умолчанию аннотацией storageclass.kubernetes.io/is-default-class: 'true'. Тогда PVC без поля storageClassName получит том именно от него.
Роль CSI-драйверов
Container Storage Interface - спецификация на базе gRPC, по которой Kubernetes общается с хранилищем. Драйвер поставляет вендор СХД, поэтому один и тот же код Kubernetes работает с NetApp, Ceph, Dell PowerStore, Huawei OceanStor и облачными дисками.
Драйвер выполняет набор операций: CreateVolume при появлении PVC, ControllerPublishVolume для привязки тома к узлу, NodeStageVolume и NodePublishVolume для монтирования, DeleteVolume при удалении PVC, CreateSnapshot для снапшотов. Вызывающая сторона - sidecar-контейнеры из поставки драйвера: external-provisioner, external-attacher, external-resizer, external-snapshotter, node-driver-registrar и livenessprobe.
Имена provisioner у популярных драйверов:
- Ceph RBD: rook-ceph.rbd.csi.ceph.com
- CephFS: rook-ceph.cephfs.csi.ceph.com
- Longhorn: driver.longhorn.io
- NFS CSI: nfs.csi.k8s.io
Установка идёт через Helm или набор манифестов. Проверка после развёртывания: kubectl get csidrivers и kubectl get pods -n kube-system, где должны быть поды вида csi-...-node и csi-...-controller в состоянии Running. Если node-пода драйвера упал на одном узле, поды на этой машине останутся в ContainerCreating. Общие принципы отказоустойчивого кластера, включая HA control plane, RBAC и NetworkPolicy, описаны в руководстве по проектированию production-ready Kubernetes.
Выбор решения для хранения: Rook/Ceph, Longhorn или NFS
Три варианта закрывают большинство задач: Rook/Ceph для продакшена с высокими требованиями, Longhorn для небольших кластеров и быстрого старта, NFS для уже существующего NAS или файлового сервера. Критерии шире, чем работает или нет: важны число узлов, наличие свободных дисков, поддержка RWX и допустимая сложность эксплуатации. Подробное сравнение по отказоустойчивости, стоимости и сложности с рекомендациями для PostgreSQL, MySQL, Kafka и Elasticsearch собрано в руководстве по выбору хранилища для Kubernetes.
Rook/Ceph: возможности и требования
Rook - оператор, который разворачивает Ceph внутри Kubernetes и управляет его жизненным циклом. Ceph отдаёт блочные тома (RADOS Block Device), файловую систему (CephFS) и S3-совместимое объектное хранилище (RGW).
Требования на практике: минимум три worker-узла, на каждом хотя бы один свободный диск без файловой системы и разделов; SSD под журналы OSD заметно поднимают производительность; оперативная память от 4 ГБ на узел только под демоны Ceph плюс запас под приложения. Команды после добавления официального чарт-репозитория Rook под именем rook-release:
helm install rook-ceph rook-release/rook-ceph --namespace rook-ceph --create-namespace helm install rook-ceph-cluster rook-release/rook-ceph-cluster --namespace rook-ceph --set operatorNamespace=rook-ceph
Репликация задаётся в пуле CRUSH: size=3, min_size=2. При отказе одного узла запись продолжается, при отказе двух одновременно кластер переходит в режим read-only. StorageClass для блочных томов создаётся с provisioner rook-ceph.rbd.csi.ceph.com, для RWX - с rook-ceph.cephfs.csi.ceph.com. Эксплуатация требует мониторинга состояния OSD, ёмкости пулов и ребалансировки после замены дисков. Развёртывание Rook/Ceph с манифестами и настройкой пулов показано в практическом руководстве по Rook/Ceph.
Longhorn: простое распределённое хранилище
Longhorn - проект CNCF, который хранит данные на локальных дисках узлов и синхронизирует реплики между ними. Установка сводится к одному Helm-чарту или манифесту, требования скромнее, чем у Ceph.
helm install longhorn longhorn/longhorn --namespace longhorn-system --create-namespace
На каждом узле нужен пакет open-iscsi и запущенный демон iscsid, иначе том не смонтируется. Для репликации требуется минимум три узла, на одном Longhorn тоже запустится, но без запаса по отказоустойчивости. Число копий задаётся параметром numberOfReplicas: '3'. Параметр dataLocality: 'strict-local' привязывает реплику к узлу с подом и ускоряет чтение. RWX реализуется через share-manager: Longhorn поднимает NFS-сервер внутри кластера и отдаёт том по NFSv4. Снапшоты, бэкапы в S3 или NFS и расписания входят в комплект, поэтому Longhorn часто берут для кластеров на 3-10 узлов и edge-инсталляций.
NFS как внешнее хранилище для Kubernetes
NFS подключает к кластеру уже работающий файловый сервер: NAS, TrueNAS, отдельную виртуальную машину. Для динамического выделения ставят nfs-subdir-external-provisioner, который создаёт на сервере подкаталог под каждый PVC.
helm install nfs-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \ --namespace nfs-provisioner --create-namespace \ --set nfs.server=10.0.0.20 \ --set nfs.path=/export/k8s/data \ --set storageClass.name=nfs-client \ --set storageClass.reclaimPolicy=Retain
Плюсы: RWX из коробки, нулевая нагрузка на узлы кластера, привычные инструменты администрирования файлового сервера. Минусы: единая точка отказа на стороне сервера, репликации средствами Kubernetes нет, задержки зависят от сети и загрузки NAS, а случайные операции ввода-вывода обычно медленнее, чем на локальных SSD. NFS подходит для тестовых стендов, CI/CD, файловых архивов и приложений с невысокими требованиями к диску. Если своего сервера нет, его поднимают на виртуальной машине: облачный провайдер вроде Timeweb Cloud даёт VPS с сетевыми дисками, которые экспортируются по NFS. Практические детали подключения, права доступа и параметры монтирования разобраны в руководстве по настройке NFS для контейнеров.
| Критерий | Rook/Ceph | Longhorn | NFS |
|---|---|---|---|
| Сложность установки | Оператор, 3 и более узла с дисками | Helm-чарт, open-iscsi на узлах | Сервер NFS плюс провижионер |
| Минимум узлов | 3 с неразмеченными дисками | 3 для репликации, 1 без отказоустойчивости | Требований к узлам нет |
| Репликация | 3 копии в пуле: size=3, min_size=2 | numberOfReplicas 2-3 | На стороне сервера, вне Kubernetes |
| ReadWriteMany | CephFS | share-manager по NFSv4 | Из коробки |
| Снапшоты и бэкапы | RBD-снапшоты, экспорт в S3 | Встроенные снапшоты, бэкап в S3 или NFS | rsync, tar, снапшоты NAS |
| Типовой сценарий | Продакшен, базы данных, S3 | Кластеры 3-10 узлов, edge | Тесты, CI/CD, архивы |
Рекомендации по выбору: для продакшена с базами данных и жёсткими требованиями к отказоустойчивости берите Ceph или Longhorn в зависимости от размера команды и числа узлов; для быстрого старта на трёх узлах Longhorn даёт рабочее решение за вечер; при минимальных затратах и наличии NAS достаточно NFS.
Пошаговая настройка динамического выделения хранилища
Порядок одинаков для любого бэкенда: драйвер, StorageClass, PVC, под. Различаются команды установки и параметры.
Установка CSI-драйвера или провижионера
Добавьте официальный Helm-репозиторий выбранного решения, затем установите чарт. Для Longhorn и Rook команды приведены выше, для NFS-провижионера - в разделе про NFS. Проверка после установки:
kubectl get pods -n longhorn-system kubectl get pods -n nfs-provisioner kubectl get pods -n rook-ceph kubectl get csidrivers
Все поды драйвера должны быть в состоянии Running, а в выводе csidrivers появиться строка с именем вашего драйвера. Если node-пода висит в CrashLoopBackOff, проверьте наличие open-iscsi и доступность каталога /var/lib/kubelet на узле.
Создание StorageClass и PVC
Для NFS StorageClass описывается так:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-client
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner
parameters:
archiveOnDelete: 'false'
onDelete: delete
reclaimPolicy: Retain
volumeBindingMode: Immediate
mountOptions:
- nfsvers=4.1
- noatime
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: web-uploads
namespace: production
spec:
storageClassName: nfs-client
accessModes:
- ReadWriteMany
resources:
requests:
storage: 5Gi
Примените оба объекта одной командой kubectl apply -f storage.yaml и проверьте результат: kubectl get pvc -n production. Ожидаемый статус - Bound. Параметр archiveOnDelete: 'false' удаляет каталог на NFS-сервере вместе с PVC, значение 'true' переименовывает его и оставляет данные.
Использование PVC в поде
Deployment монтирует готовый PVC, StatefulSet создаёт PVC для каждой реплики автоматически через volumeClaimTemplates.
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: production
spec:
replicas: 1
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
securityContext:
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
containers:
- name: web
image: nginx:1.27
volumeMounts:
- name: uploads
mountPath: /data
volumes:
- name: uploads
persistentVolumeClaim:
claimName: web-uploads
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: kafka
spec:
serviceName: kafka
replicas: 3
selector:
matchLabels:
app: kafka
template:
metadata:
labels:
app: kafka
spec:
containers:
- name: kafka
image: bitnami/kafka:3.7
volumeMounts:
- name: data
mountPath: /bitnami/kafka
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn-fast
resources:
requests:
storage: 200Gi
У StatefulSet появятся PVC с именами data-kafka-0, data-kafka-1 и data-kafka-2. Проверка монтирования внутри пода: kubectl exec -it web-7d9f -n production -- df -h /data.
Типичные ошибки и подводные камни при интеграции хранилища
Проблемы с режимами доступа и монтированием
ReadWriteOnce относится к узлу. Если два пода с одним RWO-PVC планируются на разные машины, второй зависнет в ContainerCreating, а в событиях появится сообщение Multi-Attach error: volume is already exclusively attached to one node. Решения: перевести поды на один узел через affinity, раздать каждому поду свой PVC или перейти на RWX-хранилище с файловым протоколом.
Попытка подключить RWX к блочному бэкенду без поддержки файлового слоя даёт ошибку провижионера вида unsupported access mode. Блочные драйверы принимают только RWO и ROX, а RWX доступен в CephFS, NFS и Longhorn через share-manager.
Ошибки при динамическом выделении
PVC в статусе Pending почти всегда связан с одной из причин:
- В StorageClass указан несуществующий provisioner или другое имя драйвера. Проверьте вывод kubectl get csidrivers и поле provisioner.
- Нет StorageClass по умолчанию, а в PVC не задан storageClassName. Смотрите kubectl get storageclass и аннотацию is-default-class.
- Провижионер не может достучаться до хранилища: закрыты порты, неверный адрес NFS-сервера, истёкший ключ доступа к облаку. Логи лежат в подах драйвера, например kubectl logs -n rook-ceph deploy/csi-rbdplugin-provisioner -c csi-provisioner.
- Том не помещается в пул или сработала квота. В событиях PVC будет сообщение об исчерпании ёмкости.
- Топология: при volumeBindingMode: WaitForFirstConsumer под не планируется из-за нехватки ресурсов, а значит и том не создаётся.
Отдельная группа проблем связана с версиями. Манифесты StorageClass и CSIStorageCapacity используют apiVersion storage.k8s.io/v1, который стабилен с Kubernetes 1.6, а CSI-драйверы выпускаются под конкретные минорные версии кластера. Перед обновлением Kubernetes сверяйтесь с матрицей совместимости вендора драйвера: после обновления control plane старые CSI-поды могут не запуститься, и новые тома перестанут создаваться.
Права доступа и безопасность
Блочный том монтируется с правами root, и непривилегированный процесс в контейнере не сможет писать в каталог. Решение - fsGroup в securityContext пода: Kubernetes выставит группу на файлы тома. Значение fsGroupChangePolicy: OnRootMismatch ускоряет старт, меняя права только при несовпадении группы.
Для NFS права зависят от настроек экспорта на сервере. По умолчанию действует root_squash: запросы от root внутри пода отображаются на анонимного пользователя, и запись падает с Permission denied. Варианты решения: подобрать uid и gid приложения под владельца каталога, выставить fsGroup, при необходимости добавить no_root_squash для конкретного клиента. Широкий no_root_squash на весь экспорт открывает доступ к файлам сервера, поэтому применяйте его точечно.
Проверка и диагностика хранилища в Kubernetes
Команды kubectl для проверки статуса
kubectl get pvc -A kubectl get pv kubectl describe pvc postgres-data -n production kubectl get storageclass kubectl get csidrivers kubectl get pods -n longhorn-system kubectl logs -n rook-ceph deploy/csi-rbdplugin-provisioner -c csi-provisioner kubectl get volumesnapshot -A
Статус Bound у PVC означает, что том создан и привязан. Статус Released у PV появляется после удаления PVC при политике Retain, такой том нельзя переиспользовать без ручной очистки поля claimRef. У StatefulSet проверьте, что каждый под получил собственный PVC с ожидаемым именем.
Тестирование записи и чтения
Проверка сохранности данных занимает две минуты. Создайте тестовый под с монтированием PVC:
apiVersion: v1
kind: Pod
metadata:
name: storage-test
namespace: production
spec:
containers:
- name: shell
image: busybox:1.36
command: ['sh', '-c', 'sleep 3600']
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: web-uploads
Запишите файл, удалите под, создайте заново и прочитайте данные:
kubectl exec -it storage-test -n production -- sh -c 'echo hello > /data/check.txt' kubectl delete pod storage-test -n production kubectl apply -f storage-test.yaml kubectl exec -it storage-test -n production -- cat /data/check.txt kubectl exec -it storage-test -n production -- mount | grep /data
Если файл на месте, том переживает пересоздание пода. Дополнительно проверьте восстановление из снапшота: создайте PVC из VolumeSnapshot и убедитесь, что данные внутри. Такой тест ловит ошибки в настройках бэкапов до того, как они понадобятся в аварии.
Обеспечение отказоустойчивости и резервного копирования
Репликация данных в Rook/Ceph и Longhorn
В Ceph число копий задаётся в пуле: size=3 и min_size=2 означают, что запись подтверждается после двух реплик и переживает отказ одного узла. Раскладку реплик по узлам и стойкам определяет CRUSH-карта, для стойко-устойчивой схемы в неё добавляют правило failure domain по стойке.
В Longhorn число копий задаётся параметром numberOfReplicas в StorageClass, значение 3 распределяет реплики по разным узлам. Уменьшение до 1 экономит место и допустимо только для невосстановимых данных. Проверить состояние реплик можно через kubectl get replicas.longhorn.io -n longhorn-system: статус running и healthy означает синхронизацию.
Снапшоты и бэкапы
Стандартный механизм снапшотов в CSI - ресурсы VolumeSnapshotClass и VolumeSnapshot. Для них нужны CRD snapshot.storage.k8s.io и snapshot-controller в кластере; после установки создаётся снапшот без остановки приложения:
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: postgres-snap-01
namespace: production
spec:
volumeSnapshotClassName: csi-snapclass
source:
persistentVolumeClaimName: postgres-data
Longhorn умеет расписания через recurring jobs: снапшот каждый час и бэкап в S3 или NFS раз в сутки. В Ceph блочные тома копируют через rbd snap и rbd export либо через CSI-снапшоты с последующей загрузкой в объектное хранилище. Для NFS резервное копирование делают на стороне сервера: снапшоты ZFS или rsync в другой каталог. Комплексный бэкап ресурсов Kubernetes и томов закрывает Velero, команда создания выглядит так:
velero backup create prod-daily --include-namespaces production --snapshot-volumes velero backup describe prod-daily --details
Правило, которое стоит соблюдать: пока восстановление не проверено на стенде, бэкапа нет. Раз в квартал разворачивайте PVC из снапшота и сверяйте контрольные суммы данных.