Привет, друзья! Если вы работаете с Kubernetes, то наверняка сталкивались с задачей организации постоянного хранилища для stateful-приложений. В ранних версиях Kubernetes это вызывало большие сложности: встраиваемые (in-tree) volume-плагины нужно было изменять в ядре кластера, из-за чего обновления замедлялись, а интеграция с внешними хранилищами усложнялась. На помощь пришел Container Storage Interface (CSI) — стандартный API, который позволяет провайдерам хранения писать драйверы вне дерева (out-of-tree), не внося изменения в ядро Kubernetes.

В этой статье разберём принципы CSI, посмотрим на его архитектуру и на то, как работают драйверы. Для примера возьмем типичный сценарий с NFS-драйвером, опираясь на официальную спецификацию CSI. Итак, пойдём по порядку.

Содержание:

Что такое CSI и зачем он нужен

CSI — это спецификация API, разработанная под эгидой CNCF (Cloud Native Computing Foundation), которая определяет, как контейнеризированные приложения взаимодействуют с блоковыми и файловыми хранилищами. Основная идея — отделить хранение от оркестратора, а заодно чтобы разработчики хранения не зависели от обновлений Kubernetes.

До появления CSI все плагины были «вшиты» в kubelet, что приводило к проблемам:

  • Релизы задерживались: новые фичи хранения ждали выхода очередной версии.

  • Поддержка усложнялась: каждый вендор модифицировал core.

CSI вышел в General Availability в Kubernetes 1.13 (2019 год) и теперь является единственным способом добавлять новые типы томов. Он использует gRPC для коммуникации с ядром Kubernetes, поддерживает идемпотентные вызовы (повторный вызов не меняет состояние) и фокусируется на безопасности: драйверам не нужен root-доступ к хосту.

Принципы работы CSI-драйверов

CSI строится на нескольких ключевых принципах, которые делают его гибким и масштабируемым:

  • Стандартизация интерфейса. Все драйверы реализуют один protobuf-контракт (csi.proto). Protobuf (Protocol Buffers) — это языконезависимый формат сериализации данных от Google, который описывает структуру сообщений и методы RPC в виде .proto-файла. В CSI файл csi.proto описывает все возможные запросы и ответы (CreateVolume, NodePublishVolume и т.д.) с их полями и типами. Из этого файла генерируется код для разных языков (Go, Python, Java), что гарантирует совместимость между Kubernetes и любым драйвером. CSI предусматривает три сервиса: 

    • Identity — обязательный для всех драйверов, служит точкой входа для обнаружения возможностей. 

    • Controller — опционален, отвечает за глобальные операции с томами. 

    • Node — обязателен для драйверов с Node Plugin, управляет локальными операциями на нодах. Это позволяет Kubernetes взаимодействовать с любым хранилищем через единый API, без хардкода.

  • Разделение ответственности. Драйвер делится на Controller (управление ресурсами) и Node (локальные операции). Это минимизирует привилегии: Controller не трогает данные, Node только монтирует.

  • Sidecar-контейнеры для интеграции. Kubernetes не вызывает RPC напрямую. Вместо этого используются вспомогательные контейнеры (external-provisioner, external-attacher и т.д.), которые «слушают» API-сервер и делегируют вызовы драйверу.

  • Идемпотентность и безопасность. RPC можно вызывать повторно без вреда. Драйверы работают в изолированных подах, без доступа к «внутрянке» kubelet.

  • Поддержка полного жизненного цикла тома. От создания до удаления — весь цикл покрыт RPC-вызовами, включая управление снимками и расширение томов.

Благодаря этим принципам можно быстро добавлять поддержку новых хранилищ: написали драйвер, задеплоили его как Deployment/DaemonSet — и всё готово.

Архитектура CSI в Kubernetes

Архитектура CSI проста, но элегантна. Вот её основные компоненты:

  • Controller Plugin. Деплоится как Deployment и отвечает за глобальные операции: создание/удаление томов, подключение/отключение к нодам, управление снимками. Например, в AWS это вызывает EC2 API для создания EBS.

  • Sidecar-контейнеры контроллера:

    • external-provisioner. Слушает PVC, вызывает CreateVolume/DeleteVolume. Также использует ValidateVolumeCapabilities для проверки совместимости.

    • external-attacher. Подключает и отключает тома через ControllerPublishVolume/ControllerUnpublishVolume.

    • external-resizer. Расширяет тома с помощью ControllerExpandVolume (если драйвер поддерживает capability EXPAND_VOLUME).

    • external-snapshotter. Управляет снимками через CreateSnapshot, DeleteSnapshot и ListSnapshots (если драйвер это поддерживает).

    • livenessprobe. Мониторинг здоровья через Probe RPC из Identity Service.

  • Node Plugin. DaemonSet на каждом рабочем узле. Монтирует/размонтирует тома локально. Регистрируется в kubelet через node-driver-registrar. Важно: плагин работает на каждой ноде и имеет привилегированный доступ к файловой системе (на Linux требуется CAP_SYS_ADMIN). Это необходимо, чтобы выполнять монтирование и работать с блочными устройствами.

  • Sidecar-контейнеры для плагина на рабочих узлах:

    • node-driver-registrar. Регистрирует Node Plugin в kubelet. Использует NodeGetInfo, чтобы получить информацию о ноде.

Все компоненты общаются по gRPC через Unix-сокеты. При этом Kubernetes ничего не знает о самом драйвере: sidecar-контейнеры — это как «телефон», через который ядро Kubernetes связывается с драйвером. За счет этого мы получаем полную стандартизацию и унификацию внешнего взаимодействия с драйвером.

Capabilities: как Kubernetes узнает о возможностях драйвера

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

Как работают Capabilities

Каждый сервис CSI (Identity, Controller, Node) имеет свой метод для возврата списка поддерживаемых capabilities:

  • Identity Service. GetPluginCapabilities — сообщает о поддержке Controller Service и топологических ограничениях.

  • Controller Service. ControllerGetCapabilities — перечисляет возможности контроллера (создание томов, снимки, расширение и т.д.).

  • Node Service. NodeGetCapabilities — описывает возможности ноды (подготовительное монтирование, статистика, расширение и т.д.).

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

Зачем нужны Capabilities

Kubernetes использует capabilities для принятия решений на разных этапах работы.

Пример 1: Расширение тома

Пользователь запрашивает расширение PVC. Если драйвер сообщил о capability EXPAND_VOLUME в ControllerGetCapabilities, Kubernetes:

  1. Вызовет ControllerExpandVolume для расширения тома на стороне хранилища,

  2. Затем вызовет NodeExpandVolume для расширения файловой системы на ноде.

Если capability отсутствует, Kubernetes вернет ошибку при попытке расширения.

Пример 2: Подготовительное монтирование томов

Если драйвер не сообщает о capability STAGE_UNSTAGE_VOLUME в NodeGetCapabilities:

  • Kubernetes пропустит вызовы NodeStageVolume и NodeUnstageVolume.

  • Будет вызывать только NodePublishVolume и NodeUnpublishVolume.

  • Это нормально для файловых систем типа NFS, которые не требуют подготовительного монтирования.

