Для работы с дисками для данных, которые должны храниться постоянно, Kubenetes предоставляет два типа ресурсов – PersistentVolume и PersistentVolumeClaim.
PersistentVolume – непосредственно система хранения, раздел на жёстком диске, например AWS EBS, подключенном к одному из AWS EC2, и с точки зрения этого кластера является таким же ресурсом как, например, Kubernetes Worker Node.
PersistentVolumeClaim – запрос от пользователя на использование такого диска, аналог Kubernetes pod – поды используют ресурсы Worker Node, а PVC – ресурсы PersistentVolume. По аналогии же с подами – поды запрашивают у рабочей ноды ЦПУ и память, а PVC – необходимый им размер диска, и тип доступа – ReadWriteOnce
, ReadOnlyMany
или ReadWriteMany
, см. AccessModes.
PersistentVolume могут быть созданы двумя способами – динамически (рекомендуемый способ), и статически.
При статичном методе сначала создаётся набор дисков, например EBS, которые затем используются кластером для PersistentVolumeClaim.
В случе, если для PersistentVolumeClaim не удалось найди подходящий PV – кластер может создать отдельный PV конкретно для этого PVC – это и будет динамическое создание PVC.
При этом в PVC должен быть задан Storage Class, и такой класс должен поддерживаться кластером.
К примеру, для AWS EKS по-умолчанию создан StorageClass
gp2:
[simterm]
$ kubectl get storageclass NAME PROVISIONER AGE gp2 (default) kubernetes.io/aws-ebs 64d
[/simterm]
Содержание
Типы дисков
Для понимания роли PersistentVolume – рассмотрим доступные системы хранения:
- Node-local хранение (
emptyDir
иhostPath
) - Cloud volumes (например,
awsElasticBlockStore
,gcePersistentDisk
иazureDiskVolume
) - File-sharing volumes, такие как Network File System
- Distributed-file systems (например, CephFS, RBD и GlusterFS)
- специальные типы разделов, такие как
PersistentVolumeClaim
,secret
иgitRepo
emptyDir
и hostPath
подключаются к подам, и хранят данные или в памяти, или на диске. Но т.к. они зависимы от подов, их содержимое доступно только пока работает контейнер, и когда контейнере останавливается – данные удаляются.
Cloud volumes, NFS и PersistentVolume разделы независимы от подов, и располагаются вне их.
Создание PersistentVolumeClaim
Static PersistentVolume provisioning
Создание EBS
При Static provisioning – сначала создаётся AWS EBS, и затем объект PersistentVolume, которому явно указывается использование этого EBS.
Создаём EBS:
[simterm]
$ aws ec2 --profile arseniy --region us-east-2 create-volume --availability-zone us-east-2a --size 50 { "AvailabilityZone": "us-east-2a", "CreateTime": "2020-07-29T13:10:12.000Z", "Encrypted": false, "Size": 50, "SnapshotId": "", "State": "creating", "VolumeId": "vol-0928650905a2491e2", "Iops": 150, "Tags": [], "VolumeType": "gp2" }
[/simterm]
Сохраняем его ID – “vol-0928650905a2491e2”.
Создание PersistentVolume
Описываем манифест, назовём его pv-static.yaml
:
apiVersion: v1 kind: PersistentVolume metadata: name: pv-static spec: capacity: storage: 5Gi accessModes: - ReadWriteOnce storageClassName: gp2 awsElasticBlockStore: fsType: ext4 volumeID: vol-0928650905a2491e2
Тут:
capacity
: размер дискаaccessModes
: типа доступа, тутReadWriteOnce
– раздел может быть смонтирован только к одной рабочей ноде с правами чтения/записиstorageClassName
: тип хранилища, см. нижеawsElasticBlockStore
: тип используемого дискаfsType
: тип файловой системыvolumeID
: ID AWS EBS диска
Создаём PersistentVolume:
[simterm]
$ kubectl apply -f pv-static.yaml persistentvolume/pv-static created
[/simterm]
Проверяем:
[simterm]
$ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pv-static 5Gi RWO Retain Available 69s
[/simterm]
StorageClass
Параметр storageClassName
определяет тип хранилища.
И для PVC, и для PV должен быть задан один и тот же класс, иначе PVC не подключит PV, и STATUS такого PVC будет Pending.
Если для PVC не задан StorageClass
– будет использован дефолтный:
[simterm]
$ kubectl get storageclass -o wide NAME PROVISIONER AGE gp2 (default) kubernetes.io/aws-ebs 65d
[/simterm]
При этом если StorageClass
не указан для PV – он будет создан без указания класса, и PVC с классом по-умолчанию его не подключит, а выдаст ошибку “Cannot bind to requested volume “pvname”: storageClassName does not match“:
[simterm]
... Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning VolumeMismatch 12s (x17 over 4m2s) persistentvolume-controller Cannot bind to requested volume "pvname": storageClassName does not match ...
[/simterm]
См. документацию тут>>> и тут>>>.
Создание PersistentVolumeClaim
Добавляем PersistentVolumeClaim, который будет использовать этот PV:
kind: PersistentVolumeClaim apiVersion: v1 metadata: name: pvc-static spec: accessModes: - ReadWriteOnce resources: requests: storage: 5Gi volumeName: pv-static
Создаём его:
[simterm]
$ kubectl apply -f pvc-static.yaml persistentvolumeclaim/pvc-static created
[/simterm]
Проверяем:
[simterm]
$ kubectl get pvc pvc-static NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE pvc-static Bound pv-static 5Gi RWO gp2 31s
[/simterm]
Dynamic PersistentVolume provisioning
Динамическое создание PersistentVolume аналогично статическому с той разницей, что мы не создаём EBS, и не создаём отдельного ресурса PersistentVolume – вместо этого мы опишем PersistentVolumeClaim, который самостоятельно создаст EBS, и смонтирует его к EC2 WorkerNode:
kind: PersistentVolumeClaim apiVersion: v1 metadata: name: pvc-dynamic spec: accessModes: - ReadWriteOnce resources: requests: storage: 5Gi
Создаём PVC:
[simterm]
$ kubectl apply -f pvc-dynamic.yaml persistentvolumeclaim/pvc-dynamic created
[/simterm]
Проверяем:
[simterm]
$ kubectl get pvc pvc-dynamic NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE pvc-dynamic Pending gp2 45s
[/simterm]
Но почему статус Pending? Смотрим события:
[simterm]
$ kubectl describe pvc pvc-dynamic ... Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal WaitForFirstConsumer 1s (x4 over 33s) persistentvolume-controller waiting for first consumer to be created before binding Mounted By: <none>
[/simterm]
WaitForFirstConsumer
Проверим ещё раз наш дефолтный StorageClass
:
[simterm]
$ kubectl describe sc gp2 Name: gp2 IsDefaultClass: Yes ... Provisioner: kubernetes.io/aws-ebs Parameters: fsType=ext4,type=gp2 ... VolumeBindingMode: WaitForFirstConsumer Events: <none>
[/simterm]
Тут VolumeBindingMode
определяет, как именно будет создаваться PersistentVolume. При значении Immediate – PV будет создан сразу же при появлении соотвествующего PVC, а при WaitForFirstConsumer
– кластер сначала ожидает появления пода, который запросит этот PV, и затем, в зависимости от AvailbiltyZone рабочей ноды, на которой был запущен под, создёт PV.
Давайте создадим поды, которые будут использовать эти PVC.
Использование PersistentVolumeClaim в Pod
Dynamic PersistentVolumeClaim
Опишем под, который будет использовать наш dynamic PVC:
apiVersion: v1 kind: Pod metadata: name: pv-dynamic-pod spec: volumes: - name: pv-dynamic-storage persistentVolumeClaim: claimName: pvc-dynamic containers: - name: pv-dynamic-container image: nginx ports: - containerPort: 80 name: "nginx" volumeMounts: - mountPath: "/usr/share/nginx/html" name: pv-dynamic-storage
Тут:
volumes
:persistentVolumeClaim
:claimName
: указываем имя PVC, который будет запрошен создаваемым подом для создания раздела
containers
:volumeMounts
: монтируем volume с именем pv-dynamic-storage в каталог/usr/share/nginx/html
Создаём под:
[simterm]
$ kubectl apply -f pv-pods.yaml pod/pv-dynamic-pod created
[/simterm]
Проверяем ещё раз наш PVC:
[simterm]
$ kubectl get pvc pvc-dynamic NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE pvc-dynamic Bound pvc-6d024b40-a239-4c35-8694-f060bd117053 5Gi RWO gp2 21h
[/simterm]
Теперь у нас появился созданный Volume с ID pvc-6d024b40-a239-4c35-8694-f060bd117053 – проверяем его:
[simterm]
$ kubectl describe pvc pvc-dynamic Name: pvc-dynamic Namespace: default StorageClass: gp2 Status: Bound Volume: pvc-6d024b40-a239-4c35-8694-f060bd117053 ... Finalizers: [kubernetes.io/pvc-protection] Capacity: 5Gi Access Modes: RWO VolumeMode: Filesystem Events: <none> Mounted By: pv-dynamic-pod
[/simterm]
Проверим раздел:
[simterm]
$ kubectl describe pv pvc-6d024b40-a239-4c35-8694-f060bd117053 Name: pvc-6d024b40-a239-4c35-8694-f060bd117053 ... StorageClass: gp2 Status: Bound Claim: default/pvc-dynamic Reclaim Policy: Delete Access Modes: RWO VolumeMode: Filesystem Capacity: 5Gi Node Affinity: Required Terms: Term 0: failure-domain.beta.kubernetes.io/zone in [us-east-2b] failure-domain.beta.kubernetes.io/region in [us-east-2] Message: Source: Type: AWSElasticBlockStore (a Persistent Disk resource in AWS) VolumeID: aws://us-east-2b/vol-040a5e004876f1a40 FSType: ext4 Partition: 0 ReadOnly: false Events: <none>
[/simterm]
И vol-040a5e004876f1a40 в AWS:
[simterm]
$ aws ec2 --profile arseniy --region us-east-2 describe-volumes --volume-ids vol-040a5e004876f1a40 --output json { "Volumes": [ { "Attachments": [ { "AttachTime": "2020-07-30T11:08:29.000Z", "Device": "/dev/xvdcy", "InstanceId": "i-0a3225e9fe7cb7629", "State": "attached", "VolumeId": "vol-040a5e004876f1a40", "DeleteOnTermination": false } ], ...
[/simterm]
Проверим в самом поде:
[simterm]
$ kk exec -ti pv-dynamic-pod bash root@pv-dynamic-pod:/# lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT nvme0n1 259:0 0 50G 0 disk |-nvme0n1p1 259:1 0 50G 0 part /etc/hosts `-nvme0n1p128 259:2 0 1M 0 part nvme1n1 259:3 0 5G 0 disk /usr/share/nginx/html
[/simterm]
nvme1n1 – наш диск.
Запишем данные:
[simterm]
root@pv-dynamic-pod:/# echo Test > /usr/share/nginx/html/index.html
[/simterm]
Убиваем под:
[simterm]
$ kk delete pod pv-dynamic-pod pod "pv-dynamic-pod" deleted
[/simterm]
Создадим заново:
[simterm]
$ kubectl apply -f pv-pods.yaml pod/pv-dynamic-pod created
[/simterm]
Проверяем данные:
[simterm]
$ kk exec -ti pv-dynamic-pod cat /usr/share/nginx/html/index.html Test
[/simterm]
Всё на месте.
Static PersistentVolumeClaim
Теперь попробуем использовать статичный PV.
Используем тот же манифест того же PV, который использовали ранее, pv-static.yaml
:
apiVersion: v1 kind: PersistentVolume metadata: name: pv-static spec: capacity: storage: 5Gi accessModes: - ReadWriteOnce storageClassName: gp2 awsElasticBlockStore: fsType: ext4 volumeID: vol-0928650905a2491e2
И берём тот же PVC – pvc-static.yaml
:
kind: PersistentVolumeClaim apiVersion: v1 metadata: name: pvc-static spec: accessModes: - ReadWriteOnce resources: requests: storage: 5Gi volumeName: pv-static
Создаём PV:
[simterm]
$ kk apply -f pv-static.yaml persistentvolume/pv-static created
[/simterm]
Проверяем:
[simterm]
$ kk get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pv-static 5Gi RWO Retain Available gp2 58s ...
[/simterm]
Создаём PVC:
[simterm]
$ kk apply -f pvc-static.yaml persistentvolumeclaim/pvc-static created
[/simterm]
Проверяем:
[simterm]
$ kk get pvc pvc-static NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE pvc-static Bound pv-static 5Gi RWO gp2 9s
[/simterm]
STATUS Bound – значит PVC нашёл свой PV, и подключился к нему.
Pod nodeAffinity
Определяем, в какой AvailabilityZone расположен наш EBS:
[simterm]
$ aws ec2 --profile arseniy --region us-east-2 describe-volumes --volume-ids vol-0928650905a2491e2 --query '[Volumes[*].AvailabilityZone]' --output text us-east-2a
[/simterm]
us-east-2a – окей, значит, нам надо и под создавать в той же AvailabilityZone.
Пишем манифест для пода:
apiVersion: v1 kind: Pod metadata: name: pv-static-pod spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: failure-domain.beta.kubernetes.io/zone operator: In values: - us-east-2a volumes: - name: pv-static-storage persistentVolumeClaim: claimName: pvc-static containers: - name: pv-static-container image: nginx ports: - containerPort: 80 name: "nginx" volumeMounts: - mountPath: "/usr/share/nginx/html" name: pv-static-storage
В отличии от Dynamic PVC – тут через nodeAffinity
мы явно задаём поиск ноды для этого пода в зоне us-east-2a.
Создаём под:
[simterm]
$ kk apply -f pv-pod-stat.yaml pod/pv-static-pod created
[/simterm]
Проверяем события:
[simterm]
0s Normal Scheduled Pod Successfully assigned default/pv-static-pod to ip-10-3-47-58.us-east-2.compute.internal 0s Normal SuccessfulAttachVolume Pod AttachVolume.Attach succeeded for volume "pv-static" 0s Normal Pulling Pod Pulling image "nginx" 0s Normal Pulled Pod Successfully pulled image "nginx" 0s Normal Created Pod Created container pv-static-container 0s Normal Started Pod Started container pv-static-container
[/simterm]
Разделы в самом поде:
[simterm]
$ kk exec -ti pv-static-pod bash root@pv-static-pod:/# lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT nvme0n1 259:0 0 50G 0 disk |-nvme0n1p1 259:1 0 50G 0 part /etc/hosts `-nvme0n1p128 259:2 0 1M 0 part nvme1n1 259:3 0 50G 0 disk /usr/share/nginx/html
[/simterm]
nvme1n1 смонтирован, всё работает.
PersistentVolume nodeAffinity
Другой вариант – PV nodeAffinity
.
В таком случае, при создании пода, который использует этот PV – Kubernetes сначала проверит, к каким рабочим нодам этот PV можно подключить, и после запустит под на одной из доступных для этого PV ноде.
В описании пода убираем nodeAffinity
:
apiVersion: v1 kind: Pod metadata: name: pv-static-pod spec: volumes: - name: pv-static-storage persistentVolumeClaim: claimName: pvc-static containers: - name: pv-static-container image: nginx ports: - containerPort: 80 name: "nginx" volumeMounts: - mountPath: "/usr/share/nginx/html" name: pv-static-storage
И добавляем его к самому PV:
apiVersion: v1 kind: PersistentVolume metadata: name: pv-static spec: nodeAffinity: required: nodeSelectorTerms: - matchExpressions: - key: failure-domain.beta.kubernetes.io/zone operator: In values: - us-east-2a capacity: storage: 50Gi accessModes: - ReadWriteOnce storageClassName: gp2 awsElasticBlockStore: fsType: ext4 volumeID: vol-0928650905a2491e2
Создаём PV:
[simterm]
$ kk apply -f pv-static.yaml persistentvolume/pv-static created
[/simterm]
Создаём PVC – там ничего не менялось:
[simterm]
$ kk apply -f pvc-static.yaml persistentvolumeclaim/pvc-static created
[/simterm]
Создаём под:
[simterm]
$ kk apply -f pv-pod-stat.yaml pod/pv-static-pod created
[/simterm]
Проверяем логи:
[simterm]
0s Normal Scheduled Pod Successfully assigned default/pv-static-pod to ip-10-3-47-58.us-east-2.compute.internal 0s Normal SuccessfulAttachVolume Pod AttachVolume.Attach succeeded for volume "pv-static" 0s Normal Pulling Pod Pulling image "nginx" 0s Normal Pulled Pod Successfully pulled image "nginx" 0s Normal Created Pod Created container pv-static-container 0s Normal Started Pod Started container pv-static-container
[/simterm]
Удаление PersistentVolume и PersistentVolumeClaim
Когда пользователь удаляет PVC, который используется подом, этот PVC удаляется не сразу – удаление откладывается, пока не будет остановлен использующий его под.
Аналогично, при удалении PV, к которому есть binding от какого-либо PVC, этот PV тоже удаляется не сразу, до тех пор, пока существует связь между PVC и PV.
Reclaiming
Документация тут>>>.
Когда пользователь заканчивает работу с PersistentVolume, он может удалить его объект из кластера, что бы освободить ресурс AWS EBS (reclaim).
Reclaim policy для PersistentVolume указывает кластеру, что делать с овободившимся диском, и может иметь значение Retained
, Recycled
или Deleted
.
Retain
Retai
n политика позволяет выполнять ручную очистку диска.
После удаления соответвующего PersistentVolumeClaim, PersistentVolume остаётся, и отмечается как “released“, однако становится недоступен для новых PersistentVolumeClaim, т.к. содержит данные предыдущего PersistentVolumeClaim.
Для того, что бы использовать такой ресурс повторно – можно либо удалить объект PersistentVolume, при этом AWS EBS останется доступен, био вручную удалить данные с диска.
Delete
При Delete
– удаление PVC приводит к удалению и соответствующего устройсва, такого как AWS EBS, GCE PD или Azure Disk.
Разделы, созданные автоматически наследуют политику из StorageClass
, которая по умолчанию задана в Delete
.
Recycle
Устарела. Выполняет удаление с раздела обычным rm -rf
.
Пример удаление PV и PVC
Итак, у нас имеется под:
[simterm]
$ kk get pod pv-static-pod NAME READY STATUS RESTARTS AGE pv-static-pod 1/1 Running 0 19s
[/simterm]
К которому подключен PVC:
[simterm]
$ kk get pvc pvc-static NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE pvc-static Bound pv-static 50Gi RWO gp2 19h
[/simterm]
Который биндится на PV:
[simterm]
$ kk get pv pv-static NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pv-static 50Gi RWO Retain Bound default/pvc-static gp2 19h
[/simterm]
У нашего PV RECLAIM POLICY
задана в Retain – значит, после удаления PVC и PV данные должны остаться на месте.
Пробуем – запишем данные:
[simterm]
$ kk exec -ti pv-static-pod bash root@pv-static-pod:/# echo Test > /usr/share/nginx/html/test.txt root@pv-static-pod:/# cat /usr/share/nginx/html/test.txt Test
[/simterm]
Выходим, и удаляем под, затем PVC:
[simterm]
$ kubectl delete pod pv-static-pod pod "pv-static-pod" deleted $ kubectl delete pvc pvc-static persistentvolumeclaim "pvc-static" deleted
[/simterm]
Проверяем статус PV:
[simterm]
$ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pv-static 50Gi RWO Retain Released default/pvc-static gp2 25s
[/simterm]
STATUS
== Released, и сейчас мы не сможем подключить его обратно к поду через PVC.
Попробуем – создадим ещё раз PVC:
[simterm]
$ kubectl apply -f pvc-static.yaml persistentvolumeclaim/pvc-static created
[/simterm]
Под:
[simterm]
$ kubectl apply -f pv-pod-stat.yaml pod/pv-static-pod created
[/simterm]
И проверяем статус PVC:
[simterm]
$ kubectl get pvc pvc-static NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE pvc-static Pending pv-static 0 gp2 59s
[/simterm]
Статус Pending.
Снова удалим под, PVC, и теперь и сам PV:
[simterm]
$ kubectl delete -f pv-pod-stat.yaml pod "pv-static-pod" deleted $ kubectl delete -f pvc-static.yaml persistentvolumeclaim "pvc-static" deleted $ kubectl delete -f pv-static.yaml persistentvolume "pv-static" deleted
[/simterm]
Создаём заново:
[simterm]
$ kubectl apply -f pv-static.yaml persistentvolume/pv-static created $ kubectl apply -f pvc-static.yaml persistentvolumeclaim/pvc-static created $ kubectl apply -f pv-pod-stat.yaml pod/pv-static-pod created
[/simterm]
Проверяем PVC:
[simterm]
$ kubectl get pvc pvc-static NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE pvc-static Bound pv-static 50Gi RWO gp2 27s
[/simterm]
И проверим данные в поде:
[simterm]
$ kubectl exec -ti pv-static-pod cat /usr/share/nginx/html/test.txt Test
[/simterm]
Данные на месте.
Изменение Reclaim Policy для PersistentVolume
Документация тут>>>.
Сейчас наш PV имеет политику Retain
:
[simterm]
$ kubectl get pv pv-static -o jsonpath='{.spec.persistentVolumeReclaimPolicy}' Retain
[/simterm]
Патчим его – меням политику на Delete
:
[simterm]
$ kubectl patch pv pv-static -p '{"spec":{"persistentVolumeReclaimPolicy":"Delete"}}' persistentvolume/pv-static patched
[/simterm]
Проверяем:
[simterm]
$ kubectl get pv pv-static -o jsonpath='{.spec.persistentVolumeReclaimPolicy}' Delete
[/simterm]
Удаляем под и PVC:
[simterm]
$ kubectl delete -f pv-pod-stat.yaml pod "pv-static-pod" deleted $ kubectl delete -f pvc-static.yaml persistentvolumeclaim "pvc-static" deleted
[/simterm]
Проверяем PV:
[simterm]
$ kubectl get pv pv-static Error from server (NotFound): persistentvolumes "pv-static" not found
[/simterm]
И AWS EBS, который использовался для этого PV:
[simterm]
$ aws ec2 --profile arseniy --region us-east-2 describe-volumes --volume-ids vol-0928650905a2491e2 An error occurred (InvalidVolume.NotFound) when calling the DescribeVolumes operation: The volume 'vol-0928650905a2491e2' does not exist.
[/simterm]
В целом, на этом всё.
Ссылки по теме
- Topology-Aware Volume Provisioning in Kubernetes
- Using preexisting persistent disks as PersistentVolumes
- Persistent volumes with persistent disks
- Kubernetes Persistent Storage: Why, Where and How
- Stateful Containers on Kubernetes using Persistent Volume and Amazon EBS