Использование PVC в Pod
PVC создан, к нему привязан PV — теперь нужно смонтировать его в Pod, чтобы приложение увидело файлы. Механизм такой же, как и для эфемерных volumes, но с дополнительными возможностями: subPath для деления одного volume между контейнерами, fsGroup для permission management, readOnly mount.
И есть классическая ловушка: PVC в Deployment не работает так, как кажется новичкам. Когда replicas > 1 и storage RWO — только один Pod сможет mount-нуться. Для per-replica storage есть StatefulSet с volumeClaimTemplates.
В этом уроке — практика mount PVC, типичные паттерны и pitfalls.
Init scripts и миграции: Postgres в Docker
Базовый mount
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
containers:
- name: app
image: nginx:1.27
volumeMounts:
- name: data
mountPath: /usr/share/nginx/html
volumes:
- name: data
persistentVolumeClaim:
claimName: web-data
Два уровня:
spec.volumes[]— объявление volume на уровне Pod. Здесь указываетсяpersistentVolumeClaim.claimNameс именем PVC из того же namespace.spec.containers[].volumeMounts[]— mount этого volume в конкретный контейнер на указанный путь.
После создания Pod kubelet:
- Видит volume
persistentVolumeClaim→ ищет PVCweb-dataв namespace - Из PVC узнаёт привязанный PV
- Через CSI вызывает NodeStageVolume (mount на node global path) и NodePublishVolume (bind mount в Pod-овую директорию)
- Контейнер видит
/usr/share/nginx/htmlс содержимым volume
kubectl exec web -- ls /usr/share/nginx/html
# index.html
# images/
subPath: один volume → разные mount points
Часто хочется один PVC разделить между несколькими контейнерами или разными mountPaths внутри одного контейнера. Тут на помощь приходит subPath.
containers:
- name: app
image: my-app:1.0
volumeMounts:
- name: storage
mountPath: /var/data
subPath: data
- name: storage
mountPath: /var/logs
subPath: logs
- name: backup-agent
image: backup:1.0
volumeMounts:
- name: storage
mountPath: /backup-source
subPath: data
volumes:
- name: storage
persistentVolumeClaim:
claimName: shared-storage
В этом примере один PVC shared-storage подключён к двум контейнерам, но в разных режимах:
- App container видит
/var/data(= subdirdataв PV) и/var/logs(= subdirlogs) - Backup agent видит
/backup-source(= тот же subdirdata, что и app)
Это удобно когда один PVC организован как несколько подкаталогов: один для data, один для logs, один для temp.
subPath для ConfigMap/Secret
Особый случай: смонтировать только один файл из ConfigMap в существующую директорию, не overwrite её.
volumes:
- name: config
configMap:
name: nginx-config
containers:
- name: nginx
volumeMounts:
- name: config
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
Без subPath mount /etc/nginx/nginx.conf overwrite весь /etc/nginx директорию из ConfigMap (скрыв default include-ы). С subPath: nginx.conf mount-ится только один файл, остальные файлы /etc/nginx/ остаются.
Тонкость: при использовании subPath для ConfigMap/Secret kubelet не обновляет файл при изменении CM/Secret (в отличие от обычного mount без subPath). Это by design — subPath это bind mount конкретного файла, без watch.
subPathExpr: динамический subPath
С v1.17 GA — subPathExpr позволяет использовать env vars из downwardAPI в subPath:
containers:
- name: app
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
volumeMounts:
- name: data
mountPath: /var/data
subPathExpr: $(POD_NAME)
volumes:
- name: data
persistentVolumeClaim:
claimName: shared
Каждый Pod получает свою директорию в volume: pod-1 пишет в <pv>/pod-1, pod-2 — в <pv>/pod-2. Полезно для DaemonSet, где много Pod-ов share один RWX volume, но нужна логическая изоляция.
Альтернатива (для большинства stateful workloads) — StatefulSet с volumeClaimTemplates, где у каждого Pod-а свой собственный PVC.
fsGroup: permission management
Когда volume монтируется в Pod, файлы в нём имеют owner/permissions, заложенные при создании. Если приложение в контейнере работает не от root (runAsUser: 1000) — оно может не иметь прав записи в volume.
securityContext.fsGroup решает это:
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
securityContext:
runAsUser: 1000
fsGroup: 2000
containers:
- name: app
image: my-app:1.0
volumeMounts:
- name: data
mountPath: /var/data
volumes:
- name: data
persistentVolumeClaim:
claimName: app-data
При mount kubelet делает chown -R :2000 /var/data && chmod g+rwX /var/data (точное поведение зависит от fsGroupChangePolicy). Процесс runAsUser: 1000 имеет supplementary group 2000 → может писать в volume.
fsGroupChangePolicy
Для больших volumes recursive chown медленный (миллионы файлов = минуты). С v1.20+ можно задать политику:
securityContext:
fsGroup: 2000
fsGroupChangePolicy: OnRootMismatch
Always(default) — каждый раз chown всех файловOnRootMismatch— chown только если root volume не имеет правильного group; не trigger recursive change если уже OK
Это значительно ускоряет старт Pod-а на больших volumes.
readOnly mount
volumeMounts:
- name: data
mountPath: /etc/secrets
readOnly: true
Даже если PV в RW режиме, для конкретного mount можно сделать read-only. Контейнер увидит read-only filesystem; запись вернёт EROFS.
Use case: shared config/secrets, которые app не должен модифицировать. Или общий dataset для нескольких контейнеров, где только один пишет, остальные читают.
PVC в Deployment: ловушка RWO
Самая частая ошибка — использовать PVC в Deployment с replicas > 1:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3
template:
spec:
containers:
- name: app
volumeMounts:
- name: data
mountPath: /var/data
volumes:
- name: data
persistentVolumeClaim:
claimName: web-data # ← ВСЕ replicas ссылаются на один PVC
Что произойдёт:
- PVC
web-dataимеет, скажем,ReadWriteOnce(типично для EBS/PD) - Все 3 replicas Deployment-а пытаются mount этот PVC
- Если 3 Pod-а попали на одну node — все 3 успешно mount-ятся (RWO = одна node), и все 3 пишут в одну директорию (конкуренция, испорченные данные)
- Если на разные nodes — два из трёх Pod-ов застрянут в
ContainerCreatingсMulti-Attach error
Это не подходящая модель для большинства случаев. Варианты решения:
1. RWX storage
Если backend поддерживает (NFS, CephFS, EFS, Azure Files) — используйте ReadWriteMany:
spec:
accessModes: [ReadWriteMany]
Все replicas mount-ят на свои nodes, у каждой свой view. Хорошо для shared assets (static files), плохо для write-heavy (concurrent write conflicts).
2. StatefulSet с volumeClaimTemplates
Для per-replica независимого storage:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: app
spec:
serviceName: app-headless
replicas: 3
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
containers:
- name: app
volumeMounts:
- name: data
mountPath: /var/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 10Gi
StatefulSet controller создаёт по PVC на каждый Pod: data-app-0, data-app-1, data-app-2. Каждый Pod имеет свой private storage. Это корректная модель для большинства stateful workloads.
3. Deployment с replicas=1
Если приложение не требует HA и работает в одной replica — Deployment с replicas: 1 и RWO PVC работает. Это OK для дев-инстансов, low-traffic интеграций, прототипов.
CKAD: типичные задания с PVC
На экзамене встречается стандартная последовательность:
Создать PVC
# Imperative (быстрее на экзамене)
kubectl create -f - <<EOF
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 5Gi
EOF
Создать Pod, использующий PVC
kubectl run app --image=nginx --dry-run=client -o yaml > pod.yaml
# Отредактировать pod.yaml — добавить volumes и volumeMounts
kubectl apply -f pod.yaml
Или сразу с volumeMounts через manifest:
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: app
image: nginx
volumeMounts:
- name: data
mountPath: /usr/share/nginx/html
volumes:
- name: data
persistentVolumeClaim:
claimName: app-data
Проверить mount
kubectl exec app -- mount | grep /usr/share/nginx/html
# /dev/xvdba on /usr/share/nginx/html type ext4 (rw,...)
kubectl exec app -- df -h /usr/share/nginx/html
# Filesystem Size Used Avail Use% Mounted on
# /dev/xvdba 5.0G 24M 5.0G 1% /usr/share/nginx/html
Расширить PVC
kubectl patch pvc app-data -p '{"spec":{"resources":{"requests":{"storage":"10Gi"}}}}'
kubectl get pvc app-data -w
# (через минуту) app-data Bound 10Gi
Удалить с сохранением данных (Retain policy)
# Посмотреть policy
kubectl get pv -o jsonpath='{.items[?(@.spec.claimRef.name=="app-data")].spec.persistentVolumeReclaimPolicy}'
# Изменить на Retain, если нужно сохранить данные
kubectl patch pv <pv-name> -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'
# Теперь удалить PVC — данные останутся
kubectl delete pvc app-data