# Развёртывание платформы SAREX в закрытом контуре

Документ описывает полный цикл разворачивания платформы в изолированном контуре: от поднятия кластера kubespray до раскатки сущностей terraform. Порядок шагов фиксированный — каждый следующий этап зависит от предыдущего, менять местами нельзя.

## Кластер: kubespray

Кластер поднимается через kubespray (`/Users/eh/SAREX/SAREX-NEW-GITLAB/kubespray`, ветка `v2.29`). Запуск идёт не с голого хоста, а из docker-образа, который собирается по `Dockerfile` в корне репозитория — образ на базе Ubuntu 22.04 с ansible, kubectl и зависимостями из `requirements.txt`. Собранный образ монтирует директорию инвентаря и гоняет `ansible-playbook` внутри контейнера, поэтому на машине оператора не нужно ничего кроме docker.

Инвентарь описывается в `inventory/<имя-контура>/inventory.ini` по образцу `inventory/sample` или уже существующих `inventory/nn`, `inventory/docker-test`. В секции `[all:vars]` задаётся `ansible_user`, а способ входа на хосты — либо `ansible_ssh_private_key_file` (путь к приватному ключу), либо `ansible_ssh_pass` вместе с `ansible_become_pass`, если на хостах пароль, а не ключ. Control-plane и worker-узлы перечисляются в секциях `[kube_control_plane]` и `[kube_node]` с `ansible_host` и `ip`.

Отдельно нужно прописать SAN будущего API-сервера — параметр `supplementary_addresses_in_ssl_keys` в `inventory/<контур>/group_vars/k8s_cluster/k8s-cluster.yml` (по умолчанию закомментирован). Туда добавляется тот IP мастера, по которому вы потом будете подключаться kubeconfig'ом — обычно это внешний/публичный адрес, а не внутренний `ip` из инвентаря. Если это не сделать, TLS-сертификат апи-сервера будет выписан только на внутренний адрес, и коннект по внешнему IP будет падать на верификации сертификата — придётся пересобирать сертификаты вручную задним числом.

После того как инвентарь готов, играется `cluster.yml`:

```
docker run --rm -it --mount type=bind,source="$(pwd)"/inventory/<контур>,dst=/inventory \
  --mount type=bind,source=${HOME}/.ssh/id_rsa,dst=/root/.ssh/id_rsa \
  <образ kubespray> \
  ansible-playbook -i /inventory/inventory.ini --private-key /root/.ssh/id_rsa cluster.yml
```

Если вход по паролю, ключ не монтируется, а `ansible-playbook` дополнительно вызывается с `--ask-pass --ask-become-pass` либо соответствующие переменные уже прописаны в инвентаре.

Играется он долго, минут 30-60 в зависимости от числа узлов. По завершении с control-plane забирается `/etc/kubernetes/admin.conf` — это и есть kubeconfig с полным доступом. Кладём его локально, проверяем коннект (`kubectl --kubeconfig admin.conf get nodes`) — на этом задача kubespray закрыта, дальше кластер существует сам по себе, kubespray больше не трогаем, пока не понадобится добавить узел или обновить версию.

## Gitea

Дальше в кластер ставится gitea — она нужна как локальная точка, через которую flux и terraform будут читать репозитории, не имея прямого выхода в интернет к `gitlab.sarex.io`. Исходник — `/Users/eh/SAREX/SAREX-NEW-GITLAB/gitea`, ветка `master`. Чарт лежит в `.helm`, состоит из `universal-chart` (сама gitea + act_runner) и `postgresql-preprod` как зависимость с `condition: postgresql.enabled` — если под БД есть managed postgres, поднимать embedded не нужно.

Внутри `universal-chart.services` описан `gitea-ci-worker` — это и есть act_runner, тот самый "воркер". Ему нужно прописать `GITEA_INSTANCE_URL` (внутренний адрес gitea), `GITEA_RUNNER_NAME`, `GITEA_RUNNER_LABELS` (метки раннера, под них потом ориентируются workflow) и, если раннер должен уметь применять что-то в самом кластере — `KUBECONFIG`/`KUBE_CONTEXT`, смонтированный из PVC. Готовый референс — раскатка в кластере `yc-infra-prod` (`.helm/values.yc-cps-prod.yaml` как пример по структуре, сама раскатка в `yc-infra-prod` — рабочий пример gitea+worker в проде).

Если для gitea выделен отдельный хост рядом с кластером (не разворачиваем внутри k8s) — тот же чарт руками разворачивается через docker-compose: контейнер gitea, контейнер act_runner, том с данными. В этом случае раннеру дополнительно монтируется kubeconfig того кластера, куда он должен катить деплои — без этого он видит только себя.

## Зеркалирование gitlab.sarex.io в gitea

Основной код (iac, terraform) живёт в `gitlab.sarex.io`, а не в контуре — контур не должен иметь туда прямого доступа на чтение снаружи каждый раз, вместо этого gitea внутри контура сама периодически подтягивает изменения. Для этого в gitlab.sarex.io заводится пользователь `gitlab-pusher`, ему создаётся токен на чтение (repository read, никаких прав на запись туда не нужно). В gitea на этот токен настраивается pull mirror — для репозитория `infra/iac` и `infra/terraform`, интервал синхронизации минимальный, который позволяет gitea — 10 минут. Меньше gitea не даёт, это не настраиваемый лимит, а особенность её пул-миррора.

После первого успешного синка в gitea появляются полные копии обоих репозиториев с той же историей веток. Дальше вся работа идёт как обычно — пушим в `gitlab.sarex.io`, gitea сама подтягивает изменения по своему расписанию.

## Flux bootstrap

Flux разворачивается через `flux bootstrap`, но не против `gitlab.sarex.io`, а против уже замирроренной репы `infra/iac` в локальной gitea — именно она физически доступна из кластера. Перед бутстрапом в кластере должен уже существовать секрет `yc-cr-auth` (dockerconfigjson с доступом к `cr.yandex/crp3ccidau046kdj8g9q`) — все helm-репозитории в `iac` заведены как OCI-репозиторий с этим registry, и без секрета flux не сможет вытащить ни один чарт с самого начала, включая свои собственные компоненты, если они тоже идут оттуда. Секрет создаётся руками до бутстрапа, flux его не создаёт и не обновляет — это на разработчике/операторе.

Дальше поток такой: пушим в `iac` на `gitlab.sarex.io`, gitea подтягивает изменение с задержкой до 10 минут, flux в кластере видит новый коммит в своей `GitRepository` (она указывает на адрес локальной gitea, не на gitlab) и применяет `Kustomization` по пути `./clusters/<имя-контура>`. Референс структуры — уже закрытые контуры `d8-ugmk-prod` и `brusnika-prod` в `iac`, там `flux-system/gotk-sync.yaml` указывает прямо на внутренний адрес gitea контура, а не наружу.

## Vault

Vault раскатывается тем же flux'ом из `iac`, манифест лежит в `infrastructure/vault` с патчем под конкретный кластер в `infrastructure/vault/<кластер>/vault.yaml`. В патче обязательно нужно поправить три вещи под конкретное железо: `storageClass` в `server.dataStorage` (в контуре обычно нет managed-дисков, часто `local-path` или локальный `nfs`, смотря что провижн заведён на нодах), и `nodeSelector`/`tolerations` — если в кластере выделены таргетные ноды под инфраструктурные сервисы (как `node.deckhouse.io/group: generic` в примере для `d8-ugmk-prod`), vault нужно на них же и посадить, иначе поды зависнут в Pending. Дефолтные `tolerations`, если под них нет отдельной группы нод, обнуляются пустым списком.

После того как HelmRelease встал и поды vault поднялись, vault ещё запечатан (sealed) — это его нормальное стартовое состояние, писать/читать в него нельзя. Инициализация делается вручную: `vault operator init` на первом поде, из вывода забираются unseal keys и root token. Каждый под vault нужно распечатать (`vault operator unseal`) отдельно — если реплик несколько (HA-режим на raft), печатать нужно все, иначе часть кластера останется недоступна и raft не соберётся в кворум. Весь вывод `vault operator init` (ключи + root token) сохраняется в k8s secret `vault-secret` в ns `vault` — это единственное место, где эти данные остаются после инициализации, вывод команды больше нигде не логируется и не хранится. Если секрет потерять, а vault при этом запечатается (рестарт пода, например) — распечатать его без этих ключей уже не выйдет, придётся поднимать заново с потерей данных.

## Terraform: ветка contour

После того как в кластере есть кластер, gitea, flux и vault, разворачиваются собственно сущности приложений — через terraform, репозиторий `infra/terraform`, ветка `contour`. Она собрана так, чтобы одним и тем же кодом обслуживать любой закрытый контур: конкретное окружение выбирается через `INFRA_ENV`, а не через отдельную ветку на контур.

Ветка `contour` при пуше в `gitlab.sarex.io` автоматически зеркалится в отдельный репозиторий `https://gitlab.sarex.io/infra/terraform-contour-mirror` — а его уже мирроит к себе gitea контура тем же pull-mirror механизмом, что и `iac`. CI (`.gitea/workflows/terraform.yml`) в самой gitea читает набор переменных и секретов, специфичных для контура — они прописываются в настройках репозитория в gitea (Settings → Actions → Variables/Secrets), не в коде. Из переменных (Variables) обязательны `INFRA_ENV` (имя блока в `infrastructure.yaml`, под которым описан этот контур) и `RUNNER_LABEL` (метка раннера, который будет исполнять джобу — должна совпадать с тем, что реально зарегистрировано в gitea act_runner). Из секретов (Secrets) нужны реквизиты S3-бэкенда для tfstate (`TF_STATE_S3_ENDPOINT`, `TF_STATE_S3_BUCKET`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`), доступ к vault контура (`VAULT_ADDR`, `VAULT_TOKEN`) и приватный age-ключ для sops (`SOPS_AGE_KEY`) — им шифруется `infrastructure-secrets.yaml`. Если раннер не in-cluster, а внешний, дополнительно нужен `KUBECONFIG_B64` — kubeconfig в base64 для доступа к кластеру контура.

Хосты инфраструктурных сервисов конкретного контура (postgres, kafka, rabbitmq, minio, если есть) прописываются не в CI-переменных, а прямо в `infrastructure.yaml`, в отдельном блоке `environments.<имя-контура>` — том самом, на который указывает `INFRA_ENV`. Секретная часть (пароли, ключи) идёт туда же по смыслу, но в `infrastructure-secrets.yaml`, зашифрованном sops на общий age-ключ. Как только оба файла содержат блок под контур и правильные CI-переменные/секреты выставлены в gitea, можно катить план и применять — terraform дальше заводит namespace'ы, базы, топики, vhosts и секреты уже без ручных шагов в кластере.