При��ер 3: Статистика томов

Если драйвер сообщает о capability GET_VOLUME_STATS:

  • Kubelet будет периодически вызывать NodeGetVolumeStats для сбора метрик.

  • Метрики отображаются в kubectl describe pvc.

  • Используются для мониторинга и принятия решений о расширении.

Важно: Capabilities определяются на этапе инициализации драйвера и не изменяются во время работы. Если драйвер не сообщает о какой-либо capability, Kubernetes не будет пытаться использовать соответствующую функциональность, даже если она технически реализована в драйвере.

Жизненный цикл тома

Жизненный цикл тома в CSI состоит из нескольких четко определённых этапов, каждый из которых реализуется через соответствующие RPC-вызовы. Полный жизненный цикл тома включает следующие этапы:

  1. Provisioning (создание тома) — создание ресурса в бэкенд-хранилище.

  2. Attach (подключение к ноде) — подключение тома к конкретной ноде кластера.

  3. Stage (подготовительное монтирование) — подготовка тома на ноде (опционально).

  4. Publish (монтирование в под) — монтирование тома в файловую систему пода.

  5. Use (использование) — работа приложения с томом.

  6. Unpublish (размонтирование из пода) — размонтирование тома из пода.

  7. Unstage (очистка подготовительного монтирования) — очистка подготовительного пути (опционально).

  8. Detach (отключение от ноды) — отключение тома от ноды.

  9. Delete (удаление тома) — удаление ресурса из бэкенд-хранилища.

Давайте разберём каждый этап подробно.

Этап 1: Provisioning — создание тома

Цель. Создать том в бэкенд-хранилище и связать его с PVC в Kubernetes.

Шаг 1.1: Создание StorageClass

Перед созданием тома необходимо определить StorageClass, который указывает, какой CSI-драйвер использовать и какие параметры передавать при создании тома.

Пример StorageClass для NFS:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
   name: nfs-csi
provisioner: nfs.csi.k8s.io
parameters:
   server: nfs-server.example.com
   share: /exports
   mountPermissions: "0777"
allowVolumeExpansion: true
volumeBindingMode: Immediate

Пример StorageClass для блочного устройства (AWS EBS):

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
   name: ebs-csi
provisioner: ebs.csi.aws.com
parameters:
   type: gp3
   iops: "3000"
   throughput: "125"
   encrypted: "true"
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer

Шаг 1.2: Создание PVC

Пользователь создает PersistentVolumeClaim, который запрашивает том определенного размера и параметров.

Пример PVC:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
   name: my-pvc
spec:
   accessModes:     - ReadWriteOnce
   storageClassName: nfs-csi
   resources:
     requests:
       storage: 10Gi

Шаг 1.3: RPC-вызов CreateVolume

Когда external-provisioner (sidecar-контейнер) обнаруживает новый PVC, он вызывает RPC-метод CreateVolume у Controller Service.

Параметры CreateVolume:

  • name: уникальное имя тома (генерируется Kubernetes, например, pvc-12345678-1234-1234-1234-123456789012)

  • capacity_range: запрошенный размер тома (например, 10Gi).

  • volume_capabilities: требования к доступу:

    • access_mode: режим доступа на ноде / нодах.

    • access_type: MOUNT (файловая система) или BLOCK (блочное устройство).

  • parameters: параметры из StorageClass (например, servershare для NFS).

  • secrets: учетные данные для доступа к хранилищу (опционально).

  • content_source: источник для клонирования или восстановления из снимка (опционально).

Пример запроса CreateVolume для NFS:

CreateVolumeRequest {
   name: "pvc-12345678-1234-1234-1234-123456789012"
   capacity_range {
     required_bytes: 10737418240  // 10Gi
   }
   volume_capabilities {
     access_mode {
       mode: SINGLE_NODE_WRITER
     }
     access_type {
       mount {
         fs_type: "nfs"
       }
     }
   }
   parameters {
     key: "server"
     value: "nfs-server.example.com"
   }
   parameters {
     key: "share"
     value: "/exports"
   }
}

Ответ CreateVolume:

CreateVolumeResponse {
   volume {
     capacity_bytes: 10737418240
     volume_id: "nfs://nfs-server.example.com/exports/pvc-12345678"
     volume_context {
       key: "server"
       value: "nfs-server.example.com"
     }
     volume_context {
       key: "share"
       value: "/exports/pvc-12345678"
     }
   }
}

Что происходит на стороне драйвера:

Для NFS. Драйвер создаёт директорию на NFS-сервере (например, /exports/pvc-12345678) и настраивает права доступа.

Для блочного устройства (AWS EBS). Драйвер вызывает AWS API для создания EBS volume в указанной зоне доступности.

Пример команды для отладки (проверка созданного тома на NFS-сервере):

# На NFS-сервере
ls -la /exports/pvc-12345678
# Должна быть создана директория с соответствующими правами

Шаг 1.4: Создание PV и привязка к PVC

После успешного вызова CreateVolume, external-provisioner создает PersistentVolume и привязывает его к PVC.

Пример созданного PV:

apiVersion: v1
kind: PersistentVolume
metadata:
   name: pv-12345678
spec:
   capacity:
     storage: 10Gi
   accessModes:
     - ReadWriteOnce
   persistentVolumeReclaimPolicy: Delete
   storageClassName: nfs-csi
   csi:
     driver: nfs.csi.k8s.io
     volumeHandle: nfs://nfs-server.example.com/exports/pvc-12345678
     volumeAttributes:
       server: nfs-server.example.com
       share: /exports/pvc-12345678
   claimRef:
     name: my-pvc
     namespace: default

Проверка статуса:

kubectl get pvc my-pvc
# NAME     STATUS   VOLUME           CAPACITY   ACCESS MODES   STORAGECLASS   AGE#
my-pvc   Bound    pv-12345678      10Gi       RWO            nfs-csi        5s 
 
kubectl get pv pv-12345678 #
NAME           CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM            STORAGECLASS   AGE#
pv-12345678    10Gi       RWO            Delete           Bound    default/my-pvc   nfs-csi        5s

Этап 2: Attach — подключение тома к ноде

Цель: Подключить том к конкретной ноде кластера, сделав его доступным для использования на этой ноде.

Шаг 2.1: Создание пода с использованием PVC

Пользователь создаёт под, который использует PVC через volumeMounts.

Пример пода:

apiVersion: v1
kind: Pod
metadata:
   name: my-pod
spec:
   containers:
   - name: app
     image: nginx:latest
     volumeMounts:
     - name: data
       mountPath: /data
   volumes:
   - name: data
     persistentVolumeClaim:
       claimName: my-pvc

Шаг 2.2: Планирование пода на ноду

kube-scheduler назначает под на конкретную ноду кластера. Для томов с volumeBindingMode: WaitForFirstConsumer планирование пода происходит до создания тома (том создаётся после планирования пода), и том создаётся в зоне доступности выбранной ноды. Для volumeBindingMode: Immediate том создаётся сразу при создании PVC, до планирования пода.

