Диски и хранилище

Контейнеры смертны: пересоздался под — и всё, что было внутри его файловой системы, пропало. Поэтому данные, которые жалко терять (файлы, базы, конфиги), хранят на постоянных томах. Разберёмся, как это устроено, и настроим два домашних варианта: локальные диски и NFS на домашнем NAS.

Содержание

PV и PVC простыми словами

В Kubernetes хранилище описывается тремя объектами, и важно не путать их роли:

Связка работает так: вы создаёте PVC с запросом «10 ГБ» → StorageClass автоматически создаёт под него PV → PVC «привязывается» (Bound) к этому PV → под монтирует PVC как каталог. Проверить состояние можно так:

kubectl get pv      # тома в кластере
kubectl get pvc     # заявки; STATUS должен быть Bound
kubectl get sc      # классы хранения; (default) — класс по умолчанию
Зачем такая сложность Разделение PV/PVC отделяет «кто предоставляет диски» от «кто их потребляет». Приложению всё равно, NFS там под капотом или локальный SSD — манифест PVC один и тот же. Хотите перенести данные на другое хранилище — меняете только StorageClass/PV, приложение не трогаете.

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, окажется в каталоге на диске узла и переживёт пересоздание пода.

Главное ограничение локальных томов Данные local-path живут на конкретном узле. Если узел умрёт — под с томом не переедет на другой узел (Kubernetes не умеет мигрировать локальные диски). Для одноузлового кластера это не проблема. Для многоузлового — либо смиряемся (просто, надёжно), либо используем NFS (ниже), либо настоящие распределённые хранилища вроде Longhorn (тяжёлые для новичка, но это следующий уровень).

Режимы доступа (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)

Что означают опции:

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

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
Reclaim Policy Политика Retain означает: при удалении PVC данные на NFS останутся нетронутыми (PV перейдёт в Released). Политика Delete у provisioner'ов удаляет данные вместе с заявкой — удобно для временных вещей, опасно для медиатеки.

Права доступа на томах

Самая частая проблема при первом монтировании тома: контейнер не может писать в примонтированный каталог — Permission denied. Причина проста: каталог на диске принадлежит root (или nobody на NFS), а процесс в контейнере работает от другого UID.

Три способа решить:

Как узнать UID образа Посмотрите в Dockerfile образа строку USER или запустите временный контейнер: docker run --rm имя-образа id — команда покажет uid/gid процесса.

Резервное копирование томов

PVC — это не бэкап. Том защищает данные от пересоздания пода, но не от удаления, ошибок приложения и смерти диска. Минимальный рабочий подход для дома:

Подробнее про бэкапы кластера целиком (включая etcd) — на странице «Советы».

Что выбрать

СценарийРекомендуемое хранилище
Одноузловой кластер, любые приложения local-path — просто и быстро (локальный SSD)
Несколько узлов, приложение должно пережить падение узла NFS (provisioner) — том доступен с любого узла
Медиатека, общие файлы, бэкапы NFS с ручными PV — полный контроль над каталогами
Базы данных с высокой нагрузкой local-path на SSD (NFS заметно медленнее и капризнее для БД)
NFS и базы данных PostgreSQL, MySQL и особенно SQLite (используется в Nextcloud, Gitea) не любят NFS: блокировки файлов по сети работают хуже, а скорость ниже. Правило домашнего кластера: файлы — на NFS, базы — на локальных SSD (local-path), и не забудьте про бэкапы (см. «Советы»).

Шаг 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-сервере
Что дальше Хранилище готово. Переходите к странице «Приложения» — задеплоим первый сервис, поставим Dashboard и соберём собственное приложение в контейнер.