Проект на субботний вечер: Пишем минимальный CSI-драйвер для Kubernetes с нуля

Jun 6, 2026

Давненько у меня не было никаких "личных" субботних хакатончиков, хотя ранее они были не только по субботним вечерам. После недолгих размышений было принято решение попробовать то, о чём давно чесались руки: написать небольшой CSI-driver для Kubernetes.

Зачем?

Это небольшой проект для углубления знаний о том, как внутри работает Kubernetes. У меня нет цели собрать полноценный драйвер и зачем-то его выпускать и использовать. Можно было просто ознакомиться с официальным примером драйвера, но мне хотелось пописать код. Так и появился мой minimal CSI driver.

Документация и требуемое окружение

Кратко о CSI-драйвере

Интерфейс CSI абстрагирует логику работы со слоем хранения данных от самого ядра Kubernetes. Полноценный CSI-драйвер состоит из трех основных gRPC-компонентов:

  • Identity Service: Возвращает общую информацию о драйвере (его имя, версию и поддерживаемые возможности). Используется Kubernetes для проверки готовности плагина.
  • Controller Plugin: Отвечает за глобальные операции с томами: создание/удаление, а также за прикрепление тома к конкретному узлу (Attach и/ Detach). Обычно деплоится как одиночный под.
  • Node Plugin: Выполняется на каждом конкретном воркер-ноде в виде DaemonSet. Отвечает за непосредственное форматирование, монтирование и размонтирование дисков в файловую систему, к которой у контейнеров будет доступ.

Proof of concept

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

workload.yaml


apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: minimal-csi
provisioner: minimal.csi.dronov.net
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: false
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: minimal-csi-test
  namespace: default
spec:
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  storageClassName: minimal-csi
  resources:
    requests:
      storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
  name: minimal-csi-test
  namespace: default
spec:
  restartPolicy: Never
  containers:
    - name: test
      image: busybox:1.37
      command:
        - /bin/sh
        - -c
        - |
          set -eu
          echo "minimal CSI driver works" | tee /data/result.txt
          test "$(cat /data/result.txt)" = "minimal CSI driver works"
          ls -la /data
          sleep 3600
      volumeMounts:
        - name: data
          mountPath: /data
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: minimal-csi-test

Пишем код

Структура проекта описана в оригинальной статье. Мой проект тут. Самое интересное находится в pkg/driver:

  • identity.go отвечает на вопросы кубернетеса о том, как называется драйвер, какая у него версия и работает ли он
  • controller.go управляет волума. Когда создаётся PVC, метод CreateVolume создаёт описание волума и сохраняет его в памяти. При этом, как я указывал ранее, реальный диск или каталог во внешнем хранилище не создаётся. DeleteVolume, соответственно, удаляет описание волума
  • node.go предоставляет том конкретному поду. k8s передаёт путь, куда подключать волум, а NodePublishVolume создаёт там пустой каталог, который отображаеться в поде внутри контейнера как /data

Но при сборке проекта я столкнулся с первой сложностью:

# github.com/kubernetes-csi/drivers/pkg/csi-common /home/mesh/go/pkg/mod/github.com/kubernetes-csi/[email protected]/pkg/csi-common/utils.go:76:20: cannot use ids (variable of type *DefaultIdentityServer) as csi.IdentityServer value in argument to s.Start: *DefaultIdentityServer does not implement csi.IdentityServer (missing method mustEmbedUnimplementedIdentityServer) /home/mesh/go/pkg/mod/github.com/kubernetes-csi/[email protected]/pkg/csi-common/utils.go:84:20: cannot use ids (variable of type *DefaultIdentityServer) as csi.IdentityServer value in argument to s.Start: *DefaultIdentityServer does not implement csi.IdentityServer (missing method mustEmbedUnimplementedIdentityServer) /home/mesh/go/pkg/mod/github.com/kubernetes-csi/[email protected]/pkg/csi-common/utils.go:92:20: cannot use ids (variable of type *DefaultIdentityServer) as csi.IdentityServer value in argument to s.Start: *DefaultIdentityServer does not implement csi.IdentityServer (missing method mustEmbedUnimplementedIdentityServer)

Оказалось, это ошибка оригинального кода из-за старого csi-common версии v1.0.0 и новой спеки CSI, где серверные интерфейсы требуют mustEmbedUnimplementedIdentityServer. Пришлось поправить это, запустив gRPC сервер явно.

Далее, произошло следующее:

  • Как оказалось, я сделал несколько опечаток в виде неверных имён serviceAccountName в driver.yaml
  • Решил не использовать ghcr.io из-за 401 при пулле образа, а заливать в kind напрямую
  • Драйвер валился с ошибкой invalid endpoint: only unix:// endpoints are supported, поэтому пришлось также поправить написание сокета в коде

Важное техническое допущение

В NodePublishVolume мы не используем внешнее хранилище, а просто пишем в директорию на ноде, которую предоставил kubelet. Затем kubernetes предоставляет этот каталог для пода в качестве точки монтирования (тома, но тома как такового нет). Изначально я, как и автор оригинальной статьи, не думал о Ceph, NFS и реализации хранения данных где-то в сети.

Билдим бинарь
mkdir -p bin CGO_ENABLED=0 GOOS=linux go build -o bin/minimal-csi-driver cmd/driver/main.go
Собираем имейдж и заливаем в kind
docker build -t minimal-csi-driver:local .
kind load docker-image minimal-csi-driver:local --name kind
Деплоим драйвер в kind

Создадим StorageClass, PVC, Pod

kubectl apply -f deploy/kubernetes/rbac.yaml
kubectl apply -f deploy/kubernetes/driver.yaml
Проверяем сетап драйвера
kubectl apply -f deploy/kubernetes/workload.yaml
kubectl get csidriver
kubectl get storageclass
kubectl get pvc,pv
kubectl get pod minimal-csi-test -o wide

Что хочется видеть в stdout:

- PVC: Bound
- PV: Bound
- Pod: Running
Проверяем

Для проверки запустим busybox, который создаст result.txt:

~/w/m/g/minimal-csi-driver  main [!?✔] ⎈ kind-kind
❯ kubectl exec minimal-csi-test -- cat /data/result.txt
minimal CSI driver works

~/w/m/g/minimal-csi-driver  main [!?✔] ⎈ kind-kind
❯ kubectl logs minimal-csi-test
minimal CSI driver works
total 12
drwxr-xr-x    2 root     root          4096 Jun  6 18:26 .
drwxr-xr-x    1 root     root          4096 Jun  6 18:26 ..
-rw-r--r--    1 root     root            25 Jun  6 18:26 result.txt

Ура! Драйвер запустился, и мы в поде видим желаемый result.txt.

Проверим, действительно ли файл появился у нас на ноде в Kind:

~/w/m/g/minimal-csi-driver  main [✔] ⎈ kind-kind
❯ kubectl get pod minimal-csi-test -o jsonpath='{.metadata.uid}{"\n"}'
50c0d2a8-dc01-4cd6-b6f3-944425aafc29

~/w/m/g/minimal-csi-driver  main [✔] ⎈ kind-kind
❯ docker exec -it kind-control-plane bash
root@kind-control-plane:/# cd /var/lib/kubelet/pods/50c0d2a8-dc01-4cd6-b6f3-944425aafc29/volumes/kubernetes.io~csi/
root@kind-control-plane:/var/lib/kubelet/pods/50c0d2a8-dc01-4cd6-b6f3-944425aafc29/volumes/kubernetes.io~csi# find . -name result.txt
./pvc-5a6b9c1d-5eab-4eca-b077-16390887f8d9/mount/result.txt
root@kind-control-plane:/var/lib/kubelet/pods/50c0d2a8-dc01-4cd6-b6f3-944425aafc29/volumes/kubernetes.io~csi# cat ./pvc-5a6b9c1d-5eab-4eca-b077-16390887f8d9/mount/result.txt
minimal CSI driver works
root@kind-control-plane:/var/lib/kubelet/pods/50c0d2a8-dc01-4cd6-b6f3-944425aafc29/volumes/kubernetes.io~csi# exit
exit

~/w/m/g/minimal-csi-driver  main [✔] ⎈ kind-kind
❯

Что дальше?

Главный экзистенциальный вопрос: что именно мы будем использовать в качестве стораджа у данных ворклоадов? В документации описана минимальность функциональность production-ready драйвера: доделать реальное монтирование волумов, добавить поддержку снапшотов, изменения размера и многое другое.

Но для вечера субботы это уже слишком большая задача :)