Шаг 2.3: RPC-вызов ControllerPublishVolume

После того как под запланирован на ноду, external-attacher (sidecar-контейнер) вызывает RPC-метод ControllerPublishVolume у Controller Service.

Параметры ControllerPublishVolume:

  • volume_id: идентификатор тома (из CreateVolumeResponse).

  • node_id: уникальный идентификатор ноды (из NodeGetInfo).

  • volume_capability: требования к доступу (из CreateVolume).

  • readonly: флаг только для чтения (опционально).

  • secrets: учетные данные для доступа к хранилищу (опционально).

  • volume_context: контекст тома (из CreateVolumeResponse).

Пример запроса ControllerPublishVolume для NFS:

ControllerPublishVolumeRequest {
   volume_id: "nfs://nfs-server.example.com/exports/pvc-12345678"
   node_id: "node-1"
   volume_capability {
     access_mode {
       mode: MULTI_NODE_MULTI_WRITER
     }
     access_type {
       mount {
         fs_type: "nfs"
       }
     }
   }
   readonly: false
}

Ответ ControllerPublishVolume:

ControllerPublishVolumeResponse {
   publish_context {
     key: "devicePath"
     value: "nfs://nfs-server.example.com/exports/pvc-12345678"
   }
}

Что происходит на стороне драйвера:

  • Для NFS. Это может быть виртуальной операцией (no-op), так как NFS доступен всем нодам одновременно. Драйвер может просто проверить доступность share.

  • Для блочного устройства (AWS EBS). Драйвер вызывает AWS API для подключения EBS volume к EC2 инстансу (например, AttachVolume). На стороне EC2 инстанса появляется новое блочное устройство (например, /dev/xvdf).

Пример для блочного устройства (AWS EBS):

ControllerPublishVolumeRequest {
   volume_id: "vol-1234567890abcdef0"
   node_id: "i-0123456789abcdef0"
   volume_capability {
     access_mode {
       mode: SINGLE_NODE_WRITER
     }
     access_type {
       block {}
     }
   }
}
  ControllerPublishVolumeResponse {
   publish_context {
     key: "devicePath"
     value: "/dev/xvdf"
   }
}

Проверка подключения (для блочных устройств):

# На ноде, к которой подключен том lsblk
# Должно появиться новое устройство (например, /dev/xvdf)
# Для AWS EBS можно проверить через AWS CLI aws ec2 describe-volumes --volume-ids vol-1234567890abcdef0
# Должно показать, что том подключен к инстансу

Важно. Для shared storage (NFS, CIFS) этап Attach может быть пропущен, если драйвер не сообщает о capability PUBLISH_UNPUBLISH_VOLUME в ControllerGetCapabilities. В этом случае Kubernetes пропускает вызовы ControllerPublishVolume и ControllerUnpublishVolume. Для блочных устройств это критический этап, так как том может быть подключен только к одной ноде за раз (для access mode ReadWriteOnce).

Этап 3: Stage — подготовительное монтирование (опционально)

Цель. Подготовить том на ноде для использования. Этот этап выполняется один раз на ноду, даже если том используется несколькими подами.

Важно. Этап Stage выполняется только если драйвер сообщает о capability STAGE_UNSTAGE_VOLUME в NodeGetCapabilities. Для файловых систем типа NFS этот этап может быть пропущен.

Шаг 3.1: RPC-вызов NodeStageVolume

Kubelet вызывает RPC-метод NodeStageVolume у Node Service.

Параметры NodeStageVolume:

  • volume_id: идентификатор тома.

  • staging_target_path: путь для подготовительного монтирования (например, /var/lib/kubelet/plugins/kubernetes.io/csi/pv/pvc-12345678-1234-1234-1234-123456789012/globalmount). Формат пути: /var/lib/kubelet/plugins/kubernetes.io/csi/pv/<volume-handle>/globalmount, где <volume-handle> — это volume_id из CreateVolumeResponse.

  • volume_capability: требования к доступу.

  • publish_context: контекст из ControllerPublishVolumeResponse (например, devicePath для блочных устройств).

  • secrets: учетные данные (опционально).

  • volume_context: контекст тома из CreateVolumeResponse.

Пример запроса NodeStageVolume для блочного устройства:

NodeStageVolumeRequest {
   volume_id: "vol-1234567890abcdef0"
   staging_target_path: "/var/lib/kubelet/plugins/kubernetes.io/csi/pv/vol-1234567890abcdef0/globalmount"
   volume_capability {
     access_mode {
       mode: SINGLE_NODE_WRITER
     }
     access_type {
       mount {
         fs_type: "ext4"
         mount_flags: "noatime"
       }
     }
   }
   publish_context {
     key: "devicePath"
     value: "/dev/xvdf"
   }
}

Что происходит на стороне драйвера:

  • Для блочного устройства:

  1. Драйвер проверяет, отформатировано ли устройство (например, через blkid).

  2. Если не отформатировано, форматирует его (например, mkfs.ext4 /dev/xvdf).

  3. Монтирует устройство в staging path (например, mount -t ext4 /dev/xvdf /var/lib/kubelet/plugins/.../globalmount).

  • Для NFS. Этот этап обычно пропускается, так как NFS не требует подготовительного монтирования.

Пример команд, которые выполняет драйвер для блочного устройства:

# Проверка форматирования blkid /dev/xvdf
# Если не отформатировано, форматируем 
mkfs.ext4 /dev/xvdf
 # Монтирование в staging path
mkdir -p /var/lib/kubelet/plugins/kubernetes.io/csi/pv/vol-1234567890abcdef0/globalmount
mount -t ext4 -o noatime /dev/xvdf /var/lib/kubelet/plugins/kubernetes.io/csi/pv/vol-1234567890abcdef0/globalmount

Проверка подготовительного монтирования:

# На ноде mount | grep globalmount
# Должно показать монтирование блочного устройства в staging path
ls -la /var/lib/kubelet/plugins/kubernetes.io/csi/pv/vol-1234567890abcdef0/globalmount
# Должна быть видна файловая система тома

Этап 4: Publish — монтирование тома в под

Цель. Смонтировать том в файловую систему пода, сделав его доступным для приложения.

Шаг 4.1: RPC-вызов NodePublishVolume

Kubelet вызывает RPC-метод NodePublishVolume у Node Service.

Параметры NodePublishVolume:

  • volume_id: идентификатор тома.

  • staging_target_path: путь подготовительного монтирования (если использовалось NodeStageVolume).

  • target_path: путь монтирования в под (например, /var/lib/kubelet/pods/pod-123/volumes/kubernetes.io~csi/pvc-12345678/mount).

  • volume_capability: требования к доступу.

  • readonly: флаг только для чтения.

  • publish_context: контекст из ControllerPublishVolumeResponse.

  • secrets: учетные данные (опционально).

  • volume_context: контекст тома из CreateVolumeResponse.

