# Kubernetes Infrastructure
> **Корпоративный стандарт.** Ansible-развёртывание кластера Kubernetes на Rocky Linux 9.
> Локальный запуск через CLI и автоматизированный запуск через GitLab CI/CD.
---
## Содержание
- [Инфраструктура](#инфраструктура)
- [Стек технологий](#стек-технологий)
- [Структура репозитория](#структура-репозитория)
- [Роли и компоненты](#роли-и-компоненты)
- - **Стандарт работы с хранилищем** [`roles/longhorn/README.md`](roles/longhorn/README.md)
- - **Стандарт публикации сервисов** [`roles/traefik/README.md`](roles/traefik/README.md)
- - **Стандарт GitOps-деплоя приложений** [`roles/flux/README.md`](roles/flux/README.md)
- - **Стандарт объектного хранилища S3** [`roles/minio/README.md`](roles/minio/README.md)
- - **Стандарт централизованного сбора логов** [`roles/logging/README.md`](roles/logging/README.md)
- - Sealed Secrets (kubeseal)
- [CI/CD-пайплайн](#cicd-пайплайн)
- [Порядок развёртывания](#порядок-развёртывания)
- [Архитектурные решения](#архитектурные-решения)
- [Локальная разработка](#локальная-разработка)
- [Добавление нового компонента](#добавление-нового-компонента)
---
## Инфраструктура
```
┌─────────────────────────────────────────────────────────────────┐
│ Operator workstation (manager_nodes) │
│ k8s-manager-01 10.203.0.92 │
│ kubectl · helm · k9s · kubectx/kubens │
│ Kubernetes Dashboard token → ~/.kube/dashboard-token │
└────────────────────────┬────────────────────────────────────────┘
│ kubectl / helm (kubeconfig)
┌────────────────────────▼────────────────────────────────────────┐
│ Kubernetes cluster (k8s_cluster) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Control plane k8s-master-01 10.203.0.97 │ │
│ │ kube-apiserver · etcd · controller-manager · scheduler │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Worker k8s-worker-01 10.203.0.96 │ │
│ │ kubelet · kube-proxy · Longhorn disk agent │ │
│ │ Traefik DaemonSet │ │
│ │ :10001 → kubernetes-dashboard (ClusterIP :443) │ │
│ │ :10002 → longhorn-frontend (ClusterIP :80) [auth] │ │
│ │ :10003 → grafana (ClusterIP :80) │ │
│ │ :10005 → minio (ClusterIP :9000) │ │
│ │ :10006 → minio-console (ClusterIP :9001) │ │
│ │ :10103 → db/bgbilling-dev (TCP :3306) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ CNI: Flannel v0.26.1 │ Storage: Longhorn 1.7.2 │
└─────────────────────────────────────────────────────────────────┘
```
### Узлы кластера
| Hostname | IP | Группа инвентаря | Роль |
|---|---|---|---|
| k8s-manager-01 | 10.203.0.92 | `manager_nodes` | Рабочая станция оператора |
| k8s-master-01 | 10.203.0.97 | `control_plane` | API server, etcd, scheduler |
| k8s-worker-01 | 10.203.0.96 | `workers` | Вычислительный узел |
| k8s-worker-02 | 10.203.0.212 | `workers` → `gpu_workers` | GPU-узел (NVIDIA), Ollama-стек |
| k8s-worker-NN | TBD | `workers` | Дополнительные вычислительные узлы |
> `manager_nodes` — **не** Kubernetes-узел. Это рабочая станция оператора, с которой запускаются `kubectl` и `helm`.
### Сервисы и точки доступа
| Сервис | URL | Аутентификация |
|---|---|---|
| Kubernetes Dashboard | `http://10.203.0.96:10001` | токен из `~/.kube/dashboard-token` |
| Longhorn UI | `http://10.203.0.96:10002` | BasicAuth (Secret `traefik-auth-longhorn` в ns `traefik`) |
| Grafana | `http://10.203.0.96:10003` | логин Grafana (admin, пароль из Secret `grafana-admin-secret`) |
| MinIO S3 API | `http://10.203.0.96:10005` | AWS Signature v4 (встроенная аутентификация MinIO) |
| MinIO Console | `http://10.203.0.96:10006` | логин MinIO (rootUser из Secret `minio-root-credentials`) |
| MariaDB BGBilling | `10.203.0.96:10103` | TCP-проброс (без HTTP); аутентификация средствами MariaDB |
| Ollama Proxy (k8s_ai) | `http://10.203.0.96:10200` | своя аутентификация приложения |
| Open WebUI (k8s_ai) | `http://10.203.0.96:10201` | своя аутентификация приложения |
---
## Стек технологий
| Компонент | Версия | Управляется |
|---|---|---|
| ОС | Rocky Linux 9 | Ansible |
| Kubernetes | 1.33 (pkgs.k8s.io) | Ansible → `k8s_control_plane`, `k8s_worker` |
| Container runtime | containerd (Docker CE repo) | Ansible |
| CNI | Flannel v0.26.1 / Calico v3.27.0 | Ansible → `k8s_control_plane` |
| Постоянное хранилище | Longhorn 1.7.2 | Ansible → `longhorn_prereqs`, `longhorn` |
| Ingress / proxy | Traefik 32.1.0 | Ansible → `traefik` |
| Kubernetes Dashboard | v2.7.0 | Ansible → `k8s_manager` |
| Объектное хранилище | MinIO 5.4.0 (chart) | Ansible → `minio` |
| Централизованные логи | Loki 7.0.0 + Grafana Alloy 1.8.2 + Grafana 10.5.15 | Ansible → `logging` |
| Шифрование Secret'ов | Sealed Secrets 2.16.1 | Ansible → `sealed_secrets` |
| GitOps | Flux CD (latest stable) | Ansible → `flux` |
| GitLab Agent | agentk (latest stable) | Ansible → `gitlab_agent` |
| Автоматизация | Ansible (посм. requirements.yml) | — |
| CI/CD | GitLab CI/CD | `.gitlab-ci.yml` |
---
## Структура репозитория
```
.gitlab-ci.yml — CI/CD-пайплайн (стадии validate → setup)
ansible.cfg — кэширование фактов, pipelining, YAML-вывод
requirements.yml — коллекции Ansible Galaxy
inventory/prod/
hosts.yml — статический список хостов (3 группы)
group_vars/
all.yml — SSH-учётные данные из переменных окружения
k8s_cluster.yml — общие переменные для control_plane + workers
control_plane.yml — k8s_version, CIDR, CNI
workers.yml — конфигурация дисков Longhorn
manager_nodes.yml — версии инструментов, часовой пояс
traefik.yml — traefik_port_map: таблица портов и backend-сервисов
gpu_workers.yml — node_labels, override дисков Longhorn для GPU-узлов
playbooks/ — один плейбук = одна CI/CD-задача
setup_manager.yml
setup_control_plane.yml
setup_worker_plane.yml
setup_longhorn.yml
setup_traefik.yml
setup_flux.yml
setup_gitlab_agent.yml
setup_minio.yml
setup_logging.yml
setup_sealed_secrets.yml
setup_gpu.yml
roles/
k8s_manager/
k8s_control_plane/
k8s_worker/
longhorn_prereqs/
longhorn/ README.md ← стандарт работы с хранилищем
traefik/ README.md ← стандарт публикации сервисов
flux/ README.md ← стандарт GitOps-деплоя приложений
gitlab_agent/
minio/ README.md ← стандарт объектного хранилища S3
logging/ README.md ← стандарт централизованного сбора логов
sealed_secrets/
gpu_prereqs/ — nvidia-container-toolkit + containerd runtime на GPU-узле
gpu_device_plugin/ — node label + NVIDIA k8s-device-plugin с manager-ноды
gpu_model_storage/ README.md ← стандарт локального RAID1-хранилища под модели
```
---
## Роли и компоненты
### `k8s_control_plane` · `k8s_worker` · `k8s_manager`
Базовые роли развёртывания кластера. Не имеют отдельного role-README — полное описание в разделе [Архитектурные решения](#архитектурные-решения).
| Роль | Что делает | Плейбук |
|---|---|---|
| `k8s_control_plane` | containerd → kubelet → kubeadm init → CNI → kubeconfig | `setup_control_plane.yml` |
| `k8s_worker` | containerd → kubelet → kubeadm join | `setup_worker_plane.yml` |
| `k8s_manager` | kubectl / helm / k9s, Kubernetes Dashboard | `setup_manager.yml` |
Переменные k8s_control_plane
| Переменная | Значение по умолчанию | Описание |
|---|---|---|
| `k8s_version` | `1.33` | Версия Kubernetes |
| `pod_network_cidr` | `10.244.0.0/16` | Сеть подов (Flannel) |
| `service_cidr` | `10.96.0.0/12` | Сеть сервисов |
| `cni_plugin` | `flannel` | `flannel` или `calico` |
| `flannel_version` | `v0.26.1` | Версия манифеста Flannel |
| `calico_version` | `v3.27.0` | Версия манифеста Calico |
| `kubeconfig_fetch_to_managers` | `true` | Копировать kubeconfig на manager-узел |
Переменные k8s_worker
| Переменная | Значение по умолчанию | Описание |
|---|---|---|
| `k8s_version` | `1.33` | Должна совпадать с control plane |
| `worker_join_source_host` | первый хост `control_plane` | Откуда читать join-команду |
Переменные k8s_manager
| Переменная | Значение по умолчанию | Описание |
|---|---|---|
| `dashboard_insecure` | `false` | HTTP-режим Dashboard (переопределяется в `true` в group_vars) |
| `dashboard_manifest_url` | GitHub raw v2.7.0 | Без зависимости от CDN |
| `k8s_manager_upgrade_packages` | `false` | Полное обновление ОС (opt-in) |
---
### `gpu_prereqs` · `gpu_device_plugin`
Подключение GPU-узла к кластеру. Драйвер NVIDIA ставится вручную заранее — роль его не устанавливает, только проверяет наличие (`nvidia-smi`). Запускается **после** `setup_worker_plane.yml`, отдельным плейбуком `setup_gpu.yml`, только для узлов группы `gpu_workers` (подгруппа `workers`).
| Роль | Запускается на | Что делает |
|---|---|---|
| `gpu_prereqs` | `gpu_workers` | Проверка драйвера, установка `nvidia-container-toolkit`, `nvidia-ctk runtime configure` для containerd (default runtime) |
| `gpu_device_plugin` | `manager_nodes` | `kubectl label node gpu=nvidia` для каждого узла `gpu_workers`; `kubectl apply` манифеста NVIDIA k8s-device-plugin |
Переменные gpu_prereqs / gpu_device_plugin
| Переменная | Значение по умолчанию | Описание |
|---|---|---|
| `nvidia_container_toolkit_version` | `""` (последняя) | Версия пакета `nvidia-container-toolkit` |
| `node_labels` | `{gpu: nvidia}` (в `gpu_workers.yml`) | Лейблы, навешиваемые на узел после join |
| `gpu_device_plugin_version` | `v0.17.0` | Тег релиза NVIDIA k8s-device-plugin |
| `gpu_device_plugin_manifest_url` | GitHub raw для указанной версии | Статический манифест DaemonSet |
> Порядок обязателен: `setup_worker_plane.yml` перезаписывает `/etc/containerd/config.toml` с нуля, поэтому `gpu_prereqs` (донастройка containerd под nvidia runtime) должна выполняться **после**, отдельным прогоном.
---
### `gpu_model_storage`
Выделенный локальный RAID1-раздел под файлы моделей (Ollama) на GPU-узлах. `gpu_workers` исключены из Longhorn (`longhorn_disks: []`), а корневая ФС слишком маленькая (~30 ГБ) для хранения моделей — поэтому под них нарезается отдельный раздел из неразмеченного места на тех же зеркальных дисках, что и ОС. Запускается плейбуком `setup_gpu_model_storage.yml`, независимо от порядка `kubeadm join`.
Переменные gpu_model_storage
| Переменная | Значение по умолчанию | Описание |
|---|---|---|
| `gpu_model_storage_devices` | `[]` | Диски-члены RAID1 (пусто = роль выключена); задаётся в `gpu_workers.yml` |
| `gpu_model_storage_partition_size_gb` | `1000` | Размер новой партиции на каждом диске, ГБ |
| `gpu_model_storage_raid_device` | `/dev/md1` | Имя нового mdadm-массива |
| `gpu_model_storage_mountpoint` | `/mnt/ollama-models` | Точка монтирования |
| `gpu_model_storage_fstype` | `xfs` | Файловая система |
> **Standard**: [roles/gpu_model_storage/README.md](roles/gpu_model_storage/README.md)
---
### `longhorn_prereqs` · `longhorn`
Двухэтапное развёртывание распределённого блочного хранилища.
| Роль | Запускается на | Что делает |
|---|---|---|
| `longhorn_prereqs` | `workers` | iSCSI / NFS пакеты, модули ядра, SELinux CIL-политика, форматирование и монтирование дисков XFS |
| `longhorn` | `manager_nodes` | Аннотации узлов с disk-config JSON, `helm upgrade --install longhorn` |
После установки доступен StorageClass `longhorn` (default). Приложения используют его через `storageClassName: longhorn` в PVC.
Переменные
| Переменная | Значение по умолчанию | Описание |
|---|---|---|
| `longhorn_chart_version` | `1.7.2` | Версия Helm-чарта |
| `longhorn_namespace` | `longhorn-system` | Namespace |
| `longhorn_default_replica_count` | `1` | Реплик на том; поднять до 2–3 при добавлении worker-нод |
| `longhorn_minimal_available_storage_percentage` | `10` | Минимальный свободный % хранилища |
| `longhorn_storage_over_provisioning_percentage` | `100` | Коэффициент over-provisioning |
| `longhorn_disks` | `[{device: /dev/sdb, mountpoint: /mnt/longhorn-disk1}, ...]` | Диски для форматирования; переопределяется в hosts.yml |
> **Стандарт работы с хранилищем** (PVC, StatefulSet, реплики, бэкапы, диагностика):
> [`roles/longhorn/README.md`](roles/longhorn/README.md)
---
### `traefik`
HTTP/TCP port-proxy. DaemonSet на worker-нодах. Каждый сервис получает выделенный TCP-порт из диапазона 10001–10999. Вся топология определяется через `traefik_port_map` в `inventory/prod/group_vars/traefik.yml`. Поддерживаются два режима маршрутизации: HTTP (`IngressRoute`, поле `protocol` не задаётся) и raw TCP (`IngressRouteTCP` с `HostSNI("*")`, `protocol: tcp` в port_map).
| Задача | Что делает |
|---|---|
| `firewall.yml` | Открывает 10000–10999/tcp; помещает `flannel.1`, `cni0` в зону `trusted` |
| `helm.yml` | `helm upgrade --install traefik` как DaemonSet с EntryPoint-ами из port_map |
| `middleware.yml` | Применяет `Middleware` CRD (BasicAuth) для записей с `basicauth.enabled: true` |
| `routes.yml` | Применяет `IngressRoute` (HTTP) или `IngressRouteTCP` (TCP) в зависимости от `protocol` в port_map |
Переменные
| Переменная | Значение по умолчанию | Описание |
|---|---|---|
| `traefik_chart_version` | `32.1.0` | Версия Helm-чарта |
| `traefik_namespace` | `traefik` | Namespace |
| `traefik_port_map` | `[]` | Список сервисов; переопределяется в `group_vars/traefik.yml` |
> **Стандарт публикации сервисов** (добавление порта, BasicAuth, диагностика, таблица активных портов):
> [`roles/traefik/README.md`](roles/traefik/README.md)
---
### `flux`
Устанавливает Flux CD CLI и выполняет `flux bootstrap gitlab`, подключая кластер к fleet-репозиторию `k8s-fleet` на `gitlab.gigacoms.info`.
| Задача | Что делает |
|---|---|
| `install.yml` | Flux CLI → `/usr/local/bin/flux`, bash-completion, `flux check --pre` |
| `bootstrap.yml` | `flux bootstrap gitlab` — deploy key, Flux-контроллеры в `flux-system`, манифесты в `clusters/production` |
Fleet-репозиторий: `gitlab.gigacoms.info/k8s/k8s-fleet` · путь: `clusters/production`
Переменные
| Переменная | Значение по умолчанию | Описание |
|---|---|---|
| `flux_version` | `""` (latest stable) | Версия Flux CLI |
| `flux_gitlab_hostname` | `gitlab.gigacoms.info` | Хост GitLab |
| `flux_gitlab_owner` | `k8s` | GitLab-группа fleet-репозитория |
| `flux_gitlab_repository` | `k8s-fleet` | Имя fleet-репозитория |
| `flux_gitlab_branch` | `main` | Ветка |
| `flux_gitlab_path` | `clusters/production` | Путь внутри репозитория |
| `flux_gitlab_token` | из `$GITLAB_FLUX_TOKEN` | GitLab PAT (scope: api) |
> **Стандарт GitOps-деплоя приложений** (структура fleet-repo, multi-branch, примеры с Longhorn и Traefik, CI/CD-шаблон):
> [`roles/flux/README.md`](roles/flux/README.md)
---
### `minio`
S3-совместимое объектное хранилище на базе MinIO. Запускается как standalone StatefulSet на worker-ноде, хранилище предоставляется отдельным StorageClass `longhorn-minio` (reclaimPolicy: Retain). Доступ — через Traefik на портах 10005 (S3 API) и 10006 (Console UI).
| Задача | Что делает |
|---|---|
| `storageclass.yml` | Создаёт StorageClass `longhorn-minio` (Retain, numberOfReplicas=1) |
| `namespace.yml` | Создаёт namespace `minio` |
| `helm.yml` | `helm upgrade --install minio minio/minio` с values из шаблона |
**Предварительное условие:** создать Secret `minio-root-credentials` вручную в namespace `minio`:
```bash
kubectl create secret generic minio-root-credentials \
--from-literal=rootUser=minioadmin \
--from-literal=rootPassword='СИЛЬНЫЙ_ПАРОЛЬ' \
-n minio
```
Переменные
| Переменная | Значение по умолчанию | Описание |
|---|---|---|
| `minio_chart_version` | `5.4.0` | Версия Helm-чарта `minio/minio` |
| `minio_image_tag` | `RELEASE.2025-04-22T22-12-26Z` | Тег образа MinIO |
| `minio_namespace` | `minio` | Namespace |
| `minio_storage_class` | `longhorn-minio` | StorageClass для PVC |
| `minio_storage_size` | `1Ti` | Размер PVC |
| `minio_console_url` | `http://10.203.0.96:10006` | URL Console (для MINIO_BROWSER_REDIRECT_URL) |
| `minio_root_secret` | `minio-root-credentials` | Имя Secret с rootUser/rootPassword |
| `minio_buckets` | `[backups, artifacts]` | Бакеты, создаваемые при старте |
> **Стандарт объектного хранилища S3** (StorageClass, пользователи, lifecycle, интеграция с Loki, масштабирование):
> [`roles/minio/README.md`](roles/minio/README.md)
---
### `logging`
PLG-стек централизованного сбора логов: **Loki** (S3 backend → MinIO), **Grafana Alloy** (DaemonSet, замена EOL Promtail), **Grafana** (UI + алерты). Все три компонента разворачиваются в namespace `monitoring` из manager-узла через Helm.
| Задача | Что делает |
|---|---|
| `namespace.yml` | Создаёт namespace `monitoring` |
| `minio-user.yml` | Создаёт bucket `loki-chunks`, пользователя `loki` с политикой только на этот bucket, Secret `loki-minio-secret` |
| `loki.yml` | `helm upgrade --install loki` в режиме single-binary с S3 backend (MinIO) |
| `alloy.yml` | `helm upgrade --install alloy` как DaemonSet — сбор логов подов + journald на всех нодах |
| `grafana.yml` | `helm upgrade --install grafana` с provisioning datasource Loki |
**Предварительное условие:** создать Secret `grafana-admin-secret` вручную в namespace `monitoring`:
```bash
kubectl create namespace monitoring
kubectl create secret generic grafana-admin-secret \
--from-literal=admin-user=admin \
--from-literal=admin-password='СИЛЬНЫЙ_ПАРОЛЬ' \
-n monitoring
```
Переменные
| Переменная | Значение по умолчанию | Описание |
|---|---|---|
| `loki_chart_version` | `7.0.0` | Версия Helm-чарта `grafana/loki` |
| `alloy_chart_version` | `1.8.2` | Версия Helm-чарта `grafana/alloy` |
| `grafana_chart_version` | `10.5.15` | Версия Helm-чарта `grafana/grafana` |
| `loki_minio_endpoint` | `http://minio.minio.svc.cluster.local:9000` | S3 endpoint для Loki |
| `loki_minio_bucket` | `loki-chunks` | Bucket для хранения чанков и индексов |
| `loki_retention_days` | `31` | Срок хранения логов в днях |
| `loki_wal_storage_size` | `10Gi` | PVC для WAL/temp Loki (не логи — они в MinIO) |
| `grafana_storage_size` | `5Gi` | PVC для базы данных Grafana |
| `grafana_root_url` | `http://10.203.0.96:10003` | Внешний URL Grafana (Traefik) |
| `grafana_admin_secret` | `grafana-admin-secret` | Имя Secret с admin-user/admin-password |
> **Стандарт централизованного сбора логов** (архитектура PLG, масштабирование, переход на Graylog, диагностика):
> [`roles/logging/README.md`](roles/logging/README.md)
---
### `sealed_secrets`
Разворачивает [Sealed Secrets](https://github.com/bitnami-labs/sealed-secrets) — контроллер для безопасного хранения зашифрованных Secret'ов в Git. Запускается из manager-узла: контроллер — в `kube-system` через Helm, CLI `kubeseal` — на manager-узле.
| Задача | Что делает |
|---|---|
| `cli.yml` | Скачивает `kubeseal` с GitHub Releases (версия из переменной или latest), идемпотентно |
| `helm.yml` | `helm upgrade --install sealed-secrets` из `bitnami-labs/sealed-secrets` в `kube-system` |
**Использование после установки:**
```bash
# Зашифровать Secret
kubectl create secret generic my-secret --from-literal=password=mysecret --dry-run=client -o yaml \
| kubeseal --format yaml > my-sealedsecret.yaml
# Применить (можно коммитить в Git — зашифрован публичным ключом контроллера)
kubectl apply -f my-sealedsecret.yaml
```
Переменные
| Переменная | Значение по умолчанию | Описание |
|---|---|---|
| `sealed_secrets_chart_version` | `2.16.1` | Версия Helm-чарта |
| `sealed_secrets_namespace` | `kube-system` | Namespace контроллера |
| `sealed_secrets_cli_version` | `""` (latest) | Версия kubeseal CLI; пустая — берёт latest с GitHub |
---
### `gitlab_agent`
Устанавливает GitLab Agent for Kubernetes (agentk). Работает совместно с Flux: Flux деплоит, agentk даёт видимость в GitLab UI (Operate → Kubernetes clusters).
Agentk инициирует исходящее WebSocket-соединение к KAS — входящих портов не требует.
| Задача | Что делает |
|---|---|
| `config.yml` | Клонирует fleet-repo, создаёт `.gitlab/agents/production/config.yaml`, коммитит и пушит при изменении |
| `helm.yml` | `helm upgrade --install gitlab-agent` с токеном и KAS-адресом |
**Предварительное условие:** зарегистрировать агент в GitLab UI → Operate → Kubernetes clusters → Connect a cluster → имя `production` → токен в CI/CD переменную `GITLAB_AGENT_TOKEN`.
Переменные
| Переменная | Значение по умолчанию | Описание |
|---|---|---|
| `gitlab_agent_hostname` | `gitlab.gigacoms.info` | Хост GitLab |
| `gitlab_agent_name` | `production` | Имя агента (совпадает с именем в GitLab UI) |
| `gitlab_agent_namespace` | `gitlab-agent` | Namespace |
| `gitlab_agent_kas_address` | `wss://gitlab.gigacoms.info/-/kubernetes-agent/` | KAS WebSocket-адрес |
| `gitlab_agent_token` | из `$GITLAB_AGENT_TOKEN` | Токен агента |
| `gitlab_agent_ci_access_group` | `k8s` | GitLab-группа с доступом к кластеру через CI |
---
## CI/CD-пайплайн
Стадии: **validate** → **setup**
| Задача | Триггер | Описание |
|---|---|---|
| `validate/lint` | каждый push / MR | `ansible-lint` всех плейбуков и ролей |
| `validate/syntax-check` | каждый push / MR | Синтаксическая проверка всех плейбуков |
| `setup/manager_nodes` | ручной, ветка `main` | `setup_manager.yml` |
| `setup/control_plane` | ручной, ветка `main` | `setup_control_plane.yml` |
| `setup/workers` | ручной, ветка `main` | `setup_worker_plane.yml` |
| `setup/longhorn` | ручной, ветка `main` | `setup_longhorn.yml` |
| `setup/traefik` | ручной, ветка `main` | `setup_traefik.yml` |
| `setup/flux` | ручной, ветка `main` | `setup_flux.yml` |
| `setup/gitlab_agent` | ручной, ветка `main` | `setup_gitlab_agent.yml` |
| `setup/minio` | ручной, ветка `main` | `setup_minio.yml` |
| `setup/logging` | ручной, ветка `main` | `setup_logging.yml` |
| `setup/sealed_secrets` | ручной, ветка `main` | `setup_sealed_secrets.yml` |
| `setup/gpu_workers` | ручной, ветка `main` | `setup_gpu.yml` |
| `setup/gpu_model_storage` | ручной, ветка `main` | `setup_gpu_model_storage.yml` |
`resource_group: production` — все setup-задачи сериализованы, одновременно выполняется только одна.
### Переменные GitLab CI/CD (Settings → CI/CD → Variables)
| Переменная | Описание |
|---|---|
| `ANSIBLE_SSH_USER` | SSH-пользователь для целевых серверов |
| `ANSIBLE_SSHKEY_ID_RSA` | Содержимое приватного SSH-ключа (тип: File) |
| `ANSIBLE_BECOME_PASS` | Пароль sudo |
| `GITLAB_FLUX_TOKEN` | GitLab PAT (scope: api) для Flux bootstrap и клонирования fleet-repo |
| `GITLAB_AGENT_TOKEN` | Токен GitLab Agent (получить в GitLab → Operate → Kubernetes clusters) |
| `MINIO_ROOT_PASSWORD` | Пароль root-пользователя MinIO (используется при ручном создании Secret) |
| `LOKI_MINIO_PASSWORD` | Пароль пользователя `loki` в MinIO (создаётся Ansible при запуске setup_logging) |
| `GRAFANA_ADMIN_PASSWORD` | Пароль администратора Grafana (задаётся в Secret `grafana-admin-secret` вручную) |
---
## Порядок развёртывания
Для нового кластера — строго в следующем порядке:
```
1. setup:manager_nodes — инструменты оператора
2. setup:control_plane — Kubernetes, CNI, Dashboard
3. setup:workers — подключение worker-нод
4. setup:longhorn — хранилище (prereqs на workers → Helm с manager)
5. setup:traefik — порт-прокси (firewall на workers → Helm с manager)
6. setup:minio — объектное хранилище S3 (StorageClass + Helm с manager)
7. setup:flux — GitOps bootstrap
8. setup:gitlab_agent — видимость кластера в GitLab UI
9. setup:logging — PLG-стек (Loki + Alloy + Grafana)
10. setup:sealed_secrets — контроллер Sealed Secrets + kubeseal CLI
11. setup:gpu_workers — только для узлов gpu_workers, после их setup:workers
```
> **Перед запуском setup:minio** создать Secret вручную: `kubectl create secret generic minio-root-credentials --from-literal=rootUser=minioadmin --from-literal=rootPassword='...' -n minio`
> **Перед запуском setup:logging** создать Secret вручную: `kubectl create namespace monitoring && kubectl create secret generic grafana-admin-secret --from-literal=admin-user=admin --from-literal=admin-password='...' -n monitoring`
---
## Архитектурные решения
### kubeadm: version derive at runtime
`kubernetesVersion` в конфиге kubeadm определяется из `kubeadm version -o short` в runtime. Исключает ошибки несоответствия версий при изменении пакета без изменения переменной.
### single-node: снятие control-plane taint
Taint `node-role.kubernetes.io/control-plane:NoSchedule` снимается автоматически. Dashboard и прочие workload'ы размещаются на master-узле при single-node-конфигурации.
### kubeadm join: ежезапускная генерация токена
Join-токены истекают через 24 часа. Роль `k8s_worker` генерирует свежий токен на control plane при каждом запуске; шаг join идемпотентен — пропускается, если узел уже в кластере.
### Longhorn: монтирование до аннотации
Диски форматируются как XFS и монтируются по UUID (`nofail`) **до** установки Longhorn. Longhorn обнаруживает диски через аннотации узлов — не через автоопределение. Это предотвращает переформатирование при повторном запуске плейбука.
### Traefik: нет hostname, нет TLS
Каждый сервис получает выделенный порт (10001–10999). Нет hostname-routing, нет TLS — клиент подключается по `http://:`. TLS терминируется на уровне Traefik при необходимости. Dashboard работает в HTTP-режиме (`dashboard_insecure: true`).
### Flux + agentk: разделение ответственности
Ansible управляет **ОС и кластером** (установка, конфигурация нод). Flux управляет **приложениями внутри кластера** (деплой, обновление образов). agentk обеспечивает **видимость** состояния кластера в GitLab UI.
### GPU-узлы: отдельный плейбук, вручную ставится только драйвер
`gpu_workers` — подгруппа `workers`. NVIDIA-драйвер ставится на сервер вручную и не управляется Ansible (роль `gpu_prereqs` лишь проверяет `nvidia-smi` и падает, если драйвера нет). `setup_gpu.yml` запускается отдельно и **после** `setup_worker_plane.yml`, так как роль `k8s_worker` перегенерирует `/etc/containerd/config.toml` с нуля и стёрла бы настройку nvidia runtime, если бы порядок был обратным. GPU-узлы исключены из дефолтной конфигурации Longhorn (`longhorn_disks: []` в `gpu_workers.yml`) — это compute-нода, не storage-нода.
### Firewalld и CNI-интерфейсы
> **Критично.** `flannel.1` и `cni0` должны быть в зоне `trusted` на каждом узле кластера.
Без этого pod-to-pod-трафик от workers, приходящий на master, обрабатывается зоной `internal` — она пропускает только явно перечисленные порты. Любой порт нового сервиса молча отбрасывается, даже если порт открыт на уровне хоста.
При добавлении любой роли с `firewalld` — обязательно добавлять:
```yaml
- name: Firewall | Trust CNI interfaces
ansible.posix.firewalld:
zone: trusted
interface: "{{ item }}"
permanent: true
state: enabled
loop:
- flannel.1
- cni0
```
---
## Локальная разработка
```bash
# Установка коллекций Ansible Galaxy
ansible-galaxy collection install -r requirements.yml -p collections/ --force
# Проверка синтаксиса
ansible-playbook --syntax-check -i inventory/prod playbooks/setup_manager.yml
# Линтинг
ansible-lint playbooks/ roles/
# Dry-run (режим check + diff)
ansible-playbook -i inventory/prod playbooks/setup_control_plane.yml --check --diff
# Запуск только на одном хосте
ansible-playbook -i inventory/prod playbooks/setup_worker_plane.yml --limit k8s-worker-01
```
---
## Добавление нового компонента
1. Добавить хост в `inventory/prod/hosts.yml` (если новая нода)
2. Создать `inventory/prod/group_vars/.yml` (если новая группа)
3. Создать роль `roles//` со стандартной структурой
4. Создать `roles//README.md` — подробный корп. стандарт использования компонента
5. Создать `playbooks/.yml`
6. Добавить CI/CD-задачу в `.gitlab-ci.yml` (`stage: setup`, `resource_group: production`)
7. Обновить **этот README**: краткое описание роли + ссылка на `roles//README.md`
8. Обновить `CLAUDE.md`: добавить роль в структуру репозитория и ключевые решения