Немного теории
Ранее разбирались с деплойментами, запускали наши (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) запрос на постоянный том. Данная сущность освобождает от необходимости понимания специфики инфраструктуры, и делает наше приложение переносимым на любые облачные или локальные датасторы.
Теперь создание постоянных томов ложиться на плечи админа. Админ создает некий пул различного типа хранилищ, например NFS и RBD. Затем объявляет PV, отправляя дескриптор в API кубернетес. Разработчик приложения по необходимости создает запрос на резервирование постоянного тома PVC (Обычно указывается только размер требуемого диска). Кластер кубернетеса определяет PV по подходящему размеру и режиму доступа и связывает PVC c томом PV. Далее разработчик, в своем деплойменте монтирует том, ссылаясь на созданный PVC.
Что бы каждый раз администратору не создавать новые постоянные тома, в k8s придумали провижионеры (provisioners). Провижионеры это определенный софт, который позволяет работать с различными системами хранения данных и динамически резервировать тома.
Администратор разворачивает провижионер, определяет один или несколько ресурсов в StorageClass. Далее разработчик в своем запросе (pvc) ссылается на нужный StorageClass. Вместо pv, который был создан статически.
В StorageClass мы определяем тип хранилища и соответвенно указываем нужный плагин для provisioner, указываем reclaimPolicy
и дополнительные параметры в зависимости от типа нашего хранилища. В дополнение, при помощи StorageClass-ов мы можем классифицировать системы хранения по типу дисков (ssd, hdd), по типу файловой системы, по QoS - уровню и уровню репликации.
И теперь разработчик создает запрос на резервирование постоянного тома (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), для понимания и создания общей картинки в голове.