Пример запроса NodePublishVolume для NFS:

NodePublishVolumeRequest {
   volume_id: "nfs://nfs-server.example.com/exports/pvc-12345678"
   target_path: "/var/lib/kubelet/pods/pod-123/volumes/kubernetes.io~csi/pvc-12345678/mount"
   volume_capability {
     access_mode {
       mode: MULTI_NODE_MULTI_WRITER
     }
     access_type {
       mount {
         fs_type: "nfs"
         mount_flags: "vers=4.1"
       }
     }
   }
   readonly: false
   volume_context {
     key: "server"
     value: "nfs-server.example.com"
   }
   volume_context {
     key: "share"
     value: "/exports/pvc-12345678"
   }
}

Что происходит на стороне драйвера:

  • Для NFS:

# Драйвер выполняет команду монтирования mount -t nfs4 -o vers=4.1,noatime nfs-server.example.com:/exports/pvc-12345678 \   /var/lib/kubelet/pods/pod-123/volumes/kubernetes.io~csi/pvc-12345678/mount
  • Для блочного устройства:

# Драйвер создаёт bind mount из staging path в target path mount --bind /var/lib/kubelet/plugins/kubernetes.io/csi/pv/vol-1234567890abcdef0/globalmount \   /var/lib/kubelet/pods/pod-123/volumes/kubernetes.io~csi/pvc-12345678/mount

Пример запроса NodePublishVolume для блочного устройства:

NodePublishVolumeRequest {
   volume_id: "vol-1234567890abcdef0"
   staging_target_path: "/var/lib/kubelet/plugins/kubernetes.io/csi/pv/vol-1234567890abcdef0/globalmount"
   target_path: "/var/lib/kubelet/pods/pod-123/volumes/kubernetes.io~csi/pvc-12345678/mount"
   volume_capability {
     access_mode {
       mode: SINGLE_NODE_WRITER
     }
     access_type {
       mount {
         fs_type: "ext4"
       }
     }
   }
   readonly: false
}

Проверка монтирования в под:

# На ноде mount | grep pvc-12345678
# Должно показать монтирование тома в target path
# Внутри пода kubectl exec my-pod -- ls -la /data
# Должна быть видна файловая система тома
# Проверка записи kubectl exec my-pod -- sh -c "echo 'test' > /data/test.txt" kubectl exec my-pod -- cat /data/test.txt
# Должно вывести: test

Этап 5: Use — использование тома

Цель. Приложение использует том для чтения и записи данных.

После успешного монтирования том становится доступным в контейнере по пути, указанному в volumeMounts.mountPath. Приложение может читать и записывать данные в этот путь.

Пример использования в приложении:

# Python пример
with open('/data/app.log', 'a') as f:
     f.write('Application started\n')
  # Данные сохраняются на томе и будут доступны после перезапуска пода

Проверка работы приложения:

# Запись данных kubectl exec my-pod -- sh -c "echo 'Hello from pod' > /data/hello.txt"
# Чтение данных kubectl exec my-pod -- cat /data/hello.txt 
# Проверка статистики тома kubectl describe pvc my-pvc
# В разделе Status должны быть видны метрики использования (если драйвер поддерживает GET_VOLUME_STATS)

Мониторинг использования тома:

Если драйвер поддерживает capability GET_VOLUME_STATS, kubelet периодически вызывает NodeGetVolumeStats для сбора метрик:

NodeGetVolumeStatsRequest {
   volume_id: "nfs://nfs-server.example.com/exports/pvc-12345678"
   volume_path: "/var/lib/kubelet/pods/pod-123/volumes/kubernetes.io~csi/pvc-12345678/mount"
}
  NodeGetVolumeStatsResponse {
   usage {
     total_bytes: 10737418240
     available_bytes: 8589934592
     used_bytes: 2147483648
   }
   volume_condition {
     abnormal: false
     message: "Volume is healthy"
   }
}

Эти метрики отображаются в kubectl describe pvc и используются для мониторинга в Prometheus.

Этап 6: Unpublish — размонтирование тома из пода

Цель. Размонтировать том из файловой системы пода при его удалении или volumeMount.

Шаг 6.1: RPC-вызов NodeUnpublishVolume

Когда под удаляется или volumeMount удаляется из пода, kubelet вызывает RPC-метод NodeUnpublishVolume у Node Service.

Параметры NodeUnpublishVolume:

  • volume_id: идентификатор тома.

  • target_path: путь монтирования в под.

Пример запроса NodeUnpublishVolume:

NodeUnpublishVolumeRequest {
   volume_id: "nfs://nfs-server.example.com/exports/pvc-12345678"
   target_path: "/var/lib/kubelet/pods/pod-123/volumes/kubernetes.io~csi/pvc-12345678/mount"
}

Что происходит на стороне драйвера:

  • Для NFS:

# Драйвер выполняет размонтирование umount /var/lib/kubelet/pods/pod-123/volumes/kubernetes.io~csi/pvc-12345678/mount
  • Для блочного устройства:

# Драйвер размонтирует bind mount umount /var/lib/kubelet/pods/pod-123/volumes/kubernetes.io~csi/pvc-12345678/mount

Важно: Метод должен быть идемпотентным. Повторный вызов с тем же target_path не должен вызывать ошибку, если том уже размонтирован.

Проверка размонтирования:

# На ноде mount | grep pvc-12345678
# Не должно показывать монтирование в target path пода

Этап 7: Unstage — очистка подготовительного монтирования (опционально)

Цель. Очистить подготовительное монтирование тома, когда том больше не используется ни одним подом на ноде.

Шаг 7.1: RPC-вызов NodeUnstageVolume

После того как все поды, использующие том, удалены с ноды, kubelet вызывает RPC-метод NodeUnstageVolume у Node Service (если использовалось NodeStageVolume).

Параметры NodeUnstageVolume:

  • volume_id: идентификатор тома.

  • staging_target_path: путь подготовительного монтирования.

Пример запроса NodeUnstageVolume:

NodeUnstageVolumeRequest {
   volume_id: "vol-1234567890abcdef0"
   staging_target_path: "/var/lib/kubelet/plugins/kubernetes.io/csi/pv/vol-1234567890abcdef0/globalmount" 
}

Что происходит на стороне драйвера:

  • Для блочного устройства:

# Драйвер размонтирует staging path umount /var/lib/kubelet/plugins/kubernetes.io/csi/pv/vol-1234567890abcdef0/globalmount  # Опционально: очистка директорииrmdir /var/lib/kubelet/plugins/kubernetes.io/csi/pv/vol-1234567890abcdef0/globalmount
  • Для NFS. Этот этап обычно пропускается, так как подготовительное монтирование не использовалось.

Важно. Метод должен быть идемпотентным. Повторный вызов не должен вызывать ошибку, если подготовительное монтирование уже очищено.

Проверка очистки:

# На ноде mount | grep globalmount
# Не должно показывать монтирован��е в staging path

Этап 8: Detach — отключение тома от ноды

Цель. Отключить том от ноды, когда том больше не используется на этой ноде.

