Диски и хранилище
Контейнеры смертны: пересоздался под — и всё, что было внутри его файловой системы, пропало. Поэтому данные, которые жалко терять (файлы, базы, конфиги), хранят на постоянных томах. Разберёмся, как это устроено, и настроим два домашних варианта: локальные диски и NFS на домашнем NAS.
PV и PVC простыми словами
В Kubernetes хранилище описывается тремя объектами, и важно не путать их роли:
- PersistentVolume (PV) — «кусок диска» в кластере: конкретное место, где лежат данные (каталог на диске узла, шара на NFS-сервере). Это как склад.
- PersistentVolumeClaim (PVC) — «заявка на диск» от приложения: «дайте мне 10 ГБ». Приложение не знает и не хочет знать, где физически лежат данные. Это как договор аренды склада.
- StorageClass — «способ изготовления PV»: описывает, кто и как создаёт тома автоматически, когда появляется заявка. Это как управляющий складом.
Связка работает так: вы создаёте PVC с запросом «10 ГБ» → StorageClass автоматически создаёт под него PV → PVC «привязывается» (Bound) к этому PV → под монтирует PVC как каталог. Проверить состояние можно так:
kubectl get pv # тома в кластере
kubectl get pvc # заявки; STATUS должен быть Bound
kubectl get sc # классы хранения; (default) — класс по умолчанию
local-path-provisioner в k3s
Если вы ставили k3s — у вас уже есть работающий StorageClass по умолчанию: local-path. Он создаёт тома в каталоге /var/lib/rancher/k3s/storage/ на узле, где работает под.
Шаг 1. Проверьте, что provisioner на месте
kubectl get sc
# NAME PROVISIONER
# local-path (default) rancher.io/local-path
kubectl get pods -n kube-system | grep local-path
Если у вас kubeadm — поставьте тот же provisioner вручную (он не привязан к k3s):
kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.30/deploy/local-path-storage.yaml
Шаг 2. Создайте PVC
Файл pvc-example.yaml:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data
spec:
accessModes:
- ReadWriteOnce # том может быть примонтирован одним узлом
resources:
requests:
storage: 5Gi
storageClassName: local-path # можно опустить — это класс по умолчанию
kubectl apply -f pvc-example.yaml
kubectl get pvc app-data
# STATUS: Bound — том создан и привязан
Шаг 3. Примонтируйте PVC в под
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-with-storage
spec:
replicas: 1
selector:
matchLabels:
app: app-with-storage
template:
metadata:
labels:
app: app-with-storage
spec:
containers:
- name: app
image: nginx
volumeMounts:
- name: data
mountPath: /usr/share/nginx/html # куда монтировать в контейнере
volumes:
- name: data
persistentVolumeClaim:
claimName: app-data # ссылка на нашу заявку
Всё, что контейнер запишет в /usr/share/nginx/html, окажется в каталоге на диске узла и переживёт пересоздание пода.
Режимы доступа (accessModes)
| Режим | Значение | Где доступен |
|---|---|---|
ReadWriteOnce (RWO) | Том монтируется для записи на одном узле | local-path, большинство дисковых типов |
ReadWriteMany (RWX) | Запись с нескольких узлов одновременно | NFS — главный козырь сетевого хранилища |
ReadOnlyMany (ROX) | Чтение с нескольких узлов | NFS и др.; редко нужен дома |
NFS-сервер на Debian / домашнем NAS
NFS — классический способ расшарить каталог по сети. Если у вас есть домашний NAS (Synology, TrueNAS, OpenMediaVault), NFS-шару можно создать в его веб-интерфейсе. Если NAS нет — поднимем NFS-сервер прямо на одном из узлов или на отдельной машине с большими дисками.
Шаг 4. Установите NFS-сервер (Debian/Ubuntu)
sudo apt update
sudo apt install -y nfs-kernel-server
Шаг 5. Создайте каталог для шары
# например, на большом диске с данными
sudo mkdir -p /srv/nfs/k8s
# nobody:nogroup — поды часто работают от произвольных UID
sudo chown -R nobody:nogroup /srv/nfs/k8s
sudo chmod 777 /srv/nfs/k8s
Шаг 6. Опубликуйте шару
Откройте файл экспортов:
sudo nano /etc/exports
Добавьте строку (здесь 192.168.1.0/24 — ваша домашняя подсеть):
/srv/nfs/k8s 192.168.1.0/24(rw,sync,no_subtree_check,no_root_squash)
Что означают опции:
rw— чтение и запись;sync— писать на диск сразу (надёжнее, чуть медленнее);no_subtree_check— стандартная рекомендация для выделенных каталогов;no_root_squash— root внутри пода остаётся root на шаре. Для домашней сети приемлемо и сильно упрощает жизнь с правами; в недоверенной сети так делать нельзя.
Примените и проверьте:
sudo exportfs -ra
sudo exportfs -v
# должна быть видна наша шара
Шаг 7. Проверьте доступность с узлов кластера
На каждом узле кластера нужен клиентский пакет NFS:
# на каждом узле кластера
sudo apt install -y nfs-common
# проверка: видно ли экспорты сервера (192.168.1.60 — адрес NFS-сервера)
showmount -e 192.168.1.60
# тестовое монтирование
sudo mkdir -p /mnt/nfs-test
sudo mount -t nfs 192.168.1.60:/srv/nfs/k8s /mnt/nfs-test
touch /mnt/nfs-test/proverka.txt && ls -l /mnt/nfs-test
sudo umount /mnt/nfs-test
nfs-subdir-provisioner
Можно создавать PV для NFS вручную (ниже), но удобнее поставить автоматический provisioner — он будет сам создавать подкаталог на шаре под каждую заявку PVC, прямо как local-path делает на локальном диске.
Шаг 8. Установите provisioner через Helm
Если Helm ещё не установлен (нужен один раз):
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
Ставим provisioner (подставьте адрес и путь вашего NFS-сервера):
helm repo add nfs-subdir-external-provisioner \
https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/
helm install nfs-provisioner \
nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \
--set nfs.server=192.168.1.60 \
--set nfs.path=/srv/nfs/k8s
Шаг 9. Сделайте его классом по умолчанию (по желанию)
# снять default с local-path
kubectl patch sc local-path -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'
# назначить default на nfs-client
kubectl patch sc nfs-client -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
Шаг 10. Проверьте работу
# PVC без указания storageClassName — возьмётся default
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-test
spec:
accessModes: [ReadWriteMany]
resources:
requests:
storage: 1Gi
EOF
kubectl get pvc nfs-test
# STATUS: Bound
# на NFS-сервере появился подкаталог:
ls /srv/nfs/k8s/
# default-nfs-test-pvc-xxxx...
Ручные PV для NFS
Иногда автоматика не нужна: вы хотите привязать PVC к конкретному существующему каталогу (например, к уже заполненной медиатеке на NAS). Тогда PV описывается вручную.
Шаг 11. Создайте PV на существующий каталог
apiVersion: v1
kind: PersistentVolume
metadata:
name: media-library
spec:
capacity:
storage: 500Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain # при удалении PVC данные сохранить
nfs:
server: 192.168.1.60
path: /srv/nfs/k8s/media
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: media-library
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 500Gi
volumeName: media-library # жёсткая привязка к нашему PV
storageClassName: "" # пустая строка: не использовать provisioner
Retain означает: при удалении PVC данные на NFS останутся нетронутыми (PV перейдёт в Released). Политика Delete у provisioner'ов удаляет данные вместе с заявкой — удобно для временных вещей, опасно для медиатеки.
Права доступа на томах
Самая частая проблема при первом монтировании тома: контейнер не может писать в примонтированный каталог — Permission denied. Причина проста: каталог на диске принадлежит root (или nobody на NFS), а процесс в контейнере работает от другого UID.
Три способа решить:
- fsGroup в манифесте пода. Kubernetes рекурсивно сменит группу каталога тома, и процесс с этой группой получит доступ:
spec: securityContext: fsGroup: 1000 # группа, которой будет доступен том - Подготовить каталог вручную на стороне хранилища:
sudo chown -R 1000:1000 /srv/nfs/k8s/media— UID/GID подобрать под конкретный образ (у Jellyfin это 1000, у многих официальных образов — тоже). - Запускать контейнер от root (
runAsUser: 0) — работает всегда, но это плохая практика безопасности; используйте только если образ иначе не умеет.
USER или запустите временный контейнер: docker run --rm имя-образа id — команда покажет uid/gid процесса.
Резервное копирование томов
PVC — это не бэкап. Том защищает данные от пересоздания пода, но не от удаления, ошибок приложения и смерти диска. Минимальный рабочий подход для дома:
- NFS-тома бэкапятся обычным копированием каталогов на стороне сервера:
rsync -a --delete /srv/nfs/k8s/ /mnt/backup-disk/nfs/по cron. - local-path тома — rsync каталога
/var/lib/rancher/k3s/storage/на NFS-шару. - Базы данных перед файловым копированием должны сделать штатный дамп (pg_dump, mariadb-dump) — иначе файл БД может оказаться битым снимком.
Подробнее про бэкапы кластера целиком (включая etcd) — на странице «Советы».
Что выбрать
| Сценарий | Рекомендуемое хранилище |
|---|---|
| Одноузловой кластер, любые приложения | local-path — просто и быстро (локальный SSD) |
| Несколько узлов, приложение должно пережить падение узла | NFS (provisioner) — том доступен с любого узла |
| Медиатека, общие файлы, бэкапы | NFS с ручными PV — полный контроль над каталогами |
| Базы данных с высокой нагрузкой | local-path на SSD (NFS заметно медленнее и капризнее для БД) |
Шаг 12. Финальная проверка
# общая картина хранилища кластера
kubectl get sc
kubectl get pv
kubectl get pvc -A
# место на дисках узлов
df -h /var/lib/rancher/k3s/storage/ # local-path в k3s
df -h /srv/nfs/k8s # на NFS-сервере