Привет, Хабр!

В Kubernetes принято разделение хранилищ на два основных типа: постоянные и временные.

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

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

Классификация хранилищ в Kubernetes не ограничивается только этим разделением. Существуют различные StorageClasses, которые позволяют определять классы хранилищ с разными характеристиками.

Также в Kubernetes реализован контейнерный интерфейс хранения.

Основы всего из этого рассмотрим в этой статье.

Persistent Volumes (PV) и Persistent Volume Claims (PVC)

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

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

Жизненный цикл PV и PVC включает в себя несколько этапов:

  1. Создание PV: администратор или АС создает PV в Kubernetes, определяя параметры хранилища, такие как размер, способ доступа и физическое местоположение.

  2. Запрос PVC: юзеры создают PVC, специфицируя требуемый объем и параметры хранилища.

  3. Привязка: Kubernetes автоматически "привязывает" PVC к соответствующему PV, если требования PVC совпадают с характеристиками PV.

  4. Использование: поды используют привязанные PV через PVC для хранения данных.

  5. Освобождение и повторное использование или удаление: когда поды, использующие PVC, удаляются, PV может быть либо перепривязан к другому PVC, либо очищен и удален, в зависимости от политики его переиспользования.

Динамический провиженинг позволяет автоматически создавать хранилище по запросу PVC без необходимости предварительного создания PV администратором. Это достигается за счет использования StorageClasses, которые описывают "классы" хранилища, доступные для динамического провиженинга.

Основные компоненты StorageClasses:

  • Provisioner: используется для провиженинга томов. Kubernetes предоставляет встроенные провиженеры (например, kubernetes.io/aws-ebs, kubernetes.io/gce-pd), а также поддерживает внешние провиженеры через интерфейс CSI.

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

  • ReclaimPolicy: политика управления жизненным циклом тома после его освобождения подом. Может быть Retain - сохранение тома для будущего использования или Delete - автоматическое удаление тома.

  • VolumeBindingMode: определяет момент привязки PV к PVC. Immediate приводит к немедленной привязке, в то время как WaitForFirstConsumer откладывает привязку до момента создания пода, который будет использовать PVC​.

PVs поддерживают различные способы доступа, включая:

  • ReadWriteOnce (RWO): Volume может быть смонтирован для чтения и записи только одним узлом.

  • ReadOnlyMany (ROX): Volume может быть смонтирован только для чтения несколькими узлами.

  • ReadWriteMany (RWX): Volume может быть смонтирован для чтения и записи несколькими узлами.

Конфигурация PV и PVC определяется через YAML-файл описание, который указывает основные характеристики, такие как:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: example-pv
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: slow
  hostPath:
    path: "/mnt/data"

Этот PV обладает объемом 10 ГиБ, поддерживает режим доступа ReadWriteOnce и будет сохранен после освобождения благодаря политике Retain.

Для создания PVC используется аналогичный синтаксис:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: example-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi

PVC запрашивает 5 ГиБ хранилища, которые могут быть предоставлены любым PV с классом хранилища slow, имеющим минимум 5 ГиБ свободного пространства и поддерживающим режим доступа ReadWriteOnce.

StorageClass определяет, как динамический провиженер должен создавать новые PV для удовлетворения запросов PVC. Пример конфигурации StorageClass:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp2

Этот StorageClass fast использует AWS EBS в качестве базовой инфраструктуры и gp2 в качестве типа хранилища. При создании PVC, указывающего на этот класс, Kubernetes автоматически провиженит новый PV на AWS EBS с указанными параметрами.

Контейнерный интерфейс хранения (CSI)

CSI использует набор gRPC сервисов для взаимодействия с Kubernetes, и каждый драйвер CSI должен реализовать, как минимум, сервисы Identity и Node. Сервис Identity позволяет компонентам Kubernetes и контейнерам CSI sidecar идентифицировать драйвер и его поддерживаемые функциональные возможности. Сервис Node необходим для обеспечения доступности тома по указанному пути и для определения, какие доп. функциональные возможности поддерживает драйвера.

Для взаимодействия с Kubernetes, драйверы CSI должны регистрироваться с помощью механизма регистрации плагинов kubelet на каждой поддерживаемой ноде. Это обеспечивает коммуникацию между kubelet и драйвером CSI через Unix Domain Socket для монтирования и размонтирования томов. Основные компоненты Kubernetes не взаимодействуют напрямую с драйверами CSI. Вместо этого, драйверы, которым необходимо выполнить операции, зависящие от API Kubernetes (например, создание тома, прикрепление тома, снимок тома и т. п.), должны отслеживать API Kubernetes и инициировать соответствующие операции CSI.

В Kubernetes для драйверов CSI используются sidecar-контейнеры, которые обеспечивают интеграцию с различными компонентами Kubernetes и выполнение специфических для CSI задач. К ним относятся:

  • External Provisioner: отслеживает объекты PersistentVolumeClaim в Kubernetes и запускает операции CreateVolume и DeleteVolume.

  • External Attacher: отслеживает объекты VolumeAttachment и запускает операции ControllerPublish и ControllerUnpublish.

  • Node-Driver Registrar: регистрирует драйвер CSI у kubelet и добавляет кастомный NodeId в метку API объекта Node Kubernetes.

К примеру создадим CSI драйвер на основе gRPC сервисов, юзая Python. Сфокусируемся на минимальной реализации с использованием двух основных сервисов: CSI Identity и Node.

Первый шаг - это определение .proto файлов, которые описывают интерфейсы gRPC, соответствующие спецификации CSI. Файлы определяют структуры данных и сервисы, которые CSI драйвер будет реализовывать.

Далее с protoc генерируем код gRPC на основе определенных .proto файлов. Так мы создадим классы и методы в Python, которые соответствуют определениям в proto файле:

protoc -I ./protos --python_out=. --grpc_python_out=. ./protos/csi.proto

Следующий шаг - это реализация сервиса Identity, который позволяет клиентам идентифицировать драйвер и его возможности:

from concurrent import futures
import grpc

import csi_pb2
import csi_pb2_grpc

class IdentityServicer(csi_pb2_grpc.IdentityServicer):
    def GetPluginInfo(self, request, context):
        return csi_pb2.GetPluginInfoResponse(
            name='my-csi-driver',
            vendor_version='1.0.0'
        )

    def GetPluginCapabilities(self, request, context):
        return csi_pb2.GetPluginCapabilitiesResponse(
            capabilities=[
                # возможности драйвера
            ]
        )

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    csi_pb2_grpc.add_IdentityServicer_to_server(IdentityServicer(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    server.wait_for_termination()

if __name__ == '__main__':
    serve()

Аналогично реализуем сервис Node, который отвечает за операции с томами на уровне ноды:

class NodeServicer(csi_pb2_grpc.NodeServicer):
    def NodePublishVolume(self, request, context):
        # монтирование тома
        pass

    def NodeUnpublishVolume(self, request, context):
        # размонтирование тома
        pass

# добавляем NodeServicer к gRPC серверу аналогично IdentityServicer

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


В завершение приглашаю вас на бесплатный урок, посетители которого узнают, как создавать и настраивать различные типы сервисов в Kubernetes: ClusterIP для внутренних связей, ExternalService для внешнего доступа, NodePort для открытия порта на уровне узла и LoadBalancer для балансировки нагрузки.