Шаг 8.1: RPC-вызов ControllerUnpublishVolume

После размонтирования тома на ноде, external-attacher вызывает RPC-метод ControllerUnpublishVolume у Controller Service.

Параметры ControllerUnpublishVolume:

  • volume_id: идентификатор тома.

  • node_id: идентификатор ноды, от которой отключается том.

Пример запроса ControllerUnpublishVolume:

ControllerUnpublishVolumeRequest {
   volume_id: "vol-1234567890abcdef0"
   node_id: "i-0123456789abcdef0"
}

Что происходит на стороне драйвера:

  • Для NFS. Если драйвер не сообщает о capability PUBLISH_UNPUBLISH_VOLUME, этот этап пропускается. Если capability присутствует, операция может быть виртуальной (no-op), так как NFS доступен всем нодам одновременно.

  • Для блочного устройства (AWS EBS). Драйвер вызывает AWS API для отключения EBS volume от EC2 инстанса (например, DetachVolume).

Пример для блочного устройства (AWS EBS):

# Драйвер вызывает AWS API aws ec2 detach-volume --volume-id vol-1234567890abcdef0 --instance-id i-0123456789abcdef0 
# На стороне EC2 инстанса блочное устройство исчезает lsblk
# Устройство /dev/xvdf больше не должно быть видно

Важно. Метод должен быть идемпотентным. Повторный вызов не должен вызывать ошибку, если том уже отключен.

Проверка отключения (для блочных устройств):

# Для AWS EBS можно проверить через AWS CLI aws ec2 describe-volumes --volume-ids vol-1234567890abcdef0
# Должно показать, что том отключен от инстанса

Этап 9: Delete — удаление тома

Цель. Удалить том из бэкенд-хранилища при удалении PVC.

Шаг 9.1: Удаление PVC

Пользователь удаляет PersistentVolumeClaim:

kubectl delete pvc my-pvc

Шаг 9.2: RPC-вызов DeleteVolume

Когда external-provisioner обнаруживает удаление PVC, он вызывает RPC-метод DeleteVolume у Controller Service.

Параметры DeleteVolume:

  • volume_id: идентификатор тома для удаления.

  • secrets: учетные данные для доступа к хранилищу (опционально).

Пример запроса DeleteVolume:

DeleteVolumeRequest {
   volume_id: "nfs://nfs-server.example.com/exports/pvc-12345678" 
}

Что происходит на стороне драйвера:

  • Для NFS. Драйвер удаляет директорию на NFS-сервере (например, rm -rf /exports/pvc-12345678).

  • Для блочного устройства (AWS EBS). Драйвер вызывает AWS API для удаления EBS volume (например, DeleteVolume).

Пример для NFS:

# На NFS-сервереrm -rf /exports/pvc-12345678

 Пример для блочного устройства (AWS EBS):

# Драйвер вызывает AWS API aws ec2 delete-volume --volume-id vol-1234567890abcdef0

Важно. Метод должен быть идемпотентным. Повторный вызов с тем же volume_id не должен вызывать ошибку, если том уже удален.

Проверка удаления:

# Проверка статуса PVC kubectl get pvc my-pvc
# Должно вернуть: Error from server (NotFound)
# Для NFS: проверка на сервереls -la /exports/pvc-12345678
# Должно вернуть: No such file or directory
# Для AWS EBS: проверка через AWS CLI aws ec2 describe-volumes --volume-ids vol-1234567890abcdef0
# Должно вернуть: InvalidVolume.NotFound

Дополнительные операции жизненного цикла

Расширение тома (Volume Expansion)

Если пользователь изменяет размер PVC (например, с 10Gi на 20Gi), происходит процесс расширения тома:

1) ControllerExpandVolume (Controller Service): расширение тома на стороне хранилища.

2) NodeExpandVolume (Node Service): расширение файловой системы на ноде.

  • Вызывается kubelet после успешного ControllerExpandVolume.

  • Для ext4: resize2fs /dev/xvdf.

  • Ссылка: NodeExpandVolume RPC.

Пример расширения PVC:

# Изменение размера PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
   name: my-pvc
spec:
   accessModes:
     - ReadWriteOnce
   storageClassName: ebs-csi
   resources:
     requests:
       storage: 20Gi
  # Увеличено с 10Gi до 20Gi

Снимки томов (Snapshots)

Для создания снимка тома используется следующий процесс:

1) CreateSnapshot (Controller Service): создание снимка тома.

  • Вызывается sidecar-контейнером external-snapshotter при создании VolumeSnapshot.

  • Ссылка: CreateSnapshot RPC.

2) Восстановление из снимка: При создании PVC из VolumeSnapshotCreateVolume получает content_source с snapshot_id

Пример создания снимка:

apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
   name: my-snapshot
spec:
   source:
     persistentVolumeClaimName: my-pvc

Клонирование томов

Для клонирования тома используется CreateVolume с параметром content_source, содержащим volume_id исходного тома.

Пример клонирования:

apiVersion: v1kind: PersistentVolumeClaim
metadata:
   name: cloned-pvc
spec:
   dataSource:
     name: my-pvc
     kind: PersistentVolumeClaim
   accessModes:
     - ReadWriteOnce
   storageClassName: ebs-csi
   resources:
     requests:
       storage: 10Gi

Подробнее про сервисы CSI

CSI определяет три сервиса: Identity (обязательный для всех), Controller (опционально, для глобальных операций), Node (обязательный для драйверов с Node Plugin).

Identity Service: точка входа в CSI

Identity Service — это первый и обязательный сервис, который должен реализовать каждый CSI-драйвер. Он вызывается при инициализации плагина и позволяет Kubernetes обнаружить возможности драйвера. Все три метода этого сервиса являются обязательными.

Важно. Identity Service должен быть доступен всегда, даже если Controller или Node Service временно недоступны. Это позволяет Kubernetes понять, что драйвер установлен, но может быть временно не гот��в.

Обязательные методы Controller Service

GetPluginInfo

Возвращает базовую информацию о плагине:

  • name: уникальное имя драйвера (например, nfs.csi.k8s.io).

  • vendor_version: версия драйвера.

Этот метод вызывается первым при подключении к плагину. Kubernetes использует имя для идентификации драйвера в StorageClass и других ресурсах.

GetPluginCapabilities

Сообщает Kubernetes, какие сервисы поддерживает плагин:

  • CONTROLLER_SERVICE. Плагин поддерживает Controller Service. На основе этого Kubernetes решает, нужно ли вызывать Controller Service или достаточно только Node Service (например, для headless-драйверов, которые работают только на нодах).

  • VOLUME_ACCESSIBILITY_CONSTRAINTS. Плагин имеет топологические ограничения. Например, том доступен только в определенной зоне или регионе. Kubernetes будет учитывать это при планировании подов.

Probe

Проверка готовности плагина (health check). Возвращает READY или NOT_READY. Используется sidecar-контейнером livenessprobe для мониторинга здоровья драйвера.

Controller Service: управление жизненным циклом томов

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

Controller Service вызывается через sidecar-контейнеры Kubernetes (external-provisioner, external-attacher, external-resizer, external-snapshotter), которые отслеживают изменения в API-сервере и делегируют операции драйверу.

CreateVolume

Создаёт новый том в хранилище. Это основной метод для динамического provisioning томов.

  • name: уникальное имя тома (генерируется Kubernetes).

  • capacity_range: запрошенный размер тома.

  • volume_capabilities: требования к доступу (block/mount, read-only/read-write).

  • parameters: параметры из StorageClass (например, тип диска, зона).

  • secrets: учетные данные для доступа к хранилищу (опционально).

  • content_source: источник для клонирования или восстановления из снимка (опционально).

Метод вызывается sidecar-контейнером external-provisioner при создании PVC. Драйвер должен создать том в бэкенд-хранилище и вернуть volume_id, который будет использоваться для всех последующих операций с этим томом.

Пример для NFS: Создание директории на NFS-сервере (например, /exports/my-vol-12345).

DeleteVolume

Удаляет том из хранилища. Вызывается при удалении PVC, когда том больше не используется.

  • volume_id: идентификатор тома для удаления.

Метод должен быть идемпотентным: повторный вызов с тем же volume_id не должен вызывать ошибку, если том уже удален. Вызывается sidecar-контейнером external-provisioner при удалении PVC.

Пример для NFS: Удаление директории на NFS-сервере.

ControllerPublishVolume

Подключает том к конкретной ноде, делая его доступным для использования на этой ноде. Это критический шаг перед монтированием тома на ноде.

  • volume_id: идентификатор тома.

  • node_id: идентификатор ноды, к которой подключается том.

  • volume_capability: требования к доступу.

  • readonly: флаг только для чтения.

  • secrets: учетные данные (опционально).

Для shared storage (например, NFS) это может быть виртуальной операцией, но для блочных устройств (например, AWS EBS) это реальное подключение диска к инстансу. Вызывается sidecar-контейнером external-attacher после того, как под запланирован на ноду.

Пример для NFS: подготовка share (может быть no-op, так как NFS доступен всем нодам).

ControllerUnpublishVolume

Отключает том от ноды. Вызывается когда под удаляется или мигрирует на другую ноду.

  • volume_id: идентификатор тома.

  • node_id: идентификатор ноды, от которой отключается том.

Метод должен быть идемпотентным. Вызывается sidecar-контейнером external-attacher после размонтирования тома на ноде.

Пример для NFS: Cleanup операций (может быть no-op).

ValidateVolumeCapabilities

Проверяет, поддерживает ли том запрошенные capabilities (например, access modes, volume mode).

  • volume_id: идентификатор тома (для существующих томов) или пустой (для проверки параметров).

  • volume_capabilities: запрошенные capabilities.

  • parameters: параметры из StorageClass.

Используется для валидации запросов перед созданием или использованием тома. Может вызываться как external-provisioner для проверки параметров StorageClass, так и другими компонентами для проверки совместимости.

Пример для NFS: проверка, что запрошенный access mode (ReadWriteMany, ReadOnlyMany) поддерживается NFS.

ControllerGetCapabilities

Возвращает список возможностей Controller Service. Это ключевой метод для обнаружения поддерживаемых операций. Kubernetes использует эту информацию для принятия решений: какие операции доступны, нужно ли вызывать определенные RPC и т.д.

  • CREATE_DELETE_VOLUME: Поддержка создания и удаления томов (динамическое provisioning).

  • PUBLISH_UNPUBLISH_VOLUME: Поддержка подключения/отключения томов к нодам.

  • LIST_VOLUMES: Поддержка получения списка томов.

  • GET_CAPACITY: Поддержка запроса доступной емкости.

  • CREATE_DELETE_SNAPSHOT: Поддержка создания и удаления снимков.

  • LIST_SNAPSHOTS: Поддержка получения списка снимков.

  • CLONE_VOLUME: Поддержка клонирования томов.

  • EXPAND_VOLUME: Поддержка расширения томов (используется external-resizer).

  • VOLUME_CONDITION: Поддержка условий тома (например, «том готов», «том поврежден»).

  • GET_VOLUME: Поддержка получения информации о конкретном томе

Важно. Controller Service опционален. Драйверы могут работать только с Node Service (headless-драйверы), но большинство production-драйверов требуют Controller Service для динамического provisioning и управления томами.

Опциональные методы Controller Service

ListVolumes

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

  • max_entries: максимальное количество томов для возврата (опционально, для пагинации).

  • starting_token: токен для пагинации (опционально).

Метод должен быть реализован, если драйвер сообщает о capability LIST_VOLUMES в ControllerGetCapabilities. Может использоваться администраторами для получения списка всех томов в хранилище.

Пример для NFS: возврат списка всех директорий на NFS-сервере, которые были созданы через CSI.

GetCapacity

Возвращает доступную ёмкость хранилища для создания новых томов. Используется Kubernetes при планировании создания томов и принятии решений о размещении.

  • volume_capabilities: требования к доступу (block/mount, read-only/read-write).

  • parameters: параметры из StorageClass (например, тип диска, зона).

Метод должен быть реализован, если драйвер сообщает о capability GET_CAPACITY в ControllerGetCapabilities. Kubernetes может использовать эту информацию для принятия решений о том, где создавать тома, и для предупреждения о нехватке места.

Пример для NFS: Возврат доступного пространства на NFS-сервере для определённого типа томов.

CreateSnapshot

Создаёт снимок (snapshot) существующего тома. Используется для backup-операций и создания точек восстановления.

  • source_volume_id: идентификатор исходного тома.

  • name: уникальное имя снимка (генерируется Kubernetes).

  • secrets: учётные данные для доступа к хранилищу (опционально).

  • parameters: дополнительные параметры для создания снимка (опционально).

Метод должен быть реализован, если драйвер сообщает о capability CREATE_DELETE_SNAPSHOT в ControllerGetCapabilities. Вызывается sidecar-контейнером external-snapshotter при создании VolumeSnapshot. Возвращает snapshot_id, который можно использовать для восстановления тома.

Пример для NFS: Создание моментального снимка директории на NFS-сервере (например, через LVM snapshot или rsync).

DeleteSnapshot

Удаляет снимок тома. Используется для очистки старых снимков и освобождения места.

  • snapshot_id: идентификатор снимка для удаления.

  • secrets: учётные данные (опционально).

Метод должен быть реализован, если драйвер сообщает о capability CREATE_DELETE_SNAPSHOT в ControllerGetCapabilities. Метод должен быть идемпотентным: повторный вызов с тем же snapshot_id не должен вызывать ошибку, если снимок уже удалён. Вызывается sidecar-контейнером external-snapshotter при удалении VolumeSnapshot.

Пример для NFS: удаление моментального снимка директории на NFS-сервере.

ListSnapshots

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

  • snapshot_id: фильтр по идентификатору снимка (опционально).

  • source_volume_id: фильтр по исходному тому (опционально).

  • max_entries: максимальное количество снимков для возврата (опционально, для пагинации).

  • starting_token: токен для пагинации (опционально).

  • secrets: учетные данные (опционально).

Метод должен быть реализован, если драйвер сообщает о capability LIST_SNAPSHOTS в ControllerGetCapabilities. Может использоваться для получения списка всех снимков или фильтрации по исходному тому.

Пример для NFS: возврат списка всех моментальных снимков на NFS-сервере или снимков конкретного тома.

ControllerExpandVolume

Расширяет том на стороне хранилища. Это первый этап процесса расширения тома, который выполняется до расширения файловой системы на ноде.

  • volume_id: идентификатор тома для расширения.

  • capacity_range: новый запрошенный размер тома.

  • secrets: учётные данные (опционально).

  • volume_capability: требования к доступу (опционально).

Метод должен быть реализован, если драйвер сообщает о capability EXPAND_VOLUME в ControllerGetCapabilities. Вызывается sidecar-контейнером external-resizer при изменении размера PVC. После успешного расширения тома на стороне хранилища, kubelet вызовет NodeExpandVolume для расширения файловой системы на ноде.

Пример для NFS: увеличение квоты или размера директории на NFS-сервере.

Пример для блочного устройства: расширение диска в облачном хранилище (например, AWS EBS volume).

ControllerGetVolume

Возвращает информацию о конкретном томе. Используется для отладки, мониторинга и получения статуса тома.

  • volume_id: идентификатор тома.

  • volume_attributes: атрибуты тома для фильтрации (опционально).

Метод должен быть реализован, если драйвер сообщает о capability GET_VOLUME в ControllerGetCapabilities. Возвращает детальную информацию о томе, включая его состояние, условия (volume conditions) и метаданные. Может использоваться для диагностики проблем с томами.

Пример для NFS: возврат информации о директории на NFS-сервере: размер, права доступа, статус.

GetMetadataDelta

Возвращает инкрементальные изменения метаданных тома для инкрементальных бэкапов. Позволяет эффективно отслеживать изменения между снимками.

  • volume_id: идентификатор тома.

  • base_snapshot_id: базовый снимок для сравнения.

  • target_snapshot_id: целевой снимок для сравнения (опционально).

  • secrets: учётные данные (опционально).

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

Пример для NFS: возврат списка изменённых файлов между двумя моментальными снимками директории.

ControllerModifyVolume

Модифицирует параметры существующего тома. Позволяет изменять свойства тома без его пересоздания.

  • volume_id: идентификатор тома для модификации.

  • mutations: список изменений для применения к тому.

  • secrets: учётные данные (опционально).

Метод позволяет изменять свойства тома, такие как параметры производительности, политики репликации или другие настройки, определённые драйвером. Это полезно для оптимизации работы томов без необходимости их пересоздания.

Пример для NFS: изменение параметров квоты, прав доступа или политик кэширования для директории на NFS-сервере.

Пример для блочного устройства: изменение типа диска (например, с gp2 на gp3 в AWS) или параметров IOPS.

GetSnapshot

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

  • snapshot_id: идентификатор снимка.

  • secrets: учётные данные (опционально).

Метод возвращает информацию о снимке, включая его состояние (готов, в процессе создания, ошибка), размер, дату создания и другие метаданные. Может использоваться для проверки статуса операции создания снимка или для получения деталей существующего снимка.

Пример для NFS: возврат информации о моментальном снимке директории: размер, дата создания, статус.

GetMetadataAllocated

Возвращает информацию о выделенном пространстве для тома. Используется для мониторинга использования и оптимизации размещения данных.

  • volume_id: идентификатор тома.

  • secrets: учётные данные (опционально).

Метод возвращает информацию о том, сколько места фактически выделено для тома в хранилище. Это может отличаться от запрошенного размера из-за особенностей выделения места в хранилище (например, округление до ближайшего блока или минимального размера).

Пример для NFS: Возврат информации о фактически выделенном пространстве для директории на NFS-сервере (может отличаться от запрошенного размера из-за квот или округления).

Node Service: локальные операции на нодах

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

Node Service вызывается напрямую kubelet через Unix-сокет. Kubelet регистрирует Node Plugin через sidecar-контейнер node-driver-registrar, который использует NodeGetInfo для получения информации о ноде.

Обязательные методы Node Service

NodeStageVolume

Подготавливает том для использования на ноде. Это первый этап монтирования, который выполняется один раз на ноду, даже если том используется несколькими подами.

  • volume_id: идентификатор тома.

  • staging_target_path: путь для подготовительного монтирования (например, /var/lib/kubelet/plugins/kubernetes.io/csi/pv/volume-id/globalmount).

  • volume_capability: требования к доступу (block/mount, read-only/read-write).

  • secrets: учётные данные для доступа к хранилищу (опционально).

  • volume_context: контекст тома из CreateVolumeResponse.

Для блочных устройств этот метод обычно форматирует том (если нужно) и монтирует его в подготовительный путь. Для файловых систем (например, NFS) этот этап может быть пропущен, если драйвер не сообщает о capability STAGE_UNSTAGE_VOLUME.

Вызывается kubelet перед NodePublishVolume. Позволяет переиспользовать одно подготовительное монтирование для нескольких подов, что повышает эффективность.

Пример для NFS: для NFS подготовительное монтирование может быть пропущено, так как NFS можно монтировать напрямую в под.

Пример для блочного устройства: форматирование диска (mkfs.ext4) и монтирование в подготовительный путь (/var/lib/kubelet/plugins/.../globalmount).

NodeUnstageVolume

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

  • volume_id: идентификатор тома.

  • staging_target_path: путь подготовительного монтирования.

Метод должен быть идемпотентным. Вызывается kubelet после NodeUnpublishVolume, когда все поды, использующие том, удалены с ноды.

Пример для NFS: Cleanup операций (может быть no-op, если подготовительное монтирование не использовалось).

Пример для блочного устройства: размонтирование подготовительного пути (umount /var/lib/kubelet/plugins/.../globalmount).

NodePublishVolume

Монтирует том в файловую систему пода. Это финальный этап, который делает том доступным внутри контейнера.

  • volume_id: идентификатор тома.

  • staging_target_path: путь подготовительного монтирования (если использовалось подготовительное монтирование).

  • target_path: путь монтирования в под (например, /var/lib/kubelet/pods/pod-id/volumes/kubernetes.io~csi/volume-id/mount).

  • volume_capability: требования к доступу.

  • readonly: флаг только для чтения.

  • secrets: учётные данные (опционально).

  • volume_context: контекст тома.

Для блочных устройств этот метод создаёт bind mount из подготовительного пути в target path. Для файловых систем (NFS, CIFS) метод монтирует том напрямую в target path.

Вызывается kubelet для каждого пода, который использует том. После успешного вызова том становится доступным в контейнере по указанному пути.

Пример для NFSmount -t nfs server:/path /var/lib/kubelet/pods/.../mount

Пример для блочного устройстваmount --bind /var/lib/kubelet/plugins/.../globalmount /var/lib/kubelet/pods/.../mount

NodeUnpublishVolume

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

  • volume_id: идентификатор тома.

  • target_path: путь монтирования в под.

Метод должен быть идемпотентным. Вызывается kubelet при удалении пода или при удалении volumeMount из пода.

Пример для NFSumount /var/lib/kubelet/pods/.../mount

Пример для блочного устройстваumount /var/lib/kubelet/pods/.../mount (размонтирование bind mount).

NodeGetCapabilities

Возвращает список возможностей Node Service. Это ключевой метод для обнаружения поддерживаемых операций на ноде. Kubernetes использует эту информацию для принятия решений: какие операции доступны, нужно ли вызывать определенные RPC и т.д.

  • STAGE_UNSTAGE_VOLUME: поддержка подготовительного монтирования/размонтирования томов. Если отсутствует, Kubernetes будет пропускать этапы NodeStageVolume/NodeUnstageVolume (например, для NFS это нормально).

  • GET_VOLUME_STATS: поддержка получения статистики использования тома (используется kubelet для метрик).

  • EXPAND_VOLUME: поддержка расширения тома на ноде (после ControllerExpandVolume).

  • VOLUME_CONDITION: поддержка условий тома на ноде.

  • SINGLE_NODE_MULTI_WRITER: поддержка множественной записи с одной ноды (для томов с access mode ReadWriteMany).

Kubelet использует эту информацию для принятия решений: нужно ли вызывать NodeStageVolume, доступна ли статистика, можно ли расширить том и т.д.

Важно. Если драйвер не сообщает о capability STAGE_UNSTAGE_VOLUME, kubelet будет вызывать только NodePublishVolume и NodeUnpublishVolume, пропуская этапы подготовительного монтирования. Это нормально для файловых систем типа NFS.

NodeGetInfo

Возвращает информацию о ноде, на которой работает Node Plugin. Используется для регистрации драйвера в kubelet и для топологических ограничений.

  • node_id: уникальный идентификатор ноды (должен совпадать с node_id, используемым в ControllerPublishVolume).

  • max_volumes_per_node: максимальное количество томов, которые могут быть подключены к ноде (опционально, 0 означает без ограничений).

  • accessible_topology: топологические ограничения ноды (опционально, например, зона, регион).

Вызывается sidecar-контейнером node-driver-registrar при регистрации драйвера в kubelet. Kubelet использует эту информацию для:

  • регистрации драйвера в списке доступных драйверов;

  • проверки топологических ограничений при планировании подов;

  • ограничения количества томов на ноде.

Важно. Node Service работает на каждой ноде и имеет привилегированный доступ к файловой системе (требуется CAP_SYS_ADMIN на Linux). Это необходимо для выполнения операций монтирования и работы с блочными устройствами.

Опциональные методы Node Service

NodeGetVolumeStats

Возвращает статистику использования тома. Используется kubelet для сбора метрик о доступном и использованном пространстве тома.

  • volume_id: идентификатор тома.

  • volume_path: путь монтирования тома на ноде.

Метод должен быть реализован, если драйвер сообщает о capability GET_VOLUME_STATS в NodeGetCapabilities. Возвращает информацию об использовании тома:

  • available_bytes: доступное пространство.

  • total_bytes: общий размер тома.

  • used_bytes: использованное пространство.

  • inodes: статистика по inode (для файловых систем).

Эта информация используется для метрик в Prometheus (например, kubelet_volume_stats_*), отображения в kubectl describe pvc и принятия решений о расширении томов.

Пример для NFS: возврат статистики использования директории на NFS-сервере через df или statfs.

Пример для блочного устройства: возврат статистики использования блочного устройства через df или statfs.

NodeExpandVolume

Расширяет файловую систему тома на ноде. Это второй этап процесса расширения тома, который выполняется после ControllerExpandVolume.

  • volume_id: идентификатор тома для расширения.

  • volume_path: путь монтирования тома на ноде.

  • capacity_range: новый запрошенный размер тома.

  • staging_target_path: путь подготовительного монтирования (если использовалось).

  • volume_capability: требования к доступу (опционально).

Метод должен быть реализован, если драйвер сообщает о capability EXPAND_VOLUME в NodeGetCapabilities. Вызывается kubelet после успешного ControllerExpandVolume. Для блочных устройств метод расширяет файловую систему (например, resize2fs для ext4), для файловых систем может быть no-op, если расширение выполняется автоматически.

Пример для NFS: Расширение файловой системы может быть no-op, если расширение выполняется автоматически на стороне сервера.

Пример для блочного устройства: Расширение файловой системы ext4 через resize2fs /dev/sdX после расширения диска.

Опциональные функции: расширение, статистика и снимки

Хотя многие методы CSI являются опциональными, они критически важны для реального использования. Рассмотрим основные сценарии.

Расширение томов (Volume Expansion)

Процесс расширения тома состоит из двух этапов:

  1. ControllerExpandVolume (Controller Service): расширение тома на стороне хранилища. Вызывается sidecar-контейнером external-resizer при изменении размера PVC. Драйвер должен сообщить о capability EXPAND_VOLUME в ControllerGetCapabilities.

  2. NodeExpandVolume (Node Service): расширение файловой системы на ноде (например, resize2fs для ext4). Вызывается kubelet после успешного ControllerExpandVolume. Драйвер должен сообщить о capability EXPAND_VOLUME в NodeGetCapabilities.

Статистика томов (NodeGetVolumeStats)

Метод NodeGetVolumeStats возвращает информацию об использовании тома:

  • available_bytes: доступное пространство.

  • total_bytes: общий размер тома.

  • used_bytes: использованное пространство.

  • inodes: статистика по inode (для файловых систем).

Эта информация используется для:

  • метрик в Prometheus (например, kubelet_volume_stats_*);

  • отображения в kubectl describe pvc;

  • принятия решений о расширении томов.

Драйвер должен сообщить о capability GET_VOLUME_STATS в NodeGetCapabilities.

Снимки (Snapshots)

Полный цикл работы со снимками включает:

  1. CreateSnapshot: создание снимка тома. Вызывается sidecar-контейнером external-snapshotter при создании VolumeSnapshot. Возвращает snapshot_id, который можно использовать для восстановления.

  2. ListSnapshots: получение списка снимков (с фильтрацией по source volume или snapshot ID).

  3. DeleteSnapshot: удаление снимка. Вызывается при удалении VolumeSnapshot.

  4. Восстановление из снимка: при создании PVC из VolumeSnapshotCreateVolume получает source с snapshot_id, и драйвер создаёт новый том на основе снимка.

Заключение: CSI в действии

CSI — это фундаментальный сдвиг в Kubernetes storage: от монолита к плагинам. С ним можно интегрировать Ceph, AWS EBS, Yandex Cloud или свой S3-совместимый бэкенд без боли, универсально и быстро.

Если же вы хотите максимально погрузиться, можно изучить спецификации:

P. S.

Читайте также в нашем блоге: