Управление данными в Kubernetes часто кажется сложным. Если грамотно не подключить внешнее хранилище, то при любом перезапуске пода все накопленные файлы бесследно исчезнут. Этот материал — практическое руководство для тех, кто только осваивает администрирование K8s и хочет разобраться в логике работы дисков без погружения в запутанную документацию.

На простых примерах с реальной инфраструктурой изучим всю архитектуру выделения ресурсов. Развернем хранилище в тестовом кластере, смонтируем диск и заглянем внутрь файловой системы ноды.

Привет! Я Катя, младший системный администратор в Selectel. Надеюсь, моя работа поможет выстроить в голове четкую картину и сэкономит часы на дебаге, если нужный диск однажды откажется подключаться.

Введение

Хранилища предназначены для долговременного хранения данных. На локальных компьютерах эту задачу выполняют физические диски. В зависимости от операционной системы, это могут быть логические тома C:/, D:/ или устройства /dev/sda, /dev/sdb. На них и размещаются различные файлы и документы.

Аналогично и приложения, работающие в подах кластера Kubernetes, требуют выделенного дискового пространства. Однако в отличие от домашнего ПК, поды эфемерны: они могут перезапускаться или удаляться. Если сохранять информацию только внутри контейнера, при его остановке данные будут потеряны. 

Именно поэтому в Kubernetes предусмотрены специальные механизмы, позволяющие сохранять данные независимо от жизненного цикла пода. Этот принцип схож с камерой хранения в отеле: вещи остаются в сохранности независимо от присутствия их владельца в номере.

Общая архитектура хранения

Приложение ничего не знает о конкретной реализации хранилища и его устройстве. Kubernetes также не управляет дисками напрямую. Чтобы максимально просто и прозрачно предоставить поду место для размещения данных, была придумана следующая схема взаимодействия.

  1. Приложение в поде понимает, что ему нужно куда-то сохранять данные.

  2. Что делает приложение? Оно создает заявку — Persistent Volume Claim (PVC). В ней честно пишет: «Требуется столько-то гигабайт, нужны права на чтение и запись, и хранилище нужно вот такого типа».

  3. Дальше в игру вступает главный диспетчер — Kubernetes. Он изучает заявку и понимает: готового подходящего диска, или как он называется в Kubernetes, Persistent Volume (PV) — нет. Тогда он зовет переводчика — CSI-драйвер (Container Storage Interface). Заявка же (PVC) временно получает статус Pending — как заказ, который еще не подтвержден.

  4. Сам Kubernetes — не волшебник. Он не знает, как общаться с каждым облачным провайдером или файловой системой в отдельности. Их же сотни! Поэтому у него есть специальный посредник — CSI-драйвер. Этот помощник знает, как разговаривать с конкретным хранилищем на его родном языке — прямо как синхронный переводчик на конференции.

  5. CSI-драйвер берет заявку и создает новый диск прямо в облачном хранилище. Облако отвечает: «Диск готов!». Теперь Kubernetes создает для этого диска PV и наконец-то привязывает его к нашей заявке PVC. Статус Pending меняется на Bound. Все, сделка состоялась!

  6. Финал: приложение получает доступ к своему личному, пусть и условному, диску. Теперь оно может размещать на нем базы данных, логи, пользовательские файлы и вообще что угодно. И главное — даже если под перезапустится или его удалят, данные никуда не исчезнут, а останутся лежать спокойно в хранилище в ожидании нового пода.

Подытожим. 

Когда описывается под и запрашивается для него диск, то создается ресурс PersistentVolumeClaim (дословно, «заявка на постоянное хранилище», далее PVC) — запрос, но еще не само хранилище. Позже Kubernetes выделит PersistentVolume (дословно, «постоянное хранилище», далее PV) — уже ссылающийся на диск конкретный ресурс, которым и оперирует под. 

Чтобы диск был создан в инфраструктуре облачного провайдера, Kubernetes обращается к CSI-драйверу, который, в свою очередь, и делает всю работу, исходя из параметров в PVC.

Общая цепочка выглядит так: Под → PVC (PersistentVolumeClaim) → CSI (Container Storage Interface) → физический диск → PV (PersistentVolume)

Сам K8s никогда не управляет дисками напрямую — вместо этого он контролирует цепочку абстракций, которые и связывают под с реальным хранилищем. 

Теперь, уяснив принципы взаимодействия пода с диском, и начнем наше знакомство с работой хранилища в K8s.

Источником диска всегда выступает внешнее хранилище: ресурсы облачных провайдеров или мощные локальные серверы с решениями Ceph и NFS.

CSI и StorageClass — создание и управление дисками

CSI (Container Storage Interface) — это драйвер, который непосредственно работает с дисками. Именно ему Kubernetes делегирует задачи по созданию, удалению и подключению томов к узлам кластера.

Технически компоненты CSI представляют собой обычные поды. Проверить их наличие можно командой:

kubectl -n kube-system get pods | grep csi

Обычно разворачивается два типа подов:

  • controller — отвечает за создание тома (volume),

  • node — монтирует созданный том на конкретную ноду для использования подом.

В нашем тестовом кластере с одной worker-нодой мы видим два запущенных пода — controller и node-плагин:

kube-system   csi-cinder-controllerplugin-78858d84f9-pdj2r          6/6     Running   0          59m

kube-system   csi-cinder-nodeplugin-nbvhk                           3/3     Running   0          59m

StorageClass — это профиль конфигурации, который указывает кластеру, какой тип накопителя нужно выделить и с помощью какого CSI-драйвера. Он работает как шаблон, где заранее зафиксированы технические параметры будущего диска, его производительность и правила удаления данных.

Пример манифеста StorageClass:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast.ru-3b
provisioner: cinder.csi.openstack.org
reclaimPolicy: Delete
volumeBindingMode: Immediate
parameters:
  type: fast.ru-3b

В примере выше используется CSI‑драйвер Cinder от платформы Openstack. 

Посмотреть, какие есть в кластере классы хранилищ и их основные характеристики можно с помощью команды:

kubectl get storageclass

Пример вывода:

NAME         PROVISIONER                RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
fast.ru-3b   cinder.csi.openstack.org   Delete          Immediate           true                   14m

Можно также увидеть и подробную информацию о конкретном классе:

kubectl describe storageclass fast.ru-3b

Команда выше незаменима, когда нужно разобраться в детальных настройках профиля или найти причину ошибки при создании диска.

Managed Kubernetes на выделенных серверах

Снизьте расходы на ИТ‑инфраструктуру и улучшите производительность микросервисов.

Подробнее →

Как связаны между собой PV и PVC

Связь между заявкой (PVC) и выделенным томом (PV) устанавливается через процесс привязки (binding). Kubernetes не выдает первый попавшийся диск: контроллер проводит строгую проверку совместимости объектов. Чтобы связка состоялась, должны совпасть три ключевых условия:

  • идентичный класс хранилища — StorageClass;

  • совпадающие режимы доступа — Access Modes;

  • достаточный объем — емкость выделяемого PV должна быть больше или равна размеру, запрошенному в PVC.

Если подходящего тома нет, контроллер инициирует его динамическое создание или переводит заявку в режим ожидания.

Требования к будущему хранилищу фиксируются в спецификации. Пример минимального манифеста PVC, который будет использоваться в практической части:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: busybox-pvc
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: fast.ru-3b
  resources:
    requests:
      storage: 5Gi

Разберем поля манифеста:

  • apiVersion — используемая версия Kubernetes API;

  • kind — тип создаваемого объекта;

  • metadata.name — уникальное имя заявки (busybox-pvc);

  • spec.accessModes — конфигурация прав доступа к диску;

  • spec.resources.requests.storage — запрашиваемый объем дискового пространства (5Gi).

После применения манифеста Kubernetes либо находит подходящий PersistentVolume, либо создает новый с помощью StorageClass и CSI‑драйвера. Далее происходит сама привязка (binding) — когда для PVC находится подходящий PV.

В текущем пространстве имен статус объектов проверяется командами:

kubectl get pvc
kubectl get pv

Флаг -A (или его полная версия --all-namespaces) указывает Kubernetes, что нужно показать ресурсы во всех пространствах имен кластера — например:

