# aero — Ansible-провижининг docker-хостов

Ansible-роль `docker`, устанавливающая на сервер:

- **Docker Engine** (`docker-ce`, `docker-ce-cli`);
- **containerd**;
- **docker compose** и (где есть) buildx;
- обновление пакетов системы при старте.

Ветка установки выбирается автоматически по пакетному менеджеру хоста:

| Семейство | Пакетный менеджер | Пакеты |
| --- | --- | --- |
| Ubuntu / Debian | `apt` | `docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin` |
| RedOS / RHEL | `dnf` | `docker-ce docker-ce-cli containerd docker-compose` |

> Имена пакетов в RedOS отличаются (`containerd`, `docker-compose`), поэтому
> списки разнесены по семействам в `roles/docker/defaults/main.yml`.

## Важно: где запускать

**Ansible не работает как управляющий узел на нативном Windows** (нужен POSIX).
Запуск — из **WSL** (Ubuntu). Управление окружением — через **uv**.

### Разовая настройка WSL

```bash
# 1. uv внутри WSL
curl -LsSf https://astral.sh/uv/install.sh | sh
source ~/.local/bin/env

# 2. Если DNS в WSL не резолвит (curl: Could not resolve host):
echo "nameserver 8.8.8.8" | sudo tee /etc/resolv.conf
printf "\n[network]\ngenerateResolvConf = false\n" | sudo tee -a /etc/wsl.conf

# 3. SSH-ключ в домашку WSL с правами 600 (на /mnt/c ssh отвергает ключ)
mkdir -p ~/.ssh && chmod 700 ~/.ssh
cp /mnt/c/Users/user/.ssh/local/id_ed25519 ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
```

## Запуск

Из WSL, в каталоге проекта (`/mnt/c/.../iac/aero`).

### Установка с чистого листа — одной командой

```bash
uv run poe install
```

Последовательность: `sync` → `galaxy` → **`provision`** (базовые пакеты + docker
и запуск службы) → **`stack`** (деплой конфигурации + `docker compose up -d`
сервисов приложения) → **`superuser`** (генерация и создание Django-админа).
Перед запуском впишите хосты в [inventory.ini](inventory.ini) и убедитесь, что
на control-node есть SSH-ключ и `~/.ssh/local/authorized_key.json` для входа в
реестр.

### Пошагово / отдельные команды

| Команда | Действие |
| --- | --- |
| `uv run poe ping` | проверка SSH + sudo |
| `uv run poe provision` | базовые пакеты + docker/containerd + запуск службы |
| `uv run poe deploy` | деплой конфигурации: файлы, `.env`, сертификаты, `docker login` |
| `uv run poe platform` | деплой + поднять GitOps-подложку (k3s + gitea + vault + Flux) |
| `uv run poe stack` | деплой + поднять стек (`docker compose up -d`) |
| `uv run poe gen-env` | только сгенерировать пароли и `.env` |
| `uv run poe login` | только `docker login` в реестр по ключу |
| `uv run poe superuser` | сгенерировать логин+пароль и создать Django-суперпользователя |
| `uv run poe flux-status` | состояние FluxCD в k3s |
| `uv run poe vault-status` | состояние Vault (инициализирован / распечатан) |
| `uv run poe check` / `deploy-check` | dry-run (`--check --diff`) |

Поднимаемые сервисы задаются в `sarex_services` (роль `sarex_stack`) —
`postgres(+init)`, `redis`, `rabbitmq`, `minio(+init)`, `measurements`,
`backend`, `celery`, `frontend`, `nginx` (k3s/gitea/processing не поднимаются).

> `ANSIBLE_CONFIG` и `UV_LINK_MODE` заданы в `[tool.poe.env]` — на диске `C:`
> (через `/mnt/c`) права `0777`, и ansible игнорирует `ansible.cfg` в
> world-writable каталоге, поэтому путь к конфигу передаётся явно.

### GitOps-подложка контура (k3s + gitea + vault + FluxCD)

Целевая схема контура — не «всё в compose», а **compose как подложка, k3s как
среда выполнения**. Источником правды становится gitea внутри контура, а
раскаткой занимается FluxCD.

```
docker compose                        k3s (4 ноды в контейнерах)
├── k3s-server        ──► нода  ┐
├── k3s-worker-1..3   ──► ноды  ├─► flux-system (controllers)
├── gitea (172.28.0.12) ◄────── ┘   и всё, что опишем в clusters/aero
│   └── infra/iac.git
├── vault (+init)
├── flux-k8s-init     ──► namespace flux-system + Service-мост до gitea
└── flux-bootstrap    ──► ставит Flux в k3s и привязывает к gitea
```

**Мост до gitea.** Контроллеры Flux работают внутри k3s, где резолвит CoreDNS,
а имён compose-сети там нет. Поэтому `flux-k8s-init` заводит в namespace
`flux-system` Service без селектора + Endpoints на статический IP gitea — тем же
приёмом, что `bridge-postgres`/`bridge-minio` для processing. Источник в
`GitRepository` указан именем `gitea.flux-system.svc.cluster.local`, а адрес
задан ровно в одном месте — переменной `GITEA_BRIDGE_IP` (она же `ipv4_address`
сервиса `gitea`). Контейнер `flux-bootstrap` резолвит то же имя через
`extra_hosts` — иначе flux CLI не смог бы запушить манифесты.

**Имена нод закреплены** через `hostname:` в compose. Это не косметика: k3s
берёт имя ноды из hostname, иначе им становится ID контейнера. Local-path
привязывает PV к ноде через `nodeAffinity` по имени, и после пересоздания
контейнера тома «повисли» бы.

### Хранилище PVC

`StorageClass local-path` (встроенный в k3s, он же default). Дефолтный путь
провижинера `/var/lib/rancher/k3s/storage` перекрыт bind-mount'ом на хост —
поэтому настраивать ConfigMap `local-path-config` не нужно:

```
{{ deploy_dir }}/k3s-storage/
├── server/      ← PVC, севшие на k3s-server
├── worker-1/
├── worker-2/
└── worker-3/
```

Файлы лежат на хосте обычными каталогами: переживают пересоздание контейнера
k3s и `docker compose down -v`, бэкапятся штатными средствами.

> **Следствие для stateful-сервисов.** Хранилище **node-local**: PVC привязан к
> той ноде, где под запустился первым. При переезде postgres/redis/rabbitmq/minio
> в k3s их нужно явно прибивать к конкретной ноде (`nodeSelector`), иначе данные
> размажутся по четырём каталогам, а потеря ноды сделает том недоступным.
> Бэкап должен покрывать все каталоги `k3s-storage/*`.

```bash
uv run poe platform          # поднять подложку
uv run poe flux-status       # убедиться, что Flux реконсилирует
```

Что происходит по шагам:

1. **Волна 1** — стартуют `k3s-server`, три воркера и `gitea`. Роль ждёт, пока
   k3s запишет `./k3s/kubeconfig.yaml` (и что файл непустой) — иначе
   `flux-bootstrap` смонтировал бы каталог вместо файла.
2. **`gitea-init`** — одноразовый: заводит администратора (`gitea admin user
   create`), организацию и пустой репозиторий через API. Идемпотентен.
3. **`flux-k8s-init`** — одноразовый: namespace `flux-system` и Service-мост до
   gitea (см. выше). Обязан отработать до bootstrap.
4. **`vault` + `vault-init`** — Vault на file-хранилище. `vault-init` живёт
   постоянно: инициализирует Vault одним ключом, распечатывает его и включает
   `kv-v2` на пути `secrets/` (тот же путь, что в k8s-контурах). После ребута
   хоста Vault поднимается запечатанным — цикл распечатывает его сам.
5. **`flux-bootstrap`** — одноразовый: `flux bootstrap git` ставит контроллеры в
   k3s и пушит их манифесты в gitea в `clusters/aero/flux-system`.

> **FluxCD не работает в docker-compose** — это контроллеры Kubernetes. В compose
> живёт только установщик; после его успеха Flux работает внутри k3s.

Секреты подложки (`GITEA_ADMIN_PASSWORD`, `K3S_TOKEN`) генерируются так же, как
остальные — в `aero/.secrets/<host>/`. Unseal-ключ и root-токен Vault лежат на
отдельном docker-томе `sarex-vault-init` с правами `600` и **в git не попадают**.

#### Границы текущего этапа

Сделана только подложка. Осознанно **не** сделано:

- Репозиторий в gitea содержит лишь `clusters/aero/flux-system` — то, что запушил
  bootstrap. Наполнение (`apps/`, `infrastructure/`) и перенос прикладных сервисов
  из compose в k3s — следующий этап.
- Vault поднят и распечатан, но **kubernetes auth не настроен**: для него нужен
  Vault Agent Injector внутри k3s, а он приедет уже через Flux. Пока в Vault
  можно только класть секреты, поды их ещё не читают.
- Прикладной стек (`sarex_services`) не тронут и продолжает работать в compose.

### Развёртывание sarex-стека (роль `sarex_stack`)

Копирует `docker-compose.yaml`, `nginx/templates` и `backend/uwsgi.ini` в
`/root/sarex`, рендерит `.env` из `.env.example` со **сгенерированными паролями**
и доменами `*.sarex.local.lonsdaleites.ru`, генерирует самоподписанный
TLS-сертификат на все три домена (`nginx/certs/selfsigned.{crt,key}`), логинится
в реестр и (при `sarex_compose_up=true`) поднимает сервисы приложения.
Роли/базы и MinIO-бакеты создаются одноразовыми `postgres-init`/`minio-init`.

```bash
uv run ansible-playbook deploy.yml --check --diff   # dry-run
uv run ansible-playbook deploy.yml                  # подготовка файлов
```

- Пароли генерируются один раз и персистятся на control-node в `aero/.secrets/`
  (gitignored) — повторный прогон не меняет `.env` (идемпотентно).
- Только пароли и `.env`, без остального деплоя — отдельной командой:

  ```bash
  uv run poe gen-env                          # сгенерировать/обновить .env
  rm -rf .secrets/<host> && uv run poe gen-env  # перегенерировать пароли заново
  ```
- Стек по умолчанию **не поднимается** (`sarex_compose_up: false`). Образы —
  приватные (`cr.yandex`), поэтому сперва `docker login cr.yandex` на хосте,
  затем `cd /root/sarex && docker compose up -d` (или прогон с
  `-e sarex_compose_up=true`).
- Домены нужно завести в DNS/hosts, чтобы они резолвились на хост.

### Galaxy-коллекции (опционально)

Коллекции из `requirements.yml` (`community.docker` и др.) **не требуются** для
работы роли — она использует только модули `ansible.builtin`. Ставятся при
необходимости:

```bash
uv run poe galaxy
```

## Настройка

Хосты — в [`inventory.ini`](inventory.ini) (группа `docker_hosts`). Root — через
`sudo` (`become`). Если `sudo` с паролем — добавьте `--ask-become-pass`:

```bash
uv run poe play -- --ask-become-pass
```

Переменные роли (`roles/docker/defaults/main.yml`):

| Переменная                  | По умолчанию | Назначение                                   |
| --------------------------- | ------------ | -------------------------------------------- |
| `docker_update_packages`    | `true`       | Обновлять пакеты при старте                    |
| `docker_upgrade_dist`       | `false`      | Полный `dist-upgrade` (только Debian)         |
| `docker_service_enabled`    | `true`       | Автозапуск службы docker                      |
| `docker_service_state`      | `started`    | Состояние службы после прогона                |
| `docker_users`              | `[]`         | Пользователи в группу `docker` (без sudo)     |
| `docker_redhat_add_ce_repo` | `false`      | Внешний репозиторий Docker CE (RHEL; RedOS не нужен) |

## Структура

```
aero/
├── pyproject.toml            # окружение uv + poe-задачи
├── ansible.cfg               # настройки ansible (inventory, become)
├── requirements.yml          # galaxy-коллекции (опционально)
├── inventory.ini             # хосты
├── site.yml                  # плейбук
└── roles/docker/
    ├── defaults/main.yml     # переменные и списки пакетов по семействам
    ├── tasks/
    │   ├── main.yml          # диспетчер по пакетному менеджеру + общие шаги
    │   ├── debian.yml        # ветка apt (Ubuntu/Debian)
    │   └── redhat.yml        # ветка dnf (RedOS/RHEL)
    ├── handlers/main.yml     # restart docker
    └── meta/main.yml         # метаданные и зависимости
```
