k8s/roles/longhorn/README.md
2026-07-15 11:14:21 +03:00

833 lines
32 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Longhorn — стандарт постоянного хранилища в кластере
Этот документ описывает стандарт работы с Longhorn в нашем кластере:
как подключать хранилище к приложениям, добавлять диски, управлять репликами и резервными копиями.
Является эталоном для разработчиков и DevOps.
---
## Содержание
1. [Архитектура: как Longhorn работает в кластере](#1-архитектура-как-longhorn-работает-в-кластере)
2. [Два этапа деплоя: longhorn_prereqs и longhorn](#2-два-этапа-деплоя-longhorn_prereqs-и-longhorn)
3. [Конфигурация дисков](#3-конфигурация-дисков)
4. [Пример 1: PVC для одного Pod (ReadWriteOnce)](#4-пример-1-pvc-для-одного-pod-readwriteonce)
5. [Пример 2: StatefulSet с несколькими репликами](#5-пример-2-statefulset-с-несколькими-репликами)
6. [Пример 3: база данных (PostgreSQL) с Longhorn](#6-пример-3-база-данных-postgresql-с-longhorn)
7. [Пример 4: несколько компонентов с общим хранилищем](#7-пример-4-несколько-компонентов-с-общим-хранилищем)
8. [Реплики: сколько и когда менять](#8-реплики-сколько-и-когда-менять)
9. [Резервное копирование и восстановление](#9-резервное-копирование-и-восстановление)
10. [Расширение тома](#10-расширение-тома)
11. [Добавление нового worker-узла с дисками](#11-добавление-нового-worker-узла-с-дисками)
12. [Диагностика и типичные ошибки](#12-диагностика-и-типичные-ошибки)
13. [Чеклист перед использованием хранилища](#13-чеклист-перед-использованием-хранилища)
---
## 1. Архитектура: как Longhorn работает в кластере
### Схема
```
┌─────────────────────────────────────────────────────────────────┐
│ k8s-worker-01 (10.203.0.96) │
│ │
│ ┌────────────────┐ ┌────────────────┐ │
│ │ /dev/sdb │ │ /dev/sdc │ │
│ │ XFS │ │ XFS │ │
│ │ /mnt/longhorn-│ │ /mnt/longhorn-│ │
│ │ disk1 │ │ disk2 │ │
│ └───────┬────────┘ └───────┬────────┘ │
│ │ │ │
│ Longhorn Manager Pod ─────────┘ │
│ Longhorn Engine Pod (управляет репликами) │
│ Instance Manager Pod (iSCSI-таргет для kubelet) │
│ │
│ kubelet → iSCSI → Longhorn Engine → Longhorn Replica │
│ (файлы на диске) │
└─────────────────────────────────────────────────────────────────┘
PersistentVolumeClaim (ns: myapp) ──► StorageClass: longhorn
PersistentVolume
(Longhorn Volume)
Replicas: 1 (сейчас)
→ расположены на worker-нодах
```
### Компоненты
| Компонент | Назначение |
|---|---|
| **longhorn-manager** | Оркестрирует тома, реплики, бэкапы; CRD-контроллер |
| **longhorn-engine** | Процесс ввода-вывода для каждого тома |
| **instance-manager** | Управляет engine и replica инстансами |
| **longhorn-ui** | Web-интерфейс (доступен через Traefik на порту 10002) |
| **CSI driver** | Kubernetes CSI-интерфейс; обрабатывает PVC → PV |
### StorageClass
Longhorn создаёт StorageClass `longhorn` при установке. Это **StorageClass по умолчанию** в кластере.
```bash
kubectl get storageclass
# NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ...
# longhorn (default) driver.longhorn.io Delete Immediate ...
```
`ReclaimPolicy: Delete` — при удалении PVC том удаляется. Если нужно сохранить данные — делайте снапшот или бэкап перед удалением PVC.
---
## 2. Два этапа деплоя: longhorn_prereqs и longhorn
Longhorn разворачивается через два плейбука, которые нужно запускать в порядке:
```
1. setup_longhorn.yml
├── role: longhorn_prereqs (runs on: workers)
│ ├── packages.yml — iscsi-initiator-utils, nfs-utils, cryptsetup
│ ├── kernel.yml — iscsi_tcp, dm_crypt модули
│ ├── firewall.yml — NFS (2049/tcp), portmapper (111/tcp, 111/udp)
│ ├── selinux.yml — CIL-политика для iscsid (Rocky Linux 9)
│ └── disks.yml — форматирование XFS, монтирование по UUID
└── role: longhorn (runs on: manager_nodes)
├── annotate.yml — kubectl annotate node: disk config JSON
└── helm.yml — helm upgrade --install longhorn/longhorn
```
### Когда перезапускать плейбук
| Ситуация | Что запускать |
|---|---|
| Первичная установка | `setup_longhorn.yml` полностью |
| Добавление нового worker-узла | `setup_longhorn.yml` с `--limit k8s-worker-NN` для prereqs, затем полностью для annotate |
| Добавление диска к существующей ноде | `setup_longhorn.yml` (prereqs: disks.yml пропустится для существующих; annotate обновит аннотацию) |
| Обновление версии Longhorn | Изменить `longhorn_chart_version`, запустить только `longhorn` role |
| Изменение параметров Helm | Изменить переменные, запустить только `longhorn` role |
---
## 3. Конфигурация дисков
### inventory/prod/group_vars/workers.yml — глобальный дефолт
```yaml
longhorn_disks:
- device: /dev/sdb
mountpoint: /mnt/longhorn-disk1
- device: /dev/sdc
mountpoint: /mnt/longhorn-disk2
```
Это применяется ко **всем** worker-нодам.
Если у разных нод разные диски — переопределить в `hosts.yml` (host_vars).
### inventory/prod/hosts.yml — переопределение для конкретной ноды
```yaml
workers:
hosts:
k8s-worker-01:
ansible_host: 10.203.0.96
longhorn_disks: # переопределяет group_vars для этой ноды
- device: /dev/sdb
mountpoint: /mnt/longhorn-disk1
- device: /dev/nvme0n1
mountpoint: /mnt/longhorn-nvme # SSD на этой ноде
k8s-worker-02:
ansible_host: 10.203.0.X
longhorn_disks:
- device: /dev/sdb
mountpoint: /mnt/longhorn-disk1 # только один диск
```
### Что происходит с дисками при деплое
1. `filesystem` форматирует устройство в XFS (идемпотентно — пропускает уже отформатированные)
2. Создаётся директория mountpoint
3. `blkid` получает UUID устройства
4. `/etc/fstab` добавляется запись `UUID=... /mnt/... xfs defaults,nofail`
5. Диск монтируется немедленно
6. Аннотация ноды: `node.longhorn.io/default-disks-config=[{"path":"/mnt/longhorn-disk1",...}]`
7. Longhorn читает аннотацию при старте и регистрирует диски
> `nofail` в fstab критически важен: если диск недоступен при загрузке — нода загрузится, а не зависнет.
### Проверка дисков в Longhorn
```bash
# Посмотреть ноды и их диски в Longhorn
kubectl get nodes.longhorn.io -n longhorn-system
# Детали ноды
kubectl describe node.longhorn.io k8s-worker-01 -n longhorn-system
# Через UI: http://10.203.0.96:10002 → Node
```
---
## 4. Пример 1: PVC для одного Pod (ReadWriteOnce)
**Сценарий:** одиночный Pod с постоянным хранилищем (файловый кэш, локальные данные).
### PersistentVolumeClaim
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data
namespace: my-app
spec:
accessModes:
- ReadWriteOnce # один узел в режиме чтения-записи
storageClassName: longhorn
resources:
requests:
storage: 5Gi
```
### Deployment с PVC
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: my-app
spec:
replicas: 1 # ВАЖНО: ReadWriteOnce — только 1 реплика!
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: my-app
image: registry.gigacoms.info/myteam/my-app:v1.0.0
volumeMounts:
- name: data
mountPath: /app/data
volumes:
- name: data
persistentVolumeClaim:
claimName: app-data
```
> **ReadWriteOnce** означает: том примонтирован к одному узлу одновременно.
> Несколько Pod могут использовать один RWO-том, **только если все они на одном узле**.
> С `replicas: >1` и DaemonSet это создаст проблемы при планировании на разные ноды.
### Проверить что PVC создан и привязан
```bash
kubectl get pvc -n my-app
# NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
# app-data Bound pvc-abc12345 5Gi RWO longhorn 30s
# Посмотреть на том в Longhorn
kubectl get volumes.longhorn.io -n longhorn-system | grep app-data
```
---
## 5. Пример 2: StatefulSet с несколькими репликами
**Сценарий:** очередь сообщений или key-value хранилище (Redis, RabbitMQ) с несколькими репликами.
Каждая реплика StatefulSet получает **свой отдельный PVC**.
### StatefulSet с volumeClaimTemplates
```yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis
namespace: cache
spec:
serviceName: redis
replicas: 3
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7-alpine
ports:
- containerPort: 6379
command:
- redis-server
- --appendonly yes
- --dir /data
volumeMounts:
- name: data
mountPath: /data
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
volumeClaimTemplates: # Kubernetes создаёт PVC для каждой реплики
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"] # каждая реплика = свой том
storageClassName: longhorn
resources:
requests:
storage: 10Gi
```
При `replicas: 3` Kubernetes создаст три PVC:
- `data-redis-0` → 10Gi
- `data-redis-1` → 10Gi
- `data-redis-2` → 10Gi
```bash
kubectl get pvc -n cache
# NAME STATUS VOLUME CAPACITY ACCESS MODES
# data-redis-0 Bound pvc-aaa11111 10Gi RWO
# data-redis-1 Bound pvc-bbb22222 10Gi RWO
# data-redis-2 Bound pvc-ccc33333 10Gi RWO
```
> **Важно:** при уменьшении `replicas` PVC **не удаляются автоматически**. Это намеренное поведение Kubernetes — защита от потери данных. Удалять PVC вручную после проверки.
---
## 6. Пример 3: база данных (PostgreSQL) с Longhorn
**Сценарий:** продакшн PostgreSQL с выделенным Longhorn-томом.
### Объекты
```yaml
---
# PVC отдельно от StatefulSet — для явного управления жизненным циклом
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-data
namespace: myapp
annotations:
# Документировать содержимое тома для оператора
longhorn.io/description: "PostgreSQL data for myapp — created 2026-01-01"
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 50Gi
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: myapp
spec:
serviceName: postgres
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
securityContext:
fsGroup: 999 # postgres GID — Longhorn создаст том с нужными правами
containers:
- name: postgres
image: postgres:16-alpine
ports:
- containerPort: 5432
env:
- name: POSTGRES_DB
value: myapp
- name: POSTGRES_USER
value: myuser
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
- name: PGDATA
value: /var/lib/postgresql/data/pgdata # подпапка, не корень тома
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
resources:
requests:
cpu: 200m
memory: 512Mi
limits:
cpu: 1000m
memory: 2Gi
readinessProbe:
exec:
command: [pg_isready, -U, myuser, -d, myapp]
initialDelaySeconds: 10
periodSeconds: 5
volumes:
- name: data
persistentVolumeClaim:
claimName: postgres-data
```
> Переменная `PGDATA=/var/lib/postgresql/data/pgdata` — подпапка внутри mountPath.
> PostgreSQL требует пустую директорию при инициализации. Если смонтировать том прямо в `/var/lib/postgresql/data`, Longhorn создаёт там `lost+found` и PostgreSQL падает с ошибкой.
### Резервное копирование PostgreSQL
Два уровня резервирования:
1. **Longhorn snapshot** — снапшот файловой системы (быстрый, crash-consistent)
2. **pg_dump** — логический дамп (переносимый, consistent)
```bash
# Логический дамп из Pod
kubectl exec -n myapp postgres-0 -- \
pg_dump -U myuser myapp | gzip > backup-$(date +%Y%m%d).sql.gz
# Восстановление
kubectl exec -i -n myapp postgres-0 -- \
psql -U myuser myapp < backup-20260101.sql
```
---
## 7. Пример 4: несколько компонентов с общим хранилищем
**Сценарий:** несколько Pod должны читать один и тот же набор файлов (например, общий медиа-каталог между CMS и CDN-прокси).
### Проблема: ReadWriteOnce не подходит
`ReadWriteOnce` разрешает запись только с одного узла. При нескольких Pod на разных узлах — ошибка монтирования.
### Решение A: все Pod на одном узле (nodeSelector)
```yaml
# Принудительно разместить все Pod на одной ноде
spec:
nodeSelector:
kubernetes.io/hostname: k8s-worker-01
```
Тогда PVC типа `ReadWriteOnce` работает с несколькими Pod на одном узле.
### Решение B: NFS через Longhorn (ReadWriteMany)
Longhorn поддерживает `ReadWriteMany` через встроенный NFS-шлюз начиная с версии 1.5.
```yaml
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: shared-media
namespace: cms
spec:
accessModes:
- ReadWriteMany # несколько узлов одновременно
storageClassName: longhorn
resources:
requests:
storage: 100Gi
```
```yaml
---
# CMS Pod — пишет медиафайлы
apiVersion: apps/v1
kind: Deployment
metadata:
name: cms
namespace: cms
spec:
replicas: 1
template:
spec:
containers:
- name: cms
image: registry.gigacoms.info/myteam/cms:v2.0.0
volumeMounts:
- name: media
mountPath: /app/media
volumes:
- name: media
persistentVolumeClaim:
claimName: shared-media
---
# CDN-прокси Pod — читает медиафайлы с того же тома
apiVersion: apps/v1
kind: Deployment
metadata:
name: cdn-proxy
namespace: cms
spec:
replicas: 3 # несколько реплик, разные ноды — RWX позволяет
template:
spec:
containers:
- name: cdn-proxy
image: nginx:1.27-alpine
volumeMounts:
- name: media
mountPath: /usr/share/nginx/html/media
readOnly: true # прокси только читает
volumes:
- name: media
persistentVolumeClaim:
claimName: shared-media
```
> **Ограничение RWX в Longhorn:** производительность ниже, чем RWO (идёт через NFS). Для баз данных использовать только RWO. RWX — для файлов с редкой записью и частым чтением.
---
## 8. Реплики: сколько и когда менять
### Текущая конфигурация кластера
```
Кластер: 1 worker-нода (k8s-worker-01)
Реплики: 1 (longhorn_default_replica_count: 1)
```
Одна реплика означает: **данные не реплицируются**. При потере ноды — данные недоступны до восстановления узла.
### Когда увеличивать количество реплик
| Число worker-нод | Рекомендуемые реплики | Причина |
|---|---|---|
| 1 | **1** | Некуда реплицировать |
| 2 | **2** | Реплика на каждой ноде; потеря одной ноды — нет прерывания |
| 3+ | **3** | Quorum; потеря одной ноды — данные доступны |
### Как изменить количество реплик
**Глобально** (для новых томов):
```yaml
# inventory/prod/group_vars/longhorn.yml или group_vars/k8s_cluster.yml
longhorn_default_replica_count: 2 # изменить при добавлении worker-нод
```
```bash
ansible-playbook -i inventory/prod playbooks/setup_longhorn.yml
# Применит только Helm-апгрейд, не затронет prereqs и диски
```
**Для конкретного тома** (через UI или kubectl):
```bash
# Через Longhorn API
kubectl patch volume.longhorn.io pvc-abc12345 -n longhorn-system \
--type=merge -p '{"spec":{"numberOfReplicas":2}}'
```
**Для конкретного PVC через StorageClass**:
```yaml
# Создать отдельный StorageClass с другим количеством реплик
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: longhorn-replicated
provisioner: driver.longhorn.io
parameters:
numberOfReplicas: "2"
staleReplicaTimeout: "30"
dataLocality: "disabled"
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: Immediate
```
```yaml
# PVC с кастомным StorageClass
spec:
storageClassName: longhorn-replicated # вместо longhorn
```
### soft anti-affinity (при числе нод < числа реплик)
Если `numberOfReplicas: 2`, но доступна только 1 нода — Longhorn не создаст том (нет места для реплики).
Для обхода в dev-окружении включить `replicaNodeLevelSoftAntiAffinity`:
```yaml
longhorn_replica_node_soft_anti_affinity: "true" # НЕ использовать в production
```
---
## 9. Резервное копирование и восстановление
### Longhorn Snapshot (мгновенный снапшот тома)
Снапшот — point-in-time копия тома на том же диске. Защищает от случайного удаления данных, но не от потери диска.
```bash
# Создать снапшот через kubectl
kubectl create -f - <<EOF
apiVersion: longhorn.io/v1beta2
kind: Snapshot
metadata:
name: myapp-snapshot-$(date +%Y%m%d-%H%M)
namespace: longhorn-system
spec:
volume: pvc-abc12345 # имя Longhorn-тома (не PVC)
EOF
# Список снапшотов
kubectl get snapshots.longhorn.io -n longhorn-system | grep pvc-abc12345
```
Через UI: Longhorn → Volumes → <volume> → Create Snapshot.
### Backup в S3-совместимое хранилище
Для полноценного backup необходимо настроить внешнее хранилище (S3, NFS):
```yaml
# Настройка backup target через Longhorn Settings
# UI: http://10.203.0.96:10002 → Settings → Backup Target
# Значение: s3://bucket-name@region/prefix (для S3)
# nfs://server/path (для NFS)
```
```bash
# Создать backup существующего снапшота
kubectl create -f - <<EOF
apiVersion: longhorn.io/v1beta2
kind: BackupVolume
metadata:
name: pvc-abc12345
namespace: longhorn-system
EOF
```
### Восстановление из снапшота (rollback)
```bash
# Через UI: Longhorn → Volumes → <volume> → Snapshots → Revert
# Или через kubectl (том должен быть detached)
# 1. Масштабировать Deployment в 0 (отмонтировать том)
kubectl scale deployment my-app -n my-app --replicas=0
# 2. Revert через UI или API
# 3. Вернуть Deployment
kubectl scale deployment my-app -n my-app --replicas=1
```
---
## 10. Расширение тома
Longhorn поддерживает расширение тома **без остановки Pod** (online expansion).
### Расширить PVC
```bash
# Отредактировать PVC — увеличить storage (уменьшение не поддерживается)
kubectl patch pvc app-data -n my-app \
--type=merge -p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'
# Проверить прогресс
kubectl get pvc app-data -n my-app
# STATUS: пока идёт расширение, будет Bound с условием FileSystemResizePending
# После завершения
kubectl describe pvc app-data -n my-app | grep -A5 Conditions
```
Расширение на уровне файловой системы происходит автоматически при следующем монтировании Pod (или сразу, если `allowVolumeExpansion: true` в StorageClass — а у Longhorn это включено по умолчанию).
### Проверить размер тома внутри Pod
```bash
kubectl exec -n my-app $(kubectl get pod -n my-app -l app=my-app -o name) \
-- df -h /app/data
```
---
## 11. Добавление нового worker-узла с дисками
При добавлении `k8s-worker-02`:
### Шаг 1: добавить в инвентарь
```yaml
# inventory/prod/hosts.yml
workers:
hosts:
k8s-worker-01:
ansible_host: 10.203.0.96
k8s-worker-02: # новый узел
ansible_host: 10.203.0.XX
longhorn_disks: # если диски отличаются от дефолта
- device: /dev/sdb
mountpoint: /mnt/longhorn-disk1
```
### Шаг 2: запустить prereqs только для нового узла
```bash
# Подготовка ОС только для новой ноды
ansible-playbook -i inventory/prod playbooks/setup_longhorn.yml \
--limit k8s-worker-02 \
--tags longhorn_prereqs
```
### Шаг 3: запустить join нового воркера в кластер
```bash
ansible-playbook -i inventory/prod playbooks/setup_worker_plane.yml \
--limit k8s-worker-02
```
### Шаг 4: аннотировать ноду и обновить Longhorn
```bash
# Полный плейбук обновит аннотации для всех нод включая новую
ansible-playbook -i inventory/prod playbooks/setup_longhorn.yml
```
### Шаг 5: увеличить количество реплик
```yaml
# group_vars/k8s_cluster.yml или longhorn.yml
longhorn_default_replica_count: 2 # было 1
```
```bash
ansible-playbook -i inventory/prod playbooks/setup_longhorn.yml
```
---
## 12. Диагностика и типичные ошибки
### Основные команды
```bash
# Состояние всех томов
kubectl get volumes.longhorn.io -n longhorn-system
# Детали тома
kubectl describe volume.longhorn.io <volume-name> -n longhorn-system
# Состояние реплик
kubectl get replicas.longhorn.io -n longhorn-system | grep <volume-name>
# Состояние нод Longhorn
kubectl get nodes.longhorn.io -n longhorn-system
# Логи manager
kubectl logs -n longhorn-system -l app=longhorn-manager --tail=100
# Логи CSI driver
kubectl logs -n longhorn-system -l app=longhorn-csi-plugin --tail=50
# PVC не привязывается — посмотреть события
kubectl describe pvc <pvc-name> -n <namespace>
kubectl get events -n <namespace> --sort-by='.lastTimestamp'
# Посмотреть все PVC в кластере
kubectl get pvc -A
```
### Таблица ошибок
| Симптом | Причина | Решение |
|---|---|---|
| PVC в статусе `Pending` | Нет доступных дисков / нода не готова | `kubectl get nodes.longhorn.io -n longhorn-system` — проверить schedulable |
| PVC `Pending`: `no schedulable node` | Включён soft anti-affinity=false, только 1 нода, replicas=2 | Уменьшить реплики до 1 или включить soft anti-affinity |
| Pod не стартует: `volume not found` | Том деградировал или удалён | Проверить `kubectl get volumes.longhorn.io -n longhorn-system` |
| Том в статусе `Degraded` | Нода с репликой недоступна | Подождать восстановления ноды или rebuild replica |
| Том в статусе `Faulted` | Все реплики потеряны | Восстановить из бэкапа |
| `iscsiadm` ошибки в логах | SELinux блокирует iscsid | Перепроверить CIL-политику: `semodule -l \| grep local_longhorn` |
| Медленная запись | Все реплики на одном диске | Разнести реплики по дискам (Disk Tags) |
| `lost+found` в PostgreSQL data dir | PGDATA смонтирован прямо на корень тома | Установить `PGDATA=/var/lib/postgresql/data/pgdata` (подпапка) |
| Диск не виден в Longhorn | Аннотация ноды устарела | `ansible-playbook setup_longhorn.yml` — обновит annotate |
### Проверка iSCSI на ноде
```bash
# Выполнить через SSH на worker-ноде или kubectl exec
# Статус iscsid
systemctl status iscsid
# Список активных iSCSI-сессий (примонтированные тома)
iscsiadm -m session
# Загружены ли нужные модули ядра
lsmod | grep iscsi_tcp
lsmod | grep dm_crypt
```
### Принудительный detach застрявшего тома
Если Pod удалён, но том остался в статусе attached:
```bash
# Через UI: Longhorn → Volumes → <volume> → Detach
# Или через kubectl
kubectl patch volume.longhorn.io <volume-name> -n longhorn-system \
--type=merge -p '{"spec":{"nodeID":""}}'
```
---
## 13. Чеклист перед использованием хранилища
### Первое использование Longhorn в кластере
- [ ] `setup_longhorn.yml` выполнен успешно (prereqs + helm install)
- [ ] Все worker-ноды показывают статус `Ready` в UI Longhorn (`http://10.203.0.96:10002`)
- [ ] Диски видны в UI Longhorn: Node → Disks
- [ ] StorageClass `longhorn` существует: `kubectl get storageclass`
- [ ] Тестовый PVC создаётся и переходит в `Bound`: `kubectl apply -f test-pvc.yaml`
### Перед каждым новым PVC / StatefulSet
- [ ] Выбран правильный `accessMode`:
- `ReadWriteOnce` — один узел, обычные Pod и StatefulSet
- `ReadWriteMany` — несколько узлов, только через Longhorn NFS
- [ ] `storageClassName: longhorn` указан явно (не полагаться на default при нескольких StorageClass)
- [ ] Указан разумный размер тома (расширить можно, уменьшить — нет)
- [ ] Для PostgreSQL: `PGDATA` установлен в подпапку, `fsGroup: 999` в securityContext
- [ ] Для StatefulSet: использован `volumeClaimTemplates`, не `volumes + PVC`
- [ ] `resources.requests` и `limits` заданы для контейнеров
### После деплоя
- [ ] PVC в статусе `Bound`: `kubectl get pvc -n <namespace>`
- [ ] Pod в статусе `Running`: `kubectl get pods -n <namespace>`
- [ ] Том виден в Longhorn UI со статусом `Healthy`
- [ ] Данные доступны внутри Pod: `kubectl exec -n <ns> <pod> -- ls /mountpath`
- [ ] (для продакшн-данных) Настроен регулярный snapshot или backup