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

626 lines
37 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.

# 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` |
<details>
<summary>Переменные k8s_control_plane</summary>
| Переменная | Значение по умолчанию | Описание |
|---|---|---|
| `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-узел |
</details>
<details>
<summary>Переменные k8s_worker</summary>
| Переменная | Значение по умолчанию | Описание |
|---|---|---|
| `k8s_version` | `1.33` | Должна совпадать с control plane |
| `worker_join_source_host` | первый хост `control_plane` | Откуда читать join-команду |
</details>
<details>
<summary>Переменные k8s_manager</summary>
| Переменная | Значение по умолчанию | Описание |
|---|---|---|
| `dashboard_insecure` | `false` | HTTP-режим Dashboard (переопределяется в `true` в group_vars) |
| `dashboard_manifest_url` | GitHub raw v2.7.0 | Без зависимости от CDN |
| `k8s_manager_upgrade_packages` | `false` | Полное обновление ОС (opt-in) |
</details>
---
### `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 <host> gpu=nvidia` для каждого узла `gpu_workers`; `kubectl apply` манифеста NVIDIA k8s-device-plugin |
<details>
<summary>Переменные gpu_prereqs / gpu_device_plugin</summary>
| Переменная | Значение по умолчанию | Описание |
|---|---|---|
| `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 |
</details>
> Порядок обязателен: `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`.
<details>
<summary>Переменные gpu_model_storage</summary>
| Переменная | Значение по умолчанию | Описание |
|---|---|---|
| `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` | Файловая система |
</details>
> **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.
<details>
<summary>Переменные</summary>
| Переменная | Значение по умолчанию | Описание |
|---|---|---|
| `longhorn_chart_version` | `1.7.2` | Версия Helm-чарта |
| `longhorn_namespace` | `longhorn-system` | Namespace |
| `longhorn_default_replica_count` | `1` | Реплик на том; поднять до 23 при добавлении 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 |
</details>
> **Стандарт работы с хранилищем** (PVC, StatefulSet, реплики, бэкапы, диагностика):
> [`roles/longhorn/README.md`](roles/longhorn/README.md)
---
### `traefik`
HTTP/TCP port-proxy. DaemonSet на worker-нодах. Каждый сервис получает выделенный TCP-порт из диапазона 1000110999. Вся топология определяется через `traefik_port_map` в `inventory/prod/group_vars/traefik.yml`. Поддерживаются два режима маршрутизации: HTTP (`IngressRoute`, поле `protocol` не задаётся) и raw TCP (`IngressRouteTCP` с `HostSNI("*")`, `protocol: tcp` в port_map).
| Задача | Что делает |
|---|---|
| `firewall.yml` | Открывает 1000010999/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 |
<details>
<summary>Переменные</summary>
| Переменная | Значение по умолчанию | Описание |
|---|---|---|
| `traefik_chart_version` | `32.1.0` | Версия Helm-чарта |
| `traefik_namespace` | `traefik` | Namespace |
| `traefik_port_map` | `[]` | Список сервисов; переопределяется в `group_vars/traefik.yml` |
</details>
> **Стандарт публикации сервисов** (добавление порта, 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`
<details>
<summary>Переменные</summary>
| Переменная | Значение по умолчанию | Описание |
|---|---|---|
| `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) |
</details>
> **Стандарт 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
```
<details>
<summary>Переменные</summary>
| Переменная | Значение по умолчанию | Описание |
|---|---|---|
| `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]` | Бакеты, создаваемые при старте |
</details>
> **Стандарт объектного хранилища 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
```
<details>
<summary>Переменные</summary>
| Переменная | Значение по умолчанию | Описание |
|---|---|---|
| `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 |
</details>
> **Стандарт централизованного сбора логов** (архитектура 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
```
<details>
<summary>Переменные</summary>
| Переменная | Значение по умолчанию | Описание |
|---|---|---|
| `sealed_secrets_chart_version` | `2.16.1` | Версия Helm-чарта |
| `sealed_secrets_namespace` | `kube-system` | Namespace контроллера |
| `sealed_secrets_cli_version` | `""` (latest) | Версия kubeseal CLI; пустая — берёт latest с GitHub |
</details>
---
### `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`.
<details>
<summary>Переменные</summary>
| Переменная | Значение по умолчанию | Описание |
|---|---|---|
| `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 |
</details>
---
## 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
Каждый сервис получает выделенный порт (1000110999). Нет hostname-routing, нет TLS — клиент подключается по `http://<worker-ip>:<port>`. 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/<group>.yml` (если новая группа)
3. Создать роль `roles/<role>/` со стандартной структурой
4. Создать `roles/<role>/README.md` — подробный корп. стандарт использования компонента
5. Создать `playbooks/<operation>.yml`
6. Добавить CI/CD-задачу в `.gitlab-ci.yml` (`stage: setup`, `resource_group: production`)
7. Обновить **этот README**: краткое описание роли + ссылка на `roles/<role>/README.md`
8. Обновить `CLAUDE.md`: добавить роль в структуру репозитория и ключевые решения