kubectl get pvc -A

Пример вывода:

NAMESPACE   NAME          STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
default     busybox-pvc   Bound    pvc-528f4464-5253-4796-8908-6942af89a125   5Gi        RWO            fast.ru-3b     <unset>                 64m

Если все в порядке, мы должны увидеть, что у PVC статус Bound, а у PV появилась ссылка на PVC. Однако у PVC еще может быть статус Pending. В чем отличие?

Статус Bound показывает, что успешно произошла связка PVC ↔ PV, все работает. Статус же Pending значит, нет свободного PV, не хватает емкости или возникла ошибка при создании диска на стороне провайдера.

В выводе видим  Access Modes и Reclaim Policy — это важные настройки, поэтому их рассмотрим подробнее.

Есть три вида прав доступа к PV.

  • ReadWriteOnce (RWO) — чтение и запись. Том монтируется только к одной физической ноде. Несколько подов могут делить этот диск, только если они запущены на одном узле.

  • ReadWriteMany (RWX) — чтение и запись с возможностью монтирования к нескольким нодам. Разные поды могут работать с одним ресурсом одновременно, даже если находятся на разных физических нодах кластера.

  • ReadOnlyMany (ROX) — режим «только чтение», доступный для множества нод.

Политика ReclaimPolicy определяет поведение кластера с данными после удаления PVC. Есть три варианта:

  • Delete — диск удаляется вместе со всеми данными, PV также удаляется из кластера;

  • Retain — физический том и данные сохраняются для дальнейшего ручного администрирования;

  • Recycle — данные с диска удаляются, но PV становится доступным для дальнейшего использования в кластере.

Еще важно отметить, что есть два режима выделения томов в кластере: статический (Static Provisioning) и динамический (Dynamic Provisioning). На практике почти всегда используется динамический (через StorageClass), но важно понимать разницу:

  • статическое выделение — подразумевает ручное создание PV администратором с последующим формированием ссылающегося на него PVC;

  • динамическое — автоматизирует процесс: создается только PVC, а Kubernetes генерирует хранилище самостоятельно.

В практических примерах мы как раз воспользуемся динамическим созданием хранилища в кластере.

Практика

Закрепим знания на простых примерах.

Создадим базовый кластер в Managed Kubernetes с одной воркер-нодой.

Подробнее о том как, начать работать с Managed Kubernetes — в нашей документации.

Первое, что стоит сделать в любом кластере — посмотреть существующие тома:

kubectl get pvc -A
kubectl get pv -A

Если кластер пуст, команды вернут:

No resources found

Вывод ожидаемый, так как в кластере нет клиентских подов. Создадим тестовый под на базе легковесного образа busybox и заявку на хранилище — PVC.

kubectl apply -f - <<EOF
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: busybox-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
---
apiVersion: v1
kind: Pod
metadata:
  name: busybox
spec:
  containers:
    - name: busybox
      image: busybox:latest
      command: ["/bin/sh", "-c"]
      args:
        - |
          while true; do
            date >> /data/log.txt
            sleep 10
          done
      volumeMounts:
        - name: storage
          mountPath: /data
  volumes:
    - name: storage
      persistentVolumeClaim:
        claimName: busybox-pvc
EOF

YAML‑файл для формирования PVC мы уже разобрали выше, теперь посмотрим, как создавать под.

Начинаем с указания базовой информации:

  • apiVersion — версия Kubernetes API;

  • kind — тип объекта;

  • metadata.name — служебная информация об объекте, указываем Pod;

Далее описываем список контейнеров внутри пода. В нашем случае контейнер только один:

  • spec.containers.name — имя контейнера;

  • image — образ контейнера;

  • command — команда запуска контейнера;

  • args — аргументы для команды запускают цикл, который каждые 10 секунд записывает текущую дату в файл /data/log.txt;

  • volumeMounts — монтирует том хранилища storage внутрь контейнера по пути /data;

  • volumes — задает список томов для пода: name — имя тома, persistentVolumeClaim — указывает источником тома заявку busybox-pvc.

Проверим, что заявка PVC благополучно сформирована:

kubectl get pvc -A

Пример вывода:

NAMESPACE   NAME          STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
default     busybox-pvc   Bound    pvc-528f4464-5253-4796-8908-6942af89a125   5Gi        RWO            fast.ru-3b     <unset>                 64m

Видим не только параметры самой заявки, но и уже созданный том (volume) для пода, что видно по статусу Bound у заявки. Процесс происходит обычно быстро, вплоть до нескольких секунд.

Статус Bound также значит, что у PVC появилась связка с PV. Посмотрим ресурсы в кластере:

kubectl get pv -A

Вывод:

NAME   CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                 STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
pvc-528f4464-5253-4796-8908-6942af89a125   5Gi        RWO            Delete           Bound    default/busybox-pvc   fast.ru-3b     <unset>                          64m

Действительно есть PV размером 5 ГБ — как и было указано в PVC. 

Детальную конфигурацию, которая не отображается в выводе kubectl get pvc, а также возможные причины ошибок, при их наличии, можно изучить с помощью команды:

kubectl describe pvc busybox-pvc

Здесь можно обратить внимание на статус (Bound или Pending), к какому PV он привязан, какой StorageClass используется.

Монтирование и устройство на ноде

Посмотрим, как volume попадает в под и что происходит при его старте.

Когда под запускается с подключенным томом, включается kubelet — агент, управляющий контейнерами на ноде. Он выполняет следующие шаги:

  • считывает спецификацию пода и информацию о требуемом томе;

  • с помощью драйвера CSI подключает диск к физическому серверу (ноде);

  • монтирует хранилище в файловую систему ноды;

  • прокидывает директорию внутрь изолированного контейнера.

Для приложения внутри контейнера том выглядит как обычный каталог с файлами. Все тома Kubernetes монтирует в одну директорию на ноде — /var/lib/kubelet/pods/. Путь к хранилищу конкретного пода в любой момент времени можно посмотреть по следующему пути: /var/lib/kubelet/pods/<pod-uuid>/volumes/.

Визуально выглядит это так:

Процесс монтирования тома в контейнер.
Процесс монтирования тома в контейнер.

Практика

Чтобы узнать уникальный идентификатор пода (UUID), выполняется команда:

kubectl get pod <имя-пода> -o jsonpath='{.metadata.uid}'

После подключения к ноде можно перейти в директорию монтирования:

/var/lib/kubelet/pods/<pod-uuid>/volumes/

Вывод:

/var/lib/kubelet/pods/a958e390-1f3c-4dea-89fd-d407ab644a63/volumes/kubernetes.io~csi/pvc-528f4464-5253-4796-8908-6942af89a125/mount# ls -l
total 36
-rw-r--r-- 1 root root 18821 May 14 11:30 log.txt
drwx------ 2 root root 16384 May 14 09:42 lost+found

Команда ls -l показывает сгенерированный тестовым контейнером файл log.txt, который мы указывали в deployment при развертывании busybox.

У вас будут другие UUID, так как у каждого пода свой уникальный идентификатор.

Посмотрим еще раз на полезные команды.

Работа с PVC:

kubectl get pvc
kubectl describe pvc

Получение информации о PV:

kubectl get pv

Отладка подов:

kubectl describe pod

Просмотр логов драйвера CSI:

kubectl -n kube-system get pods | grep csi
kubectl logs <csi-pod>

Заключение

Система хранения в Kubernetes выглядит структурированно, если рассматривать ее как поэтапный процесс:

  • администратор формирует PVC,

  • Kubernetes через StorageClass вызывает CSI,

  • CSI создает реальный диск в инфраструктуре,

  • появляется PV,

  • PVC и PV связываются,

  • Под начинает использовать volume.

Хранилища в Kubernetes кажутся сложными ровно до тех пор, пока не раскладываются на цепочку: Под → PVC → CSI → внешнее хранилище → PV. Такой подход объясняет происхождение дисков, алгоритмы определения их размера и зоны ответственности каждого компонента.

Понимание границы между абстракциями кластера и реальным накопителем значительно облегчает диагностику проблем с выделением ресурсов.

Главный вывод: Kubernetes не занимается физическим хранением данных, а лишь управляет надежными и безопасными маршрутами доступа к ним.