Learning Platform
Глоссарий Troubleshooting
Урок 08.04 · 22 мин
Средний
PVC mountsubPathsubPathExprfsGroupreadOnlyDeployment vs StatefulSet

Использование 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:

  1. Видит volume persistentVolumeClaim → ищет PVC web-data в namespace
  2. Из PVC узнаёт привязанный PV
  3. Через CSI вызывает NodeStageVolume (mount на node global path) и NodePublishVolume (bind mount в Pod-овую директорию)
  4. Контейнер видит /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 (= subdir data в PV) и /var/logs (= subdir logs)
  • Backup agent видит /backup-source (= тот же subdir data, что и app)

Это удобно когда один PVC организован как несколько подкаталогов: один для data, один для logs, один для temp.

subPath: один PV, разные точки monitoring
PV: /mnt/pv-dataФизический volume mount-нут на node в /var/lib/kubelet/.../volumes/. Внутри есть структура каталогов, которую создаёт само приложение или admin.
bind mount через subPath
App: /var/data ← subPath: dataКонтейнер app видит /var/data — это subdir 'data' физического volume. Изоляция от других subdirs.
App: /var/logs ← subPath: logsТот же контейнер, второй mount: /var/logs — subdir 'logs'. Разделение data/logs внутри одного PVC.
Backup: /backup-source ← subPath: dataBackup-агент в sidecar монтирует тот же subdir 'data' как /backup-source. Видит то же, что app пишет.

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 интеграций, прототипов.

Deployment vs StatefulSet с persistent storage
Deployment + 1 PVC + RWOВсе replicas пытаются использовать одного PVC. RWO позволяет mount только с одной node. На разных nodes — Multi-Attach error. Антипаттерн.
Deployment + 1 PVC + RWXВсе replicas mount общий volume через сеть (NFS/EFS). Работает, но concurrent writes могут конфликтовать. Хорошо для read-heavy / static assets.
StatefulSet + volumeClaimTemplatesКаждый Pod получает свой PVC. Имена: vct-name-sts-name-ordinal. Изоляция данных. Корректная модель для БД, брокеров, кластерного software.
Deployment + replicas=1 + RWOПростой случай: один Pod, один PVC. Storage переживает рестарт Pod-а, но HA нет. OK для small/medium loads, дев-инстансов.

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

Проверка знанийKnowledge check
Разработчик создал Deployment с replicas: 3 и spec.volumes.persistentVolumeClaim.claimName: shared-data (PVC с ReadWriteOnce на AWS EBS). После деплоя один Pod в Running, два других застряли в ContainerCreating с Multi-Attach error. Почему так и какие три варианта решения?
ОтветAnswer
Причина: ReadWriteOnce на block storage (EBS, GCP PD, Azure Disk) — это per-node mount. EBS volume может быть attached только к одному EC2 instance в каждый момент. Когда первый Pod успешно mount-ит volume на своей node, два других попадают на другие nodes — там volume не может быть attached, kubelet рапортует Multi-Attach error, Pods остаются в ContainerCreating. (Если бы все 3 Pod-а оказались на одной node — все бы mount-нулись, но это случайность scheduling и плюс возникла бы race condition при concurrent write в одну директорию.) Три варианта решения: (1) ReadWriteMany storage: использовать NFS, AWS EFS, CephFS или Azure Files — там разрешен concurrent mount с нескольких nodes. Подходит для read-heavy workloads (shared assets), но concurrent writes требуют application-level coordination. (2) StatefulSet с volumeClaimTemplates: вместо Deployment использовать StatefulSet, который создаёт отдельный PVC на каждый Pod (data-app-0, data-app-1, data-app-2). Каждая replica имеет свой private RWO storage — никаких конфликтов. Это корректная модель для stateful workloads (БД, брокеры). (3) Deployment с replicas: 1: если HA не требуется, оставить одну replica с RWO PVC — это валидно для дев-окружений, low-traffic интеграций, прототипов. Storage переживёт рестарт Pod-а, но нет горизонтального масштабирования. Выбор зависит от профиля приложения: shared read → RWX; per-Pod state → StatefulSet; singleton service → replicas:1.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. Deployment с replicas: 3, PVC shared-data (ReadWriteOnce на EBS) указан в volumes. После apply один Pod Running, два других в ContainerCreating с Multi-Attach error. Какой fix наиболее correct для типичного stateful workload (БД)?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5