Немного теории

Ранее разбирались с деплойментами, запускали наши (stateless) приложения и демонстрировали фишки self-healing в кубернетес. В рамках этого топика разберемся томами (volumes), поймем как мы может запускать приложения с сохранением состояния. В кубернетес есть несколько разновидностей типов томов. Некоторые из типов являются универсальными и подходят для локального хранения данных, какие-то могут быть приспособлены для специфичных задач. Один pod может использовать одновременно несколько типов томов.

Тип volume может быть:

  • emptyDir - пустой каталог, который может использоваться для хранения временных файлов. Время жизни этого тома тесно связанно с жизнью пода. Все данные теряются после сметри пода.
  • hostPath - этот тип используется для монтирование каталогов из файловой системы самого хоста. Данные остаются, после завершения цикла жизни пода и могут быть переиспользованы другим подом.
  • gitRepo - том, который инициализируется при проверке содержимого git-репозитория.
  • nfs - общий nfs ресурс, монтируемый в под.
  • local - локальные смонтированные устройства на узлах.
  • cephfs, glusterfs, rdb, vsphereVolume - тип используемый, для монтирования сетевых файловых хранилищ.
  • confingMap, secret - специальны тип, используемый для предоставления поду определенных ресурсов kubernetes.
  • persistentVolume, persistentVolumeClaim - способ использования заранее и динамически резервируемые постоянные хранилища.

В рамках этой записи упор будет на постоянных томах и дополнительных абстракциях вокруг постоянных томов. Название типа - PV (PersistentVolume) постоянные тома, обусловлено способу хранение данных, то есть хранилище не привязывается сроку жизни пода. Данные не теряются, если pod умрет, перезапустится или запуститься на другой ноде. Существует многожество реализации постоянных томов, например: nfs, glusterfs, cephfs, fc, iscsi, googleDisk.

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

k8s-pv-img1.png

Теперь создание постоянных томов ложиться на плечи админа. Админ создает некий пул различного типа хранилищ, например NFS и RBD. Затем объявляет PV, отправляя дескриптор в API кубернетес. Разработчик приложения по необходимости создает запрос на резервирование постоянного тома PVC (Обычно указывается только размер требуемого диска). Кластер кубернетеса определяет PV по подходящему размеру и режиму доступа и связывает PVC c томом PV. Далее разработчик, в своем деплойменте монтирует том, ссылаясь на созданный PVC.

Что бы каждый раз администратору не создавать новые постоянные тома, в k8s придумали провижионеры (provisioners). Провижионеры это определенный софт, который позволяет работать с различными системами хранения данных и динамически резервировать тома.

Администратор разворачивает провижионер, определяет один или несколько ресурсов в StorageClass. Далее разработчик в своем запросе (pvc) ссылается на нужный StorageClass. Вместо pv, который был создан статически.

В StorageClass мы определяем тип хранилища и соответвенно указываем нужный плагин для provisioner, указываем reclaimPolicy и дополнительные параметры в зависимости от типа нашего хранилища. В дополнение, при помощи StorageClass-ов мы можем классифицировать системы хранения по типу дисков (ssd, hdd), по типу файловой системы, по QoS - уровню и уровню репликации.

k8s-pv-img2.png

И теперь разработчик создает запрос на резервирование постоянного тома (PVC), указывает размер и StorageClass. Провижионер, указанный в StorageClass, создает диск нужного размера в нашей системе хранения и возвращает его id в API кубернетеса. В кластере создается манифест PV и этот манифест прибивается к запросу PVC.

Практика

Перейдем к рассмотрению практического примера. Поднимем nginx, который будем выступать в качестве webdav сервера.

Для реализации задачи нужно подготовить парочку манифестов:

  • pv.yml - в этом манифесте опишем параметры постоянного тома
  • sc.yml - манифест с параметрами storage класса
  • pvc.yml - здесь опишем запрос на резервирование постоянного дома
  • cm.yml - конфигмап с конфигурацией для nginx.
  • deployment.yml - манифейст с описанием запускаемого нами приложения
  • svc.yml - описываем параметры сервиса
  • ing.yml - здесь описываем параметры доступа к приложения, для наших клиентов.

Создание PersistentVolume

В этом примере создадим pv, который будет располагаться на файловой системе нашего minikube-сервера. (у меня это виртуалка на kvm)

Сам манифест постоянного тома:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv1-local
spec:
  capacity:
    storage: 1Gi
  volumeMode: Filesystem
  accessModes:
  - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  storageClassName: local-storage
  local:
    path: /mnt
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - minikube

В манифесте указываем:

  • Общее параметры
    • apiVersion - версию api сервера,
    • kind - тип создаваемого объекта в кубернетес,
    • metadata/name - в поле метаданных, указываем имя для нашего постоянного тома.
  • Параметры описывающие спецификацю тома
    • capacity/storage - здесь указываем размер создаваемого раздела
    • volumeMode - этот параметр указывает, как том будет смонтирован в под. По умолчанию, в значении этого параметра определено как Filesystem. Поэтому мы можем вообще его пропустить и не указывать в манифесте. Этот параметр указывает, что во внутрь пода будет смонтирован каталог. Если же в значении указать - Block, то k8s прокинет блочный девайс во внутрь пода, без какой либо файловой системы.
    • accessMode - думаю тут понятно, указываем режим доступа к тому. В нашем случаи указано ReadWriteMany. Что разрешает чтение/записть с разных нод.
    • persistentVolumeReclaimPolicy - здесь мы указываем, что будет происходить с томом, после удаления запроса на его резервирование. В нашем случаи, мы храним том.
    • storageClassName - указывается имя storage-класса, к которому будет относиться этот том.
    • local/path - в этом поле указываем тип используемого постоянного поля. И путь к точке на файловой системе.
    • nodeAffinity - этот раздел описывает настройки, позволяющие кубернетесу определить на какой ноде находится local volume.

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

[tony@i3Arch k8s-pv-example]$ kubectl apply -f pv.yml

Создание StorageClass

Через объекты storage классов, осуществляется связвание pvc и pv. В нашем случаи в создается объект storage class, с указанием так называемой заглушки - kubernetes.io/no-provisioner

Манифест:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer

Параметры:

  • apiVersion, kind - версия api, и тип создаваемого объекта.
  • metadata/name: - имя сторадж класса, обратите внимание что pv созданный ранее будет ссылаться на этот storageclass.
  • provisioner - локальный типы разделов не поддерживают провижининг, поэтому в значении указываем заглушку.
  • volumeBindingMode - режим монтирование разделов. В нашем значении говориться, что раздел будет создан при появлении потребителя/клиента.

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

[tony@i3Arch k8s-pv-example]$ kubectl apply -f sc.yml

Создание запроса на PersistentVolume

Манифест запроса постоянного раздела:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: local-share
spec:
  storageClassName: local-storage
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi

В запросе на раздел, в параметрах указываем:

  • apiVersion, kind - обшие параметры версия k8s апи, и тип объекта.
  • metadata/name - имя запроса
  • Спецификация запроса на том:
    • storageClassName - в значениии указываем storage-класс, с которого хотим получить раздел.
    • accessModes - параметр доступа к разделу. (В моем случаи полный доступ)
    • resources/requests/storage - здесь указываем запрашиваемые нами ресурсы для тома. 1 гигабайт спейса.

Применяем:

[tony@i3Arch k8s-pv-example]$ kubectl apply -f pvc.yml

Добавляем ConfigMap

В нагшем примере будет работать один под с nginx в качестве webdav сервера. Конфигурационный файл nginx, засунем в конфигмапу.

Манифест ConfigMap:

apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-conf
data: 
  default.conf: |
    server {
      listen       80 default_server;
      server_name  _;

      default_type text/plain;

      location / {
        return 200 '$hostname\n';
      }

      location /files {
        alias /data;
        autoindex on;
        client_body_temp_path /tmp;
        dav_methods PUT DELETE MKCOL COPY MOVE;
        create_full_put_path on;
        dav_access user:rw group:rw all:r;
      }
    }

Здесь уже повторяются очевидные поля, поэтому не смысла их рассматривать повторно. Применяем конфигмапу:

[tony@i3Arch k8s-pv-example]$ kubectl apply -f cm.yml

Запускаем Deployment

На этом этапе подготовим депроймент для запуска webdav сервера.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: webdav-server
spec:
  replicas: 2
  selector:
    matchLabels:
      app: webdav-server
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: webdav-server
    spec:
      containers:
        - image: nginx:1.12
          name: nginx
          ports:
            - containerPort: 80
          readinessProbe:
            failureThreshold: 3
            httpGet:
              path: /
              port: 80
            periodSeconds: 10
            successThreshold: 1
            timeoutSeconds: 1
          livenessProbe:
            failureThreshold: 3
            httpGet:
              path: /
              port: 80
            periodSeconds: 10
            successThreshold: 1
            timeoutSeconds: 1
            initialDelaySeconds: 10
          resources:
            requests:
              cpu: 50m
              memory: 100Mi
            limits:
              cpu: 100m
              memory: 100Mi
          volumeMounts:
            - name: config
              mountPath: /etc/nginx/conf.d/
            - name: data
              mountPath: /data
      volumes:
        - name: config
          configMap:
            name: nginx-conf
        - name: data
          persistentVolumeClaim:
            claimName: local-share

Подробно деплойменты разбирали в прошлых заметках, в этоми примере хотел бы ответить раздел - volumes и voluimeMounts. В разделе volumes нашего деплоймента, я определяю два раздела. Первый раздел ссылается на configMap c конфигурационным файлом nginx. Второй volume ссылкается на запрос резервирования тома - local-share.

volumes:
  # nginx конфигурация
  - name: config
    configMap:
      name: nginx-conf
  # постоянный том
  - name: data
    persistentVolumeClaim:
      claimName: local-share

И в volumeMounts ссылаясь на эти разделы указываю путь к точке монтирования:

volumeMounts:
  - name: config
    mountPath: /etc/nginx/conf.d/
  - name: data
    mountPath: /data

Применяем деплоймент,

[tony@i3Arch k8s-pv-example]$ kubectl apply -f deployment.yml

Кубернетес посмотрит в раздел с volumes и обнаружит там запрос на создание pv c именем local-share. Обратится к этому объекту pvc, для определения с каким постоянным томом он связан. И далее примонтирует обнаруженный pv во внутрь пода.

Давайте чекним наш pvs:

[tony@i3Arch k8s-pv-example]$ kubectl get pvc local-share
NAME          STATUS   VOLUME      CAPACITY   ACCESS MODES   STORAGECLASS    AGE
local-share   Bound    pv1-local   1Gi        RWX            local-storage   14m

Как видно его текущий статус - Bound, и к нему приаттачен постоянный том pv1-local, который в свою очередь объявлен в storage-классе local-storage.

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

Ну и завершающим этапом предоставим доступ к нашему приложению для клиентов. Для начала определяем новый сервис:

apiVersion: v1
kind: Service
metadata: 
  name: webdav-svc
spec:
  selector:
    app: webdav-server
  ports:
    - port: 80
      targetPort: 80
  type: ClusterIP

Думаю тут тоже все понятно, применяем манифест:

[tony@i3Arch k8s-pv-example]$ kubectl apply -f svc.yml

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

[tony@i3Arch k8s-pv-example]$ minikube addons enable ingress

Применяем манифейст ингресса,

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: 
  name: webdav-srv
spec:
  rules: 
    - host: webdav-server
      http:
        paths:
        - pathType: ImplementationSpecific
          backend: 
            service: 
              name: webdav-svc
              port:
                number: 80

Ингресс принимает запросы на хедер host: webdav-server и перенаправляет запросы на сервис webdav-svc по 80/tcp порту.

Тестим приложение

И пробуем курлой постучатся к нашему приложению (В дополнение я в /etc/hosts добавил запись с указанием ip для webdav-server).

[tony@i3Arch k8s-pv-example]$ curl webdav-server/files/
<html>
<head><title>Index of /files/</title></head>
<body bgcolor="white">
<h1>Index of /files/</h1><hr><pre><a href="../">../</a>
<a href="vda1/">vda1/</a>                                              15-Apr-2023 18:46                   -
</pre><hr></body>
</html>

Сейчас на сервере нечего нету, попробуем закинуть на него файл

[tony@i3Arch k8s-pv-example]$ curl webdav-server/files/ -T ./sc.yml

И если сейчас отобразим список файлов на сервере, то увидем наш конфиг.

[tony@i3Arch k8s-pv-example]$ curl webdav-server/files/
<html>
<head><title>Index of /files/</title></head>
<body bgcolor="white">
<h1>Index of /files/</h1><hr><pre><a href="../">../</a>
<a href="vda1/">vda1/</a>                                              15-Apr-2023 18:46                   -
<a href="sc.yml">sc.yml</a>                                             16-Apr-2023 14:25                 162
</pre><hr></body>
</html>

Подключившись к виртуалке с minikube, и залистив калатог с примонтированной папкой, тоже увидим файлик:

## Подключаюсь к виртуалке
[tony@i3Arch k8s-pv-example]$ minikube ssh

## Вывожу содержимое /mnt/
$ ls -lah /mnt/
total 8.0K
drwxrwxrwx  3 root root   80 Apr 16 14:25 .
drwxr-xr-x 19 root root  540 Apr 15 18:46 ..
-rw-r--r--  1  101  101  162 Apr 16 14:25 sc.yml
drwxr-xr-x  7 root root 4.0K Apr 12 18:42 vda1

В конце, хочу залистить схему взаимосвязи всех объектов в этом примере (без Ingress/Service), для понимания и создания общей картинки в голове. k8s-pv-yml-schemes.png

Дополнительные ссылки