Compare commits

...

84 Commits
master ... aero

Author SHA1 Message Date
emelinda
a713c46ed0 Добавлены скрипты для полного рендера манифестов и точка входа образа. 2026-08-11 03:33:56 +03:00
emelinda
492d31b253 Развёртывание контура уехало в infra/aero-deploy
Здесь остаётся только то, что читает Flux: clusters/, infrastructure/, apps/.
Ansible-роли, docker-compose подложки (k3s, gitea, vault, bootstrap Flux),
конфигурация Vault и k3s-манифесты DNS-мостов переехали в отдельный
репозиторий — их читает не кластер, а ansible с control-node, и держать их
рядом с манифестами было незачем.

Заодно уехали postgres/initdb и frontend/Dockerfile — остатки прикладного
compose-стека, убранного в 75d1fba. На них уже никто не ссылался.

Связь остаётся односторонней: роль sarex_stack пакует отсюда clusters/aero и
его замыкание и коммитит в gitea контура. Путь сюда она берёт из flux_src_root
(по умолчанию соседний каталог ../iac). Добавили компонент в
clusters/aero/controllers — допишите его в gitea_sync_paths в aero-deploy,
иначе kustomize в кластере упадёт на отсутствующем каталоге; обратной ссылки
из кода нет, поэтому напоминание продублировано в README.

Каждый удалённый файл предварительно сверен по sha256 с копией в aero-deploy:
49 совпали побайтово, 5 отличаются осознанными правками переноса.
2026-08-10 18:03:47 +03:00
emelinda
0b5a9b8f21 Reload: backend монтирует django-configmap и сам не перекатывается
В sarex_reload_workloads backend отсутствовал с оговоркой "перекатывается
сам, потому что его envs правятся в оверлее". Оговорка верна только для
правок, задевающих шаблон пода. Замена одних лишь данных ConfigMap его не
трогает: спецификация прежняя, Kubernetes причин пересоздавать под не
видит, и приложение остаётся на старом production.py.

Вылезло на CsrfExemptSessionAuthentication: Flux разложил бы новый
ConfigMap, poe reload перекатил бы frontend и celery, а backend — тот
единственный, кого правка и касалась, — продолжил бы отдавать 403.
2026-08-10 13:29:07 +03:00
emelinda
a67ab33e5d Django: DELETE падал в 403 — сессия без X-CSRFToken
SPA аутентифицируется сессионной кукой и не прикладывает X-CSRFToken, а
SessionAuthentication из DRF требует его на любом небезопасном методе:

  DELETE /api/core/videos/<id>/  ->  403
  {"detail": "CSRF Failed: CSRF token missing."}

Проверено на контуре, тремя запросами по несуществующему id (403 = отказ
авторизации, 404 = авторизация прошла):

  Bearer-токен                 -> 404   CSRF не при чём
  сессия + X-CSRFToken         -> 404   проходит
  сессия без X-CSRFToken       -> 403   ровно наша ошибка
  без авторизации              -> 401

То есть бэкенд исправен, ломается ровно сочетание "кука без заголовка".
С Bearer до SessionAuthentication дело не доходит: simple_jwt стоит в
DEFAULT_AUTHENTICATION_CLASSES раньше.

Ставим подкласс с пустым enforce_csrf на то же пятое место. Цена — с
cookie-запросов снимается защита от CSRF. Основной вектор закрыт самими
куками (sessionid и csrftoken выставляются с SameSite=Lax, браузер не
пошлёт их при кросс-сайтовом DELETE); остаётся щель в соседях по домену,
поскольку SameSite смотрит на site, а не на origin.

Импорт rest_framework в модуле настроек безопасен не всегда — обычно так
ловят AppRegistryNotReady. Здесь проверено запуском django.setup() внутри
работающего пода backend: setup проходит, строка пути к классу резолвится.
2026-08-10 13:25:06 +03:00
emelinda
75d1fba14a Compose: убран прикладной стек — он давно переехал в k3s
В docker-compose.yaml описывались 25 сервисов, а на хосте существовали
контейнеры только для 9. Остальные 16 — django, celery, frontend, s3-proxy,
processing-api/frontend, engine, measurements, nginx и их зависимости
(postgres, redis, rabbitmq, minio с init-контейнерами) — развёрнуты в k3s и
описаны в apps/ и infrastructure/. Дубликат не просто лежал мёртвым грузом,
а расходился с кластером и вводил в заблуждение.

Что это был именно мёртвый груз, а не «выключенный на время» стек:

  * контейнеров не существовало вовсе — не Exited, а не создавались;
  * именованных томов sarex-postgres-data, sarex-redis-data,
    sarex-rabbitmq-data, sarex-minio-data в docker volume ls нет;
  * стек был взаимно несовместим: nginx публиковал 80/443, те же порты
    публикует k3s-server (внутри его контейнера на них сидит istio
    ingressgateway), и docker compose up nginx упал бы на bind;
  * подъём был выключен по умолчанию (sarex_compose_up: false).

Заодно убрано всё, что обслуживало только удалённые сервисы:

  * sarex_services, sarex_compose_up, задача up и poe-таски stack/up;
  * оба handler'а (пересоздание nginx/backend/celery) — файл handlers
    остался пустым и удалён;
  * генерация самоподписанного сертификата и cert_dir: TLS выписывает
    cert-manager внутри кластера, верхнего nginx больше нет;
  * каталоги nginx/, backend/, engine/ и задачи их копирования;
  * k3s/manifests/processing — DNS-мост на IP compose-postgres/minio,
    которых больше не существует (в кластере он и не был создан).

poe install теперь идёт через platform, а не через удалённый stack — это
заодно чинит старую дыру, из-за которой install не поднимал подложку.

Проверено: docker compose config отдаёт ровно 9 сервисов, совпадающих с
docker ps на aero-01; ansible-lint новых замечаний не даёт.
2026-08-10 12:55:04 +03:00
emelinda
da2a0b33a7 Reload: литеральный скаляр вместо свёрнутого, иначе команды склеиваются
Задача падала на первом же прогоне:
  error: unknown flag: --timeout
Свёрнутый скаляр (>-) схлопывает переносы строк в пробелы, две команды
превращаются в одну —
  kubectl rollout restart deploy/x kubectl rollout status deploy/x --timeout=...
и флаг уезжает к restart, который его не знает. С литеральным (|) переносы
сохраняются.

Баг был с самого добавления задачи: до сих пор её никто не запускал, оба
переката до этого делались вручную.
2026-08-10 08:26:24 +03:00
emelinda
cb4130ad9d Reload: добавлен django/celery — он держит django-configmap
celery монтирует production.py из django-configmap, но его envs в оверлее не
правятся, поэтому шаблон пода не меняется и Kubernetes его не пересоздаёт.
backend в такой же ситуации перекатывается сам — у него envs поправлены.

Без переката celery остался бы со старым HOST и строил бы ссылки на домен
чужого контура, тогда как backend уже отдаёт правильные.
2026-08-10 08:23:10 +03:00
emelinda
b5471e664d Django: убран домен чужого контура из настроек и адресов backend
Браузер получал 404 на
  https://sarex.contour.infra.sarex.tech/media/state/previews/....png
то есть backend отдавал АБСОЛЮТНЫЕ ссылки на чужой хост. Файлы при этом
лежали у нас — уходил не тот домен.

Источников оказалось три, и все в base:

  * django-configmap, HOST = "https://sarex.contour.infra.sarex.tech" —
    именно по нему строятся абсолютные ссылки на медиа;
  * там же CSRF_TRUSTED_ORIGINS с тем же доменом: без нашего домена django
    отклонял бы POST-формы админки по проверке Referer;
  * envs backend SERVER_API_HOST и SERVER_HOST. У celery те же две
    переменные я поправил раньше, а у backend пропустил.

Конфигмап скопирован в оверлей целиком: все настройки лежат одним значением
data.production.py на 331 строку, kustomize заменяет его только целиком —
та же причина, что у nginx-configmap рядом. Изменены ровно две строки,
остальное побайтно совпадает с base, так что diff остаётся читаемым.

После правки домена sarex.contour.infra.sarex.tech в рендере
clusters/aero/apps не остаётся ни одного вхождения.
2026-08-10 08:13:07 +03:00
emelinda
2474d82cea Istio: маршруту /media/ нужен rewrite, иначе ключ уезжает с префиксом
s3-proxy не срезает префикс сам — он отображает путь URL прямо в ключ
объекта. Маршрут отдавал ему путь как есть, поэтому он искал
media/orthophotos/..., тогда как в бакете лежит orthophotos/..., и отвечал
NoSuchKey (404).

Проверено на существующем объекте через сам s3-proxy:
  /orthophotos/NTG230721_DEM/14/10920/4937.png       -> 200, 17831 байт
  /media/orthophotos/NTG230721_DEM/14/10920/4937.png -> 404 NoSuchKey

В корне бакета лежат geotiff_dems/, las_pointclouds/, orthophotos/,
pointclouds/ — префикса media/ нет вовсе.
2026-08-10 01:44:53 +03:00
emelinda
1f336b6f93 Django: s3-proxy ходил в ЧУЖОЙ MinIO
Раздача медиа отвечала 500 на каждую плитку, а под капотом был настоящий
ответ S3:
  SignatureDoesNotMatch: The request signature we calculated does not match
  the signature you provided
  status code: 403, request id: 18CA4386CBFA3E5D

Ключ к разгадке — что ответ пришёл БЫСТРО (0.07 с) и с настоящим request id.
То есть это не таймаут до недоступного хоста: s3-proxy реально достучался до
чужого MinIO. В base адрес зашит внутри vault-шаблона:
  AWS_API_ENDPOINT=https://minio.contour.infra.sarex.tech
и это домен другого контура. Наши ключи там не подходят — к счастью, иначе
контур молча читал бы и писал в чужое хранилище.

Ровно та же ловушка, что была у backend и celery: значение внутри шаблона
аннотации, через values не перекрывается, поэтому заменяется аннотация
целиком. Тогда я поправил два релиза из трёх и до s3-proxy не дошёл.

После правки чужой домен в рендере clusters/aero/apps не встречается ни разу.
2026-08-10 01:35:27 +03:00
emelinda
be205b3b14 Processing: один секрет на все файлы задач, добавлен django-auth.json
Следующая задача падала на
  run.py:91  data = json.load(file)
  json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
то есть снова читала пустой файл — на этот раз django-auth.json.

Аннотации пода показали, почему: VAULT_MOUNT_PATH у движка ОДИН НА ВСЕ
файлы. Он подставляет его в каждую аннотацию, меняя только имя ключа:

  agent-inject-file-django-auth:     django-auth.json
  agent-inject-template-django-auth: {{- with secret
                                     "secrets/data/apps/processing/yc-s3" -}}

Ключа django-auth.json в том секрете не было, агент отрендерил пустоту.

Поэтому путь переименован в secrets/apps/processing/job-files: имя yc-s3
описывало один из файлов и врало о содержимом. В секрет добавлен ключ
django-auth.json — объект {"token": "..."}, а не голая строка, потому что
задача читает файл целиком через json.load. Значение то же, что у
workspaces в DJANGO_BASIC_AUTH: base64 от логина и пароля администратора.

Правило на будущее: появился новый файл, который движок кладёт в под
задачи, — добавляется ключ в этот же секрет, с именем ровно как у файла.
2026-08-10 01:11:53 +03:00
emelinda
3640bc8229 Istio: убран маршрут /workflows/, он перехватывал навигацию SPA
Адрес вида /workflows/<uuid> — это путь ВНУТРИ SPA платформы, который
должна обработать она сама, отдав index.html. Маршрут /workflows/ на
frontend-svc.processing уводил его в nginx микрофронтенда, где такого файла
нет, и браузер получал

  404 Not Found — nginx/1.19.6

Статика микрофронтенда доезжает и без этого маршрута: nginx платформы сам
проксирует /workflows/*.js в processing (apps/django/aero/nginx-configmap).
Это уже проверялось — 200, 282339 байт.

Маршрут /workflows/api/ оставлен: его nginx платформы не обслуживает, а
бандл ходит по нему относительным путём.
2026-08-10 01:05:37 +03:00
emelinda
f3c2fa194b Processing: у секрета задач своя схема JSON, не как у движка
403 Forbidden на HeadObject объяснялся не кредами и не отсутствием объекта —
и то и другое я проверил, они в порядке. Дело в схеме файла.

Движок читает /vault/secrets/processing-s3 с ключами host/bucket/verify, а
под задачи разбирает свой файл моделью S3AccountModel с ключами
endpoint/bucket_name/access_key_id/secret_access_key/use_ssl. Совпадают
только два имени из пяти.

Коварство в том, что pydantic на формате движка НЕ падает: access_key_id и
secret_access_key подходят по имени, а endpoint и bucket_name молча берут
значения по умолчанию. Поэтому вместо понятной ошибки конфигурации задача
шла в чужое хранилище и получала 403 — креды при этом верные.

endpoint пишется без схемы, протокол задаёт use_ssl; у нас MinIO внутри
кластера по http.
2026-08-10 00:50:03 +03:00
emelinda
ab086a86b4 Processing: S3 задачам через Vault и секрет реестра dockerhub
Два шага к тому, чтобы под задачи наконец поехал.

--- 1. S3 через vault-аннотации, а не Secret в namespace ---

Аннотации созданного пода показали точное поведение движка:
  agent-inject-secret-yc-s3:   <VAULT_MOUNT_PATH>
  agent-inject-template-yc-s3: {{- with secret "<VAULT_MOUNT_PATH>" -}}
                               {{ index .Data.data "yc-s3-service-account.json" }}
  secret-volume-path-yc-s3:    /etc/sarex/yc-s3

То есть VAULT_MOUNT_PATH вопреки имени — не корень KV, а ПОЛНЫЙ путь
секрета, а имя ключа в нём должно совпадать с именем файла. Путь исправлен
на secrets/data/apps/processing/yc-s3 (форма чтения KV v2), и заведён
соответствующий секрет с ключом yc-s3-service-account.json.

Инжекция уже подтверждена на живом поде: agent-inject-status: injected,
том vault-secrets-custom-0, Secret yc-s3 в namespace больше не монтируется.

--- 2. Секрет реестра для подов задач ---

Под задачи вставал в Init:0/1:
  Unable to retrieve some image pull secrets (dockerhub)
Движок проставляет Job'ам imagePullSecrets: dockerhub, и это имя зашито в
бинарник — настраиваемой переменной нет, из pull-настроек существует только
DEFAULT_IMAGE_PULL_POLICY. Поэтому заводим секрет с таким именем в namespace
задач через существующий механизм registry_secrets; содержимое то же, что у
regcred.
2026-08-10 00:22:48 +03:00
emelinda
36313e8f6f Processing: vault-аннотации для подов задач (VAULT_ROLE/SA/MOUNT_PATH)
Сборка contour_a03d37da-dirty2 умеет не монтировать в Job готовый Secret, а
вешать на него аннотации vault-agent — тогда файл с кредами кладёт агент, и
Secret в namespace задач не нужен. Так это и работает в UGMK, где никакого
yc-s3 в namespace нет.

В master workflows-engine этого кода нет (сборка с невлитой ветки), поэтому
набор переменных восстановлен по строкам бинарника: VAULT_ROLE, VAULT_SA,
VAULT_MOUNT_PATH плюс аннотации role/secret-volume-path/agent-inject-*.

В base задан только VAULT_USE=true, остальных трёх нет ни в одном кластере
репозитория. Оттого аннотации выходили неполными, агент в под задачи ничего
не клал и файл читался как пустой.

Значения предварительные: точный путь Vault, который движок запрашивает,
будет виден в аннотациях созданного пода задачи — по нему и уточним.
2026-08-10 00:10:31 +03:00
emelinda
0c0c3ebd9d Processing: поправлено обоснование про IGNORE_TAINTS_AND_NODE_SELECTOR
В комментарии стоял тег contour_3ef5b462 — он был взят грепом по репозиторию
ДО первого слияния с master, а master поднял образ до contour_a03d37da-dirty2
(коммит f55f254), и в кластере всё это время крутится именно он. Утверждение
"образ о переменной не знает" поэтому необоснованно: сборка ручная и сделана
тем же коммитом, что добавил переменную.

Сам вывод не меняется: селектор задаётся явно, потому что проверить поддержку
переменной нечем, а не потому что её точно нет.
2026-08-09 23:58:50 +03:00
emelinda
c8555d432c Merge remote-tracking branch 'origin/master' into aero 2026-08-09 23:38:14 +03:00
emelinda
e1a81d3dd8 Processing: секрет yc-s3 для подов задач workflows
Задача падала ещё до обращения к S3, на разборе конфигурации:
  pydantic ValidationError for S3AccountModel
  Value error, Expecting value: line 1 column 1 (char 0)
  ValueError: SERVICE_S3 is not valid
"Expecting value ... char 0" — это разбор пустоты: файла по пути не было.

Секрет secrets/minio/apps/processing из Vault при этом настроен верно, и
движок его видит (S3_SERVICE_ACCOUNT=/vault/secrets/processing-s3). Но в
поды задач движок его НЕ передаёт — для них у него отдельный механизм,
зашитый в код:

  services[S3Storage] = Service{
      CredentialsName: "yc-s3",
      CredentialsPath: "/etc/sarex/yc-s3",
      Envs: {"SERVICE_S3": `{"service_account":
             "/etc/sarex/yc-s3/yc-s3-service-account.json"}`},
  }
  (pkg/kube_services/services.go)

То есть он монтирует в Job обычный Secret с именем yc-s3 из namespace
задач (JOBS_NAMESPACE=processing). Ни имя секрета, ни путь, ни имя файла
не настраиваются.

vault-agent сюда не дотягивается по построению: поды задач движок создаёт
сам во время работы, аннотаций инжекции у них нет. Поэтому секрет заводит
ansible — tasks/app-secrets.yml, шаблон app-secrets.yaml.j2 и описание в
k8s_file_secrets. Формат содержимого повторяет
engine/yc-s3-service-account.json из compose-стека и совпадает с тем, что
vault-agent рендерит самому движку.

Манифест рендерится, применяется и сразу удаляется — в нём креды открытым
текстом, ровно как у секретов реестра. Namespace объявлен в том же
манифесте: на холодном старте Flux до слоя apps ещё не дошёл, а apply в
несуществующий namespace упал бы. Повторное создание безвредно, так же
заведён namespace vault.
2026-08-09 23:30:53 +03:00
emelinda
5efa1a5fd2 Processing: задачи не запускались из-за выключенного ENABLE_TOLERATION
Симптом: движок брал задачу и падал на её создании —
  ERROR create k8s job: create k8s job: unknown service
Ни одного пода при этом не создавалось.

Причина — моя правка. Я выставил ENABLE_TOLERATION=0, чтобы поды задач не
получали nodeSelector dedicated=processing на несуществующие выделенные
ноды. Но в движке ЭТОТ ЖЕ флаг управляет регистрацией классов ресурсов:

  if resCfg.EnableToleration {
      services[HighResources] = Service{...}
  }
  (pkg/kube_services/services.go)

Поэтому задача с service_request "high-resources" отваливалась не на
планировании, а раньше — на разборе, как неизвестный сервис. В логе при
этом ни слова про nodeSelector, связь с флагом неочевидна.

Вместо выключения флага селектор переведён на метку, которая есть на всех
трёх нодах контура, — kubernetes.io/os=linux. Класс сервисов остаётся
зарегистрированным, nodeSelector совпадает с любой нодой, toleration на
несуществующий taint безвреден. То же сделано для HIGH_MEM и PERSISTENT и
для DEFAULT_NODE_SELECTOR_*, которые раньше были пустыми строками.
Ресурсы не трогаем: в base это 1 CPU и 1Gi.

IGNORE_TAINTS_AND_NODE_SELECTOR, приехавший в base из master, для этого не
подходит: в исходниках workflows-engine такой переменной нет вовсе, образ
contour_3ef5b462 о ней не знает.
2026-08-09 23:00:31 +03:00
emelinda
6d6703ed2d BIM: расширение ltree и задача досоздания баз в живой СУБД
Продолжение разбора: после появления bimapidb оба пода упали на
  psycopg2.ProgrammingError: ltree type not found in the database
а миграция api — на
  type "ltree" does not exist
  LINE 2: ALTER TABLE bimelement ADD hierarchy ltree null;
models/database.py при инициализации зовёт register_ltree(conn), без
расширения приложение не стартует. Роль bim не суперпользователь и
CREATE EXTENSION сама не сделает — расширение обязан завести бутстрап,
как у sarex_db и workflow_db. Добавлено в contour.databases.

--- Почему понадобилась отдельная задача ansible ---

Базы и расширения описаны декларативно, но заводит их скрипт бутстрапа
ТОЛЬКО при старте пода postgresql. На живом кластере правка списка
доезжает до Kubernetes и не делает ничего: спецификация пода не меняется,
пересоздания нет. Flux при этом полностью зелёный, а приложение падает с
"database does not exist" — связь между причиной и симптомом неочевидна.

Альтернатива — перезапуск postgresql-0, но это обрыв коннектов django,
processing и workspaces ради одной базы.

Добавлены tasks/databases.yml, список sarex_databases в defaults и задача
`uv run poe dbsync`. Делает ровно то же, что бутстрап, но без простоя, и
идемпотентно: существующие базы пропускаются, расширения создаются через
IF NOT EXISTS. Расширения обрабатываются отдельным шагом и всегда, а не
только для новых баз, — расширение может появиться в списке позже самой
базы, ровно так и вышло с ltree.

Проверено на живом контуре: bim-api 2/2 (uWSGI поднял воркеров),
bim-worker 2/2 (celery подключился к брокеру, 8 задач зарегистрировано).
2026-08-09 21:55:27 +03:00
emelinda
3d205fa7e0 BIM: база bimapidb и обязательная переменная GOOGLE_APPLICATION_CREDENTIALS
Оба пода уходили в рестарты, причины разные и обе — мои.

1. bim-api:
     psycopg2.OperationalError: FATAL: database "bimapidb" does not exist
   Имя базы у приложения ЗАХАРДКОЖЕНО и переменной не задаётся — в
   entrypoint_api.sh (yoyo apply ... /bimapidb) и в models/database.py
   (PooledPostgresqlExtDatabase('bimapidb', ...)). Переменной DB_NAME в
   коде нет вообще, я подставлял её впустую. Запись в contour.databases
   переименована bim_db -> bimapidb, DB_NAME из vault-шаблона убран.

   Пустая bim_db остаётся в СУБД: бутстрап умеет только создавать. Данных
   в ней нет — проверено, ноль таблиц.

2. bim-worker:
     Exception: ENV_VARIABLE NOT SET: GOOGLE_APPLICATION_CREDENTIALS
   settings.py делает check_settings(): проходит по ВСЕМ ключам ENV_FIELDS
   и падает на первом незаданном, до проверки флагов дело не доходит.
   Убирать переменную вместе с Google-хранилищем было нельзя. Возвращена;
   файла по этому пути нет и не нужно — при FEATURE_ENABLE_GCLOUD=false
   его никто не открывает, важно лишь наличие самой переменной.

Отсюда правило для этого приложения: набор переменных должен повторять
apps/bim/dsinv целиком, образ там тот же (870965d1...). Менять можно
только значения — адреса, бакет, брокер, — но не состав.
2026-08-09 20:54:20 +03:00
emelinda
96d0d2e373 BIM в контуре aero: api и worker из aero/bimbackend
Разворачивается ИМЕННО bimbackend (Python, пара api + worker), а не
platform/bim-backend-v2 из apps/bim/base — тот откачен коммитом 285255e.
../base здесь намеренно не подключён.

Различить два приложения по образу нельзя: оба пушат bim-api. Различие в
тегах — у bimbackend полный SHA коммита (CI пушит bim-api:${CI_COMMIT_SHA}
с master), у bim-backend-v2 contour_*. Взят 870965d1..., тот же, что
крутится в dsinv.

Манифесты обычные, а не universal-chart: в репозитории сервис так и описан
(apps/bim/dsinv), своего чарта у него нет.

Отличия от dsinv:
  * секреты из Vault вместо secretKeyRef на несуществующие Secret'ы;
    штатный entrypoint файлы читать не умеет, поэтому обёрнут в sh -ec;
  * PROCESSING_API_URL и WORKSPACES_API_URL приведены к именам контура —
    в dsinv это workflows-service:8000 и workspaces-service:8000, здесь
    оба сервиса называются backend-svc и слушают 80;
  * RABBIT_HOST у воркера в dsinv указывает на rabbitmq-service.bim —
    брокера в этом namespace нет ни там, ни здесь; у api в том же файле
    адрес правильный. Поставлен общий брокер контура, vhost корневой;
  * S3_BUCKET переведён на общее медиахранилище sarex-media-storage;
  * убраны GOOGLE_APPLICATION_CREDENTIALS и том с ключом — Google в
    контуре нет, флаг FEATURE_ENABLE_GCLOUD и так выключен;
  * NodePort с прибитым 31352 заменён на ClusterIP: наружу сервис выходит
    через общий Gateway;
  * снят nodeSelector name=generic, imagePullSecrets переведён на regcred.

ИЗВЕСТНОЕ ОГРАНИЧЕНИЕ, принято осознанно. S3 у этого приложения на MinIO
не настраивается: в storage/s3.py адрес зашит в код —
  endpoint_url='https://storage.yandexcloud.net'
переменной окружения для него нет. Из контура он недостижим, поэтому
операции с файлами будут падать по таймауту, а остальные эндпоинты живы.
Альтернативы — пересборка образа с вынесенным endpoint либо перевод на
BIM_CURRENT_STORAGE=local (тип есть в storage/storages.py).

Наружу выведен на bim.sarex.local.lonsdaleites.ru, имя добавлено в
dnsNames сертификата. Тот же адрес приложение знает про себя через
BIM_API_EXTERNAL_HOST и строит по нему ссылки.
2026-08-09 20:42:23 +03:00
emelinda
285255e628 Откат BIM v2: развёрнут не тот сервис
Коммит c8eefab развернул platform/bim-backend-v2 (Go, один httpserver) —
он приезжает из apps/bim/base. Нужен же aero/bimbackend: Python, пара
api + worker, S3-бакет sarex-bim-storage, связки с processing и
workspaces. В iac он лежит не в base, а отдельными манифестами
bim-api.yaml и bim-worker.yaml (см. apps/bim/dsinv).

Спутать их легко: ОБА пушат в реестр один и тот же образ bim-api, и
различить можно только по содержимому — v2 пишет в лог
"Starting BIMv2 API server", а у bimbackend точки входа
entrypoint_api.sh и entrypoint_worker.sh.

Откатывается всё: релиз, маршрут наружу, имя в сертификате, адрес
BIMV2_INTERNAL_HOST у django и обвязка (роль Vault, секрет, regcred,
путь синхронизации). Заводить их заново под bimbackend дешевле, чем
править остатки под другой сервис — у него могут отличаться и namespace,
и имя базы.

База bim_db при этом НЕ трогается и пересоздания не требует: проверено
до отката — ноль таблиц, 9 МБ пустого шаблона. v2 к ней подключился, но
схему не создал, миграции не отработали.
2026-08-09 15:04:33 +03:00
emelinda
c8eefab4b6 BIM API в контуре aero + вывод наружу
Релиз идёт из base без единого патча, и это проверено по исходникам
bim-backend-v2, а не принято на веру:

  * DJANGO_HOST в base уже правильный (backend-svc.django:80) — редкость,
    у processing и workspaces там лежал несуществующий backend:8000;
  * DB_CERT_PATH_2/3/4 указывают на сертификат управляемой СУБД Яндекса,
    которого в контуре нет, но зануления не требуют: файл читается только
    под флагом ENABLE_SSL, а при нуле к строке подключения дописывается
    ?sslmode=disable и путь не трогается;
  * пять комплектов POSTGRES_*_N в vault-шаблоне смотрят на один хост —
    в проде это реплики, в контуре СУБД одна.

Django ходил в никуда: BIMV2_INTERNAL_HOST в base ведёт в namespace
bim-api к сервису bim-backend-v2-service, а релиз разворачивается в
namespace bim и называет сервис backend-svc. Поправлено в обоих релизах
django — backend и celery.

Наружу выведен отдельным поддоменом bim.sarex.local.lonsdaleites.ru, а не
префиксом на домене платформы: своего фронтенда у сервиса нет, общего с
платформой origin тоже, а его пути конфликтовали бы с маршрутом /api/
django. Имя добавлено в dnsNames сертификата — без этого браузер получил
бы сертификат без него.

Обвязка: regcred и роль Vault для namespace bim, apps/bim в
gitea_sync_paths, секрет secrets/apps/bim/postgres (база bim_db и роль bim
уже заведены в contour.databases).

ИЗВЕСТНЫЙ РИСК, проверяется логами первого пода. В ветке develop у
bim-backend-v2 миграции катятся при старте httpserver, ошибка фатальна, а
адрес БД внутри RunOnStartup взят не из конфига, а из константы
rc1b-sse4o3n9vea392g4.mdb.yandexcloud.net:6432. Из контура он недостижим.
Если этот код попал в образ contour_f9f2a39-dirty, под уйдёт в
CrashLoopBackOff и лечиться это будет только пересборкой образа.
2026-08-09 12:45:09 +03:00
emelinda
c24f02b078 MinIO: аннотация force у Job'а бакетов должна быть enabled, не "true"
Аннотации Flux force, prune и ssa принимают enabled/disabled, а не
булево. Написанное "true" Flux молча игнорировал.

Пока Job'а не существовало, это ничем себя не проявляло: обычный apply
проходил, бакеты заводились, всё выглядело рабочим. Сломалось на первой
же правке скрипта — spec.template у Job неизменяем, и слой
infra-controllers встал целиком:

  Job/minio/minio-buckets dry-run failed (Invalid):
    Job.batch "minio-buckets" is invalid: spec.template: ... field is immutable

а вместе с ним встал зависящий от него слой apps: измерения не получили
новый бакет не потому, что правка неверна, а потому что до них не дошла
очередь.
2026-08-09 12:11:14 +03:00
emelinda
c750a08f8d Measurements пишет в общий бакет контура + перекат нагрузок в ansible
--- Бакет ---

В base у measurements имя бакета ЗАШИТО в самом vault-шаблоне:
  "buckets":["measurements"]
Из-за этого он складывал файлы в собственный бакет, тогда как
медиахранилище контура одно — sarex-media-storage: туда пишет django и
оттуда же раздаёт s3-proxy на маршруте /media/. Ссылка на файл
измерения, отданная через django, вела бы в пустоту.

Шаблон переопределён в apps/measurements/aero так, чтобы имя бакета
читалось из секрета — как уже читается endpoint. Значение задаётся в
одном месте, vault_app_secrets, и совпадает с sarex_django_s3_bucket.
Заменить пришлось аннотацию целиком: значение внутри шаблона, через
values его не перекрыть.

Из Job'а бакетов убран measurements. Это не удаление: Job умеет только
mc mb --ignore-existing, ранее созданный бакет остаётся на месте — мы
лишь перестаём его заводить.

Порядок применения безопасен: в platform.yml vault-k8s.yml идёт до
gitea-sync.yml, то есть секрет получит ключ bucket раньше, чем Flux
пересоздаст под. Иначе vault-agent-init не отрендерил бы шаблон с
несуществующим ключом и под ушёл бы в Init:Error.

--- Перекат нагрузок ---

Единственное действие, которое до сих пор делалось руками мимо роли, —
rollout restart после правки ConfigMap. Оно понадобилось дважды (порядок
set/rewrite в nginx и auth_type) и оба раза выглядело как «правка не
поехала»: Flux рапортует Applied revision, а поведение старое.

Причина в том, что у релизов universal-chart в шаблоне пода нет
контрольной суммы конфига: ConfigMap меняется, спецификация пода — нет,
Kubernetes не видит причин пересоздавать под. Декларативно это здесь не
лечится — имя ConfigMap передаётся чарту строкой, монтирует он его сам, а
configMapGenerator переименовал бы ресурс, не поправив ссылку внутри
чарта, который рендерится уже в кластере.

Добавлены tasks/reload.yml, список sarex_reload_workloads в defaults и
задача `uv run poe reload`. По умолчанию выключено: перезапуск подов
посреди обычного деплоя происходить сам не должен.
2026-08-09 11:58:06 +03:00
emelinda
0a69d528b3 Dashboard: учётка для входа и бессрочный токен
Чарт дашборда своего пользователя не создаёт — он лишь показывает форму
ввода токена и ходит в API от имени того, чей токен введён. До сих пор
дашборд открывался, но войти в него было нечем.

Добавлены ServiceAccount dashboard-admin, привязка к встроенной роли
cluster-admin и Secret типа kubernetes.io/service-account-token.

Secret нужен именно ради бессрочности: с Kubernetes 1.24 ServiceAccount
не получает вечный токен автоматически, а kubectl create token выдаёт
короткоживущий. Secret с аннотацией kubernetes.io/service-account.name —
единственный штатный способ получить токен без срока действия: поле
token заполняет контроллер, в репозиторий оно не попадает.

Права взяты максимальные осознанно: дашборд задуман как инструмент
оператора — логи, exec, перезапуск нагрузок, — и с меньшими правами
половина экранов отдаёт ошибки доступа. Для закрытого тестового контура
это приемлемо; сузить до read-only можно заменой roleRef на встроенную
view, отозвать доступ — удалением Secret.
2026-08-09 11:31:29 +03:00
emelinda
f9874afb2b Istio: ждать готовности sidecar перед стартом приложения
Envoy перехватывает исходящий трафик через iptables-правила, которые
ставит init-контейнер istio-init. Правила появляются раньше, чем envoy
начинает слушать, поэтому приложение, дёрнувшее сеть в первые секунды,
получает не таймаут, а connection refused — пакет уже завёрнут на порт
envoy, а там ещё никого нет.

Так падал workflows-api: контейнер стартовал и умирал в ту же секунду,

  INFO  Starting workflows migrations
  INFO  init database connection...
  FATAL [dial tcp 10.43.195.4:5432: connect: connection refused]

при restartCount=0 у istio-proxy. Со второй попытки envoy успевал, и
дальше всё работало — то есть симптомом был ровно один рестарт на
старте, который легко списать на случайность.

Настройка сделана на уровне mesh, а не аннотацией на поде: уязвимы все,
кто ходит в БД или брокер сразу при запуске, а это почти каждый бэкенд
контура — django катит migrate первым делом, celery подключается к
rabbitmq, workspaces-api и engine-low к postgres. Они не падали не
потому, что устроены иначе, а потому что успевали.

Цена — каждый под стартует на время готовности envoy дольше, обычно
секунду-две: istio добавляет в sidecar postStart-хук, блокирующий запуск
остальных контейнеров. На Job'ы не влияет, у них инжекция отключена.

Действует в момент инжекции, поэтому на уже запущенные поды не
распространяется — хук появится при следующем пересоздании.
2026-08-09 11:27:34 +03:00
emelinda
7d90c23734 Дополнение к auth_type: правка видна только со второй загрузки
Config держит копию конфига в localStorage под ключом "config" и на
старте берёт синхронно кеш, сверяя файл асинхронно, а http-service.ts
читает auth_type при импорте модуля. Первая загрузка после правки лишь
обновляет кеш — применяется он со следующей. Без этой сноски выглядит
так, будто подмена config.json не сработала.
2026-08-09 09:44:23 +03:00
emelinda
f45802c0b8 Вход в контур без Zitadel: auth_type=original через /static/config.json
Платформа редиректила на zitadel.contour.infra.sarex.tech — чужой
контур, из aero недостижимый, — и падала с
  invalid_request: The requested redirect_uri is missing in the client
  configuration
то есть войти было нельзя вообще.

Zitadel во фронтенде выключается штатно и в рантайме. На старте он
делает fetch("/static/config.json") и, если там есть auth_type,
БЕЗУСЛОВНО перекрывает им режим, зашитый в сборку
(src/Model/api/http-service.ts в generic/sarex-frontend). Значений два:
zitadel и original; original — классический вход по логину и паролю с
JWT, ради которого backend и получает из Vault ключи RS512, а
SERVER_ZITADEL_ENABLED в оверлее уже стоит в False.

Файл отдаётся прямо из nginx, а не подкладывается томом: static/config.json
в sarex-frontend лежит в .gitignore и заполняется на каждом стенде своим —
это штатная точка расширения. В апстримном nginx.conf под него заведён
такой же location = с no-store, только читающий с диска.

Через localStorage подменить нельзя: оверрайд оттуда действует лишь для
endpoint'ов stage, preprod и local, contour в этом списке нет.
2026-08-09 09:24:31 +03:00
emelinda
00056b9150 Processing: выровнены индексы envs после слияния master
master добавил в apps/processing/base/engine-low.yaml переменную
IGNORE_TAINTS_AND_NODE_SELECTOR, из-за чего всё начиная с индекса 21
сдвинулось на единицу. Патчи оверлея адресуются по индексу, и сборка
слоя apps упала:

  testing value /spec/values/services/backend/envs/22/name failed

Это ровно то, ради чего перед каждым replace стоит op: test — вместо
тихой перезаписи соседней переменной получили явную остановку сборки.
Индексы сдвинуты: 21→22, 22→23, 25→26, 26→27, 27→28, 28→29, 30→31,
31→32, 32→33, 66→67, 69→70, 70→71. Индексы 2, 5 и 6 не изменились.

Сама новая переменная патча не требует: в base у неё "true", то есть
апстрим теперь по умолчанию игнорирует taint'ы и nodeSelector — как раз
то поведение, ради которого здесь зануляются ENABLE_TOLERATION и
DEFAULT_NODE_SELECTOR_*. Прежние правки оставлены: они не мешают, а
убирать их вслепую, не зная приоритета флагов внутри engine, рискованно.
2026-08-09 09:24:31 +03:00
emelinda
46616f2260 Merge remote-tracking branch 'origin/master' into aero 2026-08-09 09:15:00 +03:00
emelinda
d320af6ad7 Workspaces: зафиксирован выбор версии фронтенда по данным реестра
Комментарий в оверлее переписан с «в бандле есть srx_workspaces» на
проверяемые факты. Прежняя формулировка была получена grep'ом по
подстроке, которая матчит и srx_workspacesV2, — вывод оказался верным
по случайности.

Что проверено:
  * в бандле платформы из 18 упоминаний remoteEntry.js воркспейсовое
    одно, /workspaces-v2/..., и встречается только srx_workspacesV2;
  * отдаваемый контуром /module/remoteEntry.js объявляет то же имя;
  * в cr.yandex у workspaces-v2-frontend 61 тег contour_*, у v1
    (workspaces-frontend) ни одного, образа workspace-frontend нет.

Тег contour_2a4ce3fd из base — самая свежая contour-сборка: по датам
создания образов в реестре она от 2026-07-24, предыдущая от 2026-07-19.
Собирает такие образы джоба build_contour из
generic/build-contour-frontend по правилу «ветка master и
ENABLE_BUILD_IMAGE_CONTOUR=true», то есть contour_* всегда с мастера.
Переопределять тег в оверлее не нужно.
2026-08-09 01:05:26 +03:00
emelinda
e55958f7cb Workspaces в контуре aero: api и фронтенд-ремоут
apps/workspaces/aero — оверлей поверх base:

  * DJANGO_HOST в base неверен дважды: сервиса backend не существует
    (он backend-svc), и 8000 — это targetPort контейнера, а сервис
    принимает на 80. Проверено в кластере: backend-svc:80 отдаёт 200,
    backend-svc:8000 таймаутит, backend:8000 не резолвится. Та же
    ошибка была в base у processing/engine-low.
  * ENVIRONMENT/DJANGO_ORIGINATOR — «prod» заменён на aero: значение
    уезжает в Sentry и в заголовки запросов к смежным сервисам.
  * DOCUMENTATION_HOST переписан на честное имя внутри кластера.
    Выключить интеграцию нечем: DOCUMENTATION_LOGGER_FEATURE гасит
    только логирование, а GetDocumentByWS вызывается из обработчика
    GET workspace безусловно. Сервиса documentations в контуре нет и
    не будет — карточка воркспейса отдаётся без документа.

Фронтенд идёт из base без патчей и наружу НЕ публикуется: это Module
Federation remote srx_workspaces, его подгружает главный фронтенд
платформы. Версия — v2, и это не выбор: в бандле
sarex-frontend-dev:contour_5.22.0 лежит строка
/workspaces-v2/module/remoteEntry.js и имя ремоута srx_workspaces,
обращений к v1 нет вовсе. У v1 (platform/workspaces-frontend) к тому же
нет ни одной contour-сборки.

nginx-configmap: исправлен порядок директив в трёх location'ах.
set обязан идти ДО rewrite ... break — обе принадлежат
ngx_http_rewrite_module, и флаг break прекращает обработку его
директив, поэтому set после него не выполнялся. Переменная оставалась
пустой, nginx писал «no host in upstream :80» и отдавал 500. Так падали
и /workspaces-v2/..., и /workflows/... — маршрут / при этом работал,
из-за чего дефект и не был замечен раньше.

Обвязка: regcred и роль Vault для namespace workspaces, apps/workspaces
в gitea_sync_paths, секреты secrets/apps/workspaces/postgres и
secrets/vault/common/django_auth. Последний — вопреки имени пути НЕ
токен Zitadel: workspaces читает оттуда ключ key и кладёт его в
DJANGO_BASIC_AUTH, то есть это base64 логина и пароля администратора
django. Совпадение проверено в кластере по sha256.
2026-08-09 00:15:46 +03:00
emelinda
77204c1550 Processing: фронтенд включён, api и фронтенд выведены наружу
Фронтенд был исключён из-за адресов, вшитых в бандл на этапе сборки через
константу __BUILD_ENV__. Опасение не подтвердилось: сборка обращается к тому
же origin относительными путями — иначе в yc-k8s-test и d8-ugmk-prod не
понадобился бы отдельный маршрут /workflows/api/ перед /workflows/.

Маршруты добавлены на домен платформы, а не на отдельный поддомен: поддомен
потребовал бы пересборки образа с другим __BUILD_ENV__.

  /workflows/api/  -> rewrite /api/ -> backend-svc.processing
  /workflows/      -> rewrite /     -> frontend-svc.processing

Порядок внутри пары значим: /workflows/api/ обязан идти перед /workflows/,
иначе запросы к API уйдут в раздачу статики. Оба стоят перед /api/ и / —
правила django их бы перехватили.

Маршруты положены в существующий VirtualService platform, а не в отдельный:
Istio не сливает несколько VirtualService на одной паре хост+gateway, и часть
правил молча потерялась бы.
2026-08-08 22:59:06 +03:00
emelinda
c7c194feaa Processing в контуре aero: api и engine-low
Развёрнут частично, и это осознанно:
  * engine удалён — в base у него replicaCount 0, то есть он выключен и там,
    а образ прибит к тегу с опечаткой в имени (workflows-endigne_prod);
  * frontend удалён — адреса бэкендов зашиты в образ на этапе сборки через
    __BUILD_ENV__, и ни один готовый профиль не смотрит на домен контура.
    Подключать его имеет смысл только вместе с пересборкой образа.

Отключены смежные сервисы, которых в контуре нет: documentations (PDM,
filestream), resources, bim-api, workspace, issues, comparisons, mailgun,
SMTP. Оставлен только S3. Вместе с флагом ENABLE_SMTP убрана аннотация
инжекции secrets/vault/common/smtp_auth — секрета нет, и vault-agent-init
оставил бы под в Init:Error, не дойдя до флага.

S3 приезжает из Vault отдельным файлом: приложение читает по пути из
S3_SERVICE_ACCOUNT целый JSON, а не пару переменных. Набор ключей повторяет
engine/yc-s3-service-account.json из compose-стека, который так же смотрел
в MinIO.

Планирование подов задач: engine работает kubernetes-исполнителем и сам
создаёт Job'ы, подставляя им nodeSelector dedicated=processing. Выделенных
нод в контуре нет, поды висли бы в Pending — как это уже было у postgresql.
ENABLE_TOLERATION и DEFAULT_NODE_SELECTOR_* сняты.

Заодно у celery поправлены четыре адреса на wb.sarex.io — чужой контур,
недостижимый из aero. У backend те же переменные уже вели внутрь кластера;
при WORKFLOWS_USE=1 расхождение давало бы зависания вместо явной ошибки.

Патчи адресуются по индексу env, поэтому перед каждым replace стоит op: test
на имя переменной: смена порядка в base уронит сборку явно, а не перезапишет
соседнее значение.
2026-08-08 20:51:02 +03:00
emelinda
51b9120797 Measurements в контуре aero + декларативное заведение бакетов
Django обращается к сервису измерений при MEASUREMENTS_USE_MEASUREMENTS=1
(значение по умолчанию в base), так что без него часть API отвечала ошибкой.

Сервис переименован в measurements-service, как в brusnika-*. В base он
называется measurements-svc, а django ходит на measurements-service —
переименовать сервис дешевле, чем патчить переменную в двух релизах django.

Бакеты теперь заводит Job в infrastructure/minio/aero, а не команда руками.
Чарт MinIO этого не умеет: форк не принимает ни buckets, ни provisioning.
Так бакет django был создан императивно и не пережил бы пересоздание
контура — развернув всё с нуля, получили бы работающий MinIO и приложения,
падающие на отсутствующем бакете. Measurements сломался бы на этом сразу:
имя бакета зашито в его vault-шаблоне. Идемпотентность даёт
mc mb --ignore-existing.

Сайдкар istio у Job отключён: контейнер задачи выходит, envoy продолжает
работать, и под навсегда остаётся Running.
2026-08-08 20:41:23 +03:00
emelinda
237dcb9b28 RabbitMQ: снят loopback-only для администратора
Celery не поднимался: worker падал в CrashLoopBackOff на
amqp.exceptions.AccessRefused (403) при подключении к брокеру.

Диагностика уводила в сторону — 403 читается как несовпадение пароля, но
пароль был верный: хеши значения в Vault, в файле, смонтированном в под
брокера, и в собранном BROKER_URL совпадали, а rabbitmqctl authenticate_user
с ним проходил. Настоящая причина нашлась в логе брокера:

  PLAIN login refused: user 'rabbit' can only connect via localhost

Пользователю разрешён вход только с localhost, поэтому отбивалось любое
подключение из кластера — и по той же причине в management UI нельзя было
войти снаружи.

Лечится так же, как в d8-ugmk-prod: флагом auth.enableLoopbackUser и
дублирующим advancedConfiguration. Дубль не избыточен — флаг лишь пишет в
rabbitmq.conf строку "loopback_users.<user> = false", которую брокер не
применяет: в рантайме loopback_users всё равно оставался [<<"rabbit">>].
Значение задаёт именно advanced.config.
2026-08-08 18:32:38 +03:00
emelinda
c2a9bae0db Celery включён в контуре aero
Worker был отключён патчем удаления, пока не было очередей. RabbitMQ и MinIO
развёрнуты, схема БД накачена — включаем.

Расхождения с base у celery ровно те же, что у backend: нет Kafka, MinIO
живёт внутри кластера. Аннотации инжекции несуществующих секретов убраны —
иначе vault-agent-init не отрендерит шаблон и под навсегда останется в
Init:Error. SERVER_ZITADEL_ENABLED не трогаем: в base у celery он уже False.

Миграции и создание администратора в стартовый скрипт worker'а намеренно НЕ
добавлены: схемой владеет backend, а два процесса, катящих миграции при
одновременном старте, — гонка на ровном месте.

Тег образа выравнен с backend (production_f813140d вместо production_a96dead0).
Разные версии кода у web и worker ломаются не при старте, а на конкретной
задаче, когда worker десериализует аргументы по своей версии модели.
2026-08-08 18:12:32 +03:00
emelinda
bddcc955fe Создание администратора платформы перенесено в под backend
Учётка создавалась задачей ansible через kubectl exec — по явной команде
оператора и в обход GitOps. Теперь это делает сам под при каждом старте:
стартовый скрипт подхватывает креды из Vault и вызывает штатную
createsuperuser --noinput.

Идемпотентность обеспечена ветвлением, а не подавлением ошибки: на уже
существующем логине Django возвращает "That username is already taken" с
ненулевым кодом, что под set -e из command базы уронило бы под на втором
запуске. Ошибка ожидаема и гасится веткой else.

Заодно в стартовый скрипт добавлен migrate. В entrypoint.sh образа он уже
есть, но запущен без set -e — его падение не мешает uwsgi стартовать.
Именно так контур несколько часов отдавал 200 на /admin/ с пустой схемой:
backend не мог аутентифицироваться в СУБД, миграции падали, приложение
работало. Здесь команда идёт под set -e, поэтому отказ виден сразу.

Ansible остаётся владельцем значений: генерирует логин и пароль (теперь
безусловно, а не по флагу — их ждёт vault-agent) и кладёт в
secrets/apps/django/superuser. Задача poe superuser печатает креды.

Убраны временные диагностические задачи с ignore_errors, добавленные при
разборе отказа createsuperuser.
2026-08-08 18:10:19 +03:00
emelinda
ee1881e516 feat(aero): django в k3s — backend, frontend, s3-proxy и выход наружу
Первый прикладной сервис переехал из compose в кластер. Развёрнуты backend,
frontend, redis, srx-admin и s3-proxy; celery выключен до следующего шага.
Платформа доступна по https://sarex.local.lonsdaleites.ru — корень отдаёт
frontend, /api/ и /admin/ уходят в backend, /media/ в s3-proxy.

Kafka и Zitadel в контуре не развёрнуты, и приложению они не нужны:
SERVER_KAFKA_ENABLED в base уже False, а zitadel_enabled в sarex-backend
читается ровно в одном месте — update_ams_user(), которая при выключенном
флаге сразу выходит. Но аннотации инжекции этих секретов пришлось убрать:
vault-agent-init не рендерит шаблон для несуществующего секрета и роняет под
в Init:Error ещё до старта приложения.

nginx-configmap заменён целиком. В base зашиты upstream'ы на namespace pm,
workspaces и processing; nginx резолвит их при старте и падал с
"host not found in upstream". pm убран как ненужный, для остальных двух
адрес вынесен в переменную с resolver — их резолвинг откладывается до
запроса, поэтому появление сервисов позже не потребует правки конфига.
Заменять пришлось именно патчем: объект с тем же kind+name уже приходит из
base, и добавление его в resources валит сборку слоя целиком.

Секреты django в Vault: postgres, rabbitmq, minio и общие RSA-ключи JWT.
RabbitMQ и MinIO подключены под административными кредами — отдельных
пользователей приложения чарты создавать не умеют, это осознанный долг.

Задача создания суперпользователя переписана с docker compose exec на
kubectl exec. ВНИМАНИЕ: она пока НЕ РАБОТАЕТ — команда падает, вывод скрыт
no_log. Механизм проверен отдельно и исправен, значит ошибка на стороне
Django. Чтобы увидеть причину, нужно временно снять no_log.
2026-08-08 16:49:16 +03:00
emelinda
8e14cd1e0b feat(aero): PostgreSQL с четырьмя базами и ускоренный цикл реконсиляции
PostgreSQL разворачивается пустым инстансом, состав баз повторяет то, что
сейчас провижинит postgres-init в compose:
  sarex_db     -> django       (ltree)
  workflow_db  -> processing   (uuid-ossp, ltree, hstore)
  bim_db       -> bim
  workspace_db -> workspace
Пароли администратора и владельцев берутся из Vault через
vault-agent-injector, в репозитории их нет. Расширения создаёт бутстрап
заранее: роли не суперпользователи и CREATE EXTENSION в миграциях им
недоступен. timescaledb включён в shared_preload_libraries сразу, хотя этим
базам не нужен, — параметр читается только при старте, и добавить его позже
означает перезапуск СУБД.

nodeSelector снимается postRenderers'ом. Чарт по умолчанию ставит
dedicated: sts в расчёте на выделенный пул нод; в контуре таких нет, и под
навсегда повисал в Pending, а следом застревал PVC, потому что у local-path
режим WaitForFirstConsumer. Через values это недостижимо: Helm сливает карты
по ключам, поэтому ни {}, ни другая метка dedicated: sts не убирают.

Цикл реконсиляции сокращён с 10 до 2 минут у всех Kustomization и
HelmRelease контура. Корневая Kustomization патчится из clusters/aero:
её файл генерирует flux bootstrap с пометкой DO NOT EDIT, а флаг --interval
задаёт периодичность только GitRepository. Таймаут установки postgresql
снижен с 20 до 10 минут — он определяет цену одной неудачной итерации.

Отключение собственных Gateway/VirtualService/Certificate чарта rabbitmq
переведено с JSON6902 на postRenderers: kustomize нормализует null в {}, а
пустую карту Helm сливает с дефолтами, поэтому настоящий null до Helm не
доходит ни одним путём.
2026-08-08 11:33:43 +03:00
emelinda
565faa0e17 feat(aero): базовый слой контура — Vault↔k3s, GitOps-синхронизация, выход наружу
Топология k3s: 1 мастер + 2 воркера (убран k3s-worker-3 с томом).

Vault ↔ k3s. Vault живёт в compose, вне кластера, поэтому получает
статический IP 172.28.0.13, DNS-мост в namespace vault и token reviewer
(SA vault-auth + system:auth-delegator). Ansible включает auth/kubernetes,
передавая адрес apiserver, CA и JWT ревьюера явно, и заводит политику с
ролями. Секрет token reviewer'а читается целиком в JSON: вариант
-o jsonpath={.data.ca\.crt} НЕ работает — модуль command разбирает строку
через shlex, съедает обратный слэш, kubectl возвращает пустую строку с
кодом 0, и отказ остаётся незамеченным до падения vault write.

Синхронизация репозитория в gitea. Нужное подмножество путей (замыкание
ссылок clusters/aero) едет архивом на хост и коммитится там: gitea не
публикуется дальше самого хоста, контур остаётся замкнутым. flux-bootstrap
вынесен в отдельную волну — до неё Vault получает auth/kubernetes, а
репозиторий наполняется, иначе первая реконсиляция падает на пустом репо.

Инфраструктура: vault-agent-injector (чарт vault-contour в режиме внешнего
Vault), local-path-provisioner взамен встроенного в k3s (--disable=
local-storage; путь данных прежний), rabbitmq и minio с кредами из Vault.

Выход наружу: порты 80/443 k3s-server опубликованы (istio ingressgateway
занимает эти hostPort), общий contour-gateway на wildcard-хост, сертификат
Let's Encrypt через http01. Имена в сертификате перечислены явно —
http01 не выдаёт wildcard, для них нужен dns01.

Разорваны два дедлока Flux: infra-configs больше не зависит от
infra-controllers (издатели не должны зависеть от здоровья чартов, которые
их используют), istio-config получил disableWait.

Модули ядра iptable_nat и смежные грузятся на хосте: ноды k3s —
контейнеры, istio-init правит iptables через ядро хоста, а на RED OS 8
(nf_tables) legacy-модули не загружены, из-за чего любой под с
istio-injection навсегда вставал в Init:Error.

Собственные Gateway/VirtualService/Certificate чартов rabbitmq и dashboard
отключены — маршрутизация описана централизованно. У rabbitmq это сделано
postRenderers, а не values: kustomize нормализует null в {}, а пустую карту
Helm сливает с дефолтами, возвращая их целиком.
2026-08-08 09:36:08 +03:00
emelinda
da8e40c044 feat(istio): add required empty blocks to prevent nil pointer errors in aero environment configuration 2026-08-03 16:20:37 +03:00
emelinda
4ae7a8286b feat(fluxcd): add Istio gateway HelmRelease configuration with kustomize patching in aero environment 2026-08-03 16:08:12 +03:00
emelinda
b28e5dd27a feat(fluxcd): introduce multi-layer infrastructure setup with kustomize and CRD dependency handling in aero environment 2026-08-03 16:02:35 +03:00
emelinda
037d069c85 feat(fluxcd): add HelmRelease for Istio configuration in aero environment 2026-08-03 15:40:24 +03:00
emelinda
457f0dad41 feat(ansible): add private registry configuration for k3s and Flux with secure key handling 2026-08-03 15:40:07 +03:00
emelinda
8a8cd66f59 feat(ansible): add GitOps foundation (k3s, gitea, vault, and FluxCD) with multi-wave deployment support 2026-08-03 14:49:22 +03:00
emelinda
e39404cb4c Merge branch 'master' of gitlab.sarex.io:infra/iac into aero 2026-08-03 12:19:09 +03:00
emelinda
262401efe1 Merge branch 'master' of gitlab.sarex.io:infra/iac into aero 2026-07-30 21:22:06 +03:00
emelinda
5105eee602 feat(ansible): деплой engine + k3s DNS-моста в роли sarex_stack 2026-07-30 11:52:50 +03:00
emelinda
714dc55db1 feat(engine): сервис workflows-engine (k8s-исполнитель через kubeconfig) 2026-07-30 11:51:58 +03:00
emelinda
a7c97027ae feat(k3s): processing-k8s-init — применение DNS-моста через kubectl 2026-07-30 11:51:24 +03:00
emelinda
934ccbbb73 feat(k3s): DNS-мост postgres/minio в namespace processing 2026-07-30 11:50:50 +03:00
emelinda
4c4bb6f8a4 feat(net): статические IP postgres/minio для k8s-моста 2026-07-30 11:50:30 +03:00
emelinda
497ca1342d feat(engine): образ workflows-engine + S3 SA-плейсхолдер 2026-07-30 11:49:37 +03:00
emelinda
383d19157c docs: план реализации — workflows-engine через k3s 2026-07-30 11:41:54 +03:00
emelinda
5f84c8d19b docs: spec — workflows-engine в контуре через k3s (планирование Job'ов) 2026-07-30 11:39:11 +03:00
emelinda
9a39f4568e Merge branch 'master' of gitlab.sarex.io:infra/iac into aero
# Conflicts:
#	.gitignore
2026-07-30 10:41:18 +03:00
emelinda
e122cc6f0b Drop dead DJANGO_HOST env from processing-api
workflows-api config reads only HTTP/Postgres/JWT PUBLIC_KEY; DJANGO_HOST was
never consumed by the code (leftover from the k8s manifest).
2026-07-24 18:26:32 +03:00
emelinda
6244a05c19 Wire backend to processing-api, provision workflow_db extensions, add deploy automation
- backend/celery: WORKFLOWS_USE/HOST/BASE_HOST/PREFIX -> processing-api
  (WORKFLOWSSETTINGS builds COMPARISON/DOCUMENTATION/PDM_FILES URLs)
- postgres-init: create uuid-ossp/ltree/hstore in workflow_db as superuser
  (processing role lacks CREATE EXTENSION) - fixes workflows-api migration
- sarex_stack: handlers recreate nginx / backend+celery when their mounted
  configs change (docker compose up -d does not detect bind-mount edits)
- poe: operational tasks up/ps/pull/logs/restart on the host
2026-07-24 18:22:02 +03:00
emelinda
b7f4d07544 Integrate S3 media storage via custom backend, add s3-proxy service, update Nginx for media routing, and include processing services in stack. 2026-07-24 17:48:49 +03:00
emelinda
f9bde75ed7 add SAREX_MODULES 2026-07-23 19:52:14 +03:00
emelinda
5ef655f1bc Add production backend configuration with classical JWT authentication, update provisioning tasks, and include production.py in docker-compose and Ansible workflows 2026-07-23 19:24:32 +03:00
emelinda
e4953042c0 Add JWT key generation for backend, update provisioning and deployment tasks, enhance docker-compose with protected Redis, and refine Ansible workflow with new install sequence and application stack management 2026-07-23 19:09:22 +03:00
emelinda
0f9969ddad Update backend image to production version in .env.example and docker-compose.yaml, refine Ansible tasks for conditional superuser creation 2026-07-23 18:29:46 +03:00
emelinda
8551d58d08 Update frontend image in .env.example for contour compatibility 2026-07-23 18:21:11 +03:00
emelinda
cb37ba000e Update frontend image in .env.example for contour compatibility 2026-07-23 17:26:04 +03:00
emelinda
75c47e932f Update frontend Dockerfile for contour with relative paths and adjust docker-compose.yaml to use contour-specific image. 2026-07-23 17:25:25 +03:00
emelinda
4d9b1807bf Add uWSGI configuration for backend with http-socket and worker settings. Update Nginx template, .env.example, Ansible tasks, and docker-compose.yaml to integrate uWSGI and dynamic image references. 2026-07-23 16:14:37 +03:00
emelinda
2c3fce7834 Add idempotent Postgres initialization service for app-specific roles and databases. Update .env.example and docker-compose.yaml accordingly. 2026-07-23 15:13:11 +03:00
emelinda
83a8339db7 Add base and sarex_stack Ansible roles with provisioning for Docker hosts, system configuration, Docker installation, sarex-stack deployment, and secrets handling. 2026-07-23 15:04:14 +03:00
emelinda
3a2ee9d6b6 Add init script for Postgres roles/databases, Processing and Measurements services, and update .env.example, docker-compose.yaml, and Nginx configuration 2026-07-22 18:37:24 +03:00
emelinda
333677dedf Add app-specific MinIO user with scoped bucket policy, update .env.example, docker-compose.yaml, and Nginx configuration accordingly 2026-07-22 17:59:57 +03:00
emelinda
0ad515e6c4 Add script to generate a self-signed TLS certificate for Nginx and update configurations to enable HTTPS with default and custom domains. 2026-07-22 17:04:52 +03:00
emelinda
ea23b885c4 Comment out unused ports, backend/frontend dependencies, and Nginx routes. Switch to custom registry images for Redis, RabbitMQ, MinIO, and MinIO Client in docker-compose.yaml. 2026-07-22 16:43:45 +03:00
emelinda
d87af910d6 Refactor Nginx configuration to use templated files and support multiple domains (platform, admin, MinIO). Update .env.example and docker-compose.yaml accordingly. 2026-07-22 16:06:55 +03:00
emelinda
93e9d9fed0 Add Nginx reverse proxy, infrastructure services (Postgres, Redis, RabbitMQ, MinIO), and application setup to docker-compose.yaml with updated .env.example and Nginx configuration 2026-07-22 14:51:41 +03:00
emelinda
eb2748170a Add external K3S_API_HOST and Gitea service to docker-compose.yaml 2026-07-22 14:25:29 +03:00
emelinda
361b7155bf Update volume name from k3s-agent-data to k3s-worker-data in docker-compose.yaml 2026-07-21 17:21:36 +03:00
emelinda
fb001710c3 add k3s/*
k3s-master/*
2026-07-21 17:17:30 +03:00
emelinda
b8c29be7a5 add ./k3s/*
./k3s-master/*
2026-07-21 17:12:59 +03:00
emelinda
2bbee05013 Add docker-compose.yaml for local K3s server and agent setup with example .env file 2026-07-21 17:04:18 +03:00
55 changed files with 10398 additions and 1 deletions

25
.dockerignore Normal file
View File

@ -0,0 +1,25 @@
# В контекст сборки нужны только манифесты: clusters/, apps/, infrastructure/
# и скрипты из hack/. Всё остальное — история, IDE, секреты — только раздувает
# контекст и рискует уехать в образ.
.git
.gitignore
.idea
.claude
CLAUDE.md
.env
.env.*
docs
*.md
aero
inventory.yaml
tmp
.tmp/
snapshots
out
# Секреты и ключи не должны попадать в контекст сборки даже случайно.
*.pem
*.key
*.p12
kubeconfig*
*service-account*.json

13
.gitignore vendored
View File

@ -2,7 +2,20 @@
.claude
CLAUDE.md
.env
.env.*
tmp
.tmp/
snapshots/*
# Рантайм-артефакты контура aero (gitea-data, k3s-storage, ключи реестра)
# переехали в infra/aero-deploy вместе с docker-compose, который их создаёт.
# Здесь остаётся только gitea-data: каталог мог остаться от прежней раскладки.
gitea-data/
# --- Секреты и ключи: не коммитить никогда ---
*.pem
*.key
*.p12
kubeconfig*
*service-account*.json

77
Dockerfile Normal file
View File

@ -0,0 +1,77 @@
# Полная сборка манифестов IaC-репозитория для одного контура.
#
# docker build --build-arg CONTOUR=aero -t iac:aero .
# docker run --rm iac:aero list # что собралось
# docker run --rm iac:aero cat clusters # слои Flux одним потоком
# docker run --rm -v "$PWD/out:/export" iac:aero export
#
# Контур — имя каталога в clusters/ (aero, brusnika-prod, dsinv, wb, ...).
# Тот же идентификатор служит именем overlay внутри apps/* и infrastructure/*,
# поэтому одного аргумента хватает на весь рендер.
#
# Сборка офлайновая: kustomize build разворачивает оверлеи и патчи, а
# HelmRelease остаётся Custom Resource'ом — чарты тянет Flux уже в кластере.
# Ни helm, ни доступ к OCI-реестру здесь не нужны. Исключение — валидация:
# схемы CRD kubeconform берёт из сети (отключается VALIDATE=0).
ARG KUSTOMIZE_VERSION=v5.4.3
ARG KUBECONFORM_VERSION=v0.6.7
FROM registry.k8s.io/kustomize/kustomize:${KUSTOMIZE_VERSION} AS kustomize
FROM ghcr.io/yannh/kubeconform:${KUBECONFORM_VERSION} AS kubeconform
FROM alpine:3.20 AS builder
# Контур обязателен по смыслу, но значение по умолчанию оставлено: сборка без
# --build-arg собирает aero, а не падает на пустой переменной.
ARG CONTOUR=aero
ARG VALIDATE=1
ARG KUBERNETES_VERSION=1.30.0
COPY --from=kustomize /app/kustomize /usr/local/bin/kustomize
COPY --from=kubeconform /kubeconform /usr/local/bin/kubeconform
WORKDIR /src
# Скрипт копируется отдельным слоем: правка манифестов не инвалидирует его,
# а правка скрипта не тянет за собой пересборку базовых слоёв.
COPY hack/render-contour.sh /usr/local/bin/render-contour
RUN chmod +x /usr/local/bin/render-contour
COPY . .
RUN render-contour "${CONTOUR}" /out
# Валидация отдельным слоем: смена версии Kubernetes не пересобирает рендер.
# --ignore-missing-schemas пропускает ресурсы, чьих схем нет в каталоге
# (часть CRD), но не заглушает ошибки в тех, что нашлись.
RUN if [ "${VALIDATE}" = "1" ]; then \
kubeconform \
-kubernetes-version "${KUBERNETES_VERSION}" \
-strict \
-ignore-missing-schemas \
-schema-location default \
-schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' \
-summary \
/out/*.yaml ; \
else \
echo "VALIDATE=0 — проверка схемами пропущена" ; \
fi
FROM alpine:3.20
ARG CONTOUR=aero
ENV CONTOUR=${CONTOUR}
LABEL org.opencontainers.image.title="iac-manifests" \
org.opencontainers.image.description="Собранные манифесты контура ${CONTOUR}" \
ru.iac.contour="${CONTOUR}"
COPY --from=builder /out /manifests
COPY hack/entrypoint.sh /usr/local/bin/entrypoint
RUN chmod +x /usr/local/bin/entrypoint
ENTRYPOINT ["/usr/local/bin/entrypoint"]
CMD ["list"]

View File

@ -60,7 +60,7 @@ Superset, Trino, GoAlert, GlitchTip, argo-*, otel-*) **существуют об
| Репозиторий | Зона ответственности | Ключевое содержимое |
|---|---|---|
| ★ [infra/iac](https://gitlab.sarex.io/infra/iac) ✅ | **Основной GitOps-монорепозиторий (Flux v2).** Декларативное состояние 9 кластеров: инфраструктурные компоненты + прикладные сервисы | `clusters/`, `infrastructure/` (36), `apps/` (37), `inventory.yaml`, `docs/apps/` |
| ★ [infra/iac](https://gitlab.sarex.io/infra/iac) ✅ | **Основной GitOps-монорепозиторий (Flux v2).** Декларативное состояние 9 кластеров: инфраструктурные компоненты + прикладные сервисы. Только манифесты — развёртывание контура aero выделено в [infra/aero-deploy](https://gitlab.sarex.io/infra/aero-deploy) | `clusters/`, `infrastructure/` (36), `apps/` (37), `inventory.yaml`, `docs/apps/` |
| [infra/terraform](https://gitlab.sarex.io/infra/terraform) ✅ | **Ресурсы Yandex Cloud и Kubernetes**: namespaces, PostgreSQL БД/пользователи, S3-бакеты, Valkey/Redis-пользователи, Kafka-топики, RabbitMQ, k8s-секреты. Единый источник правды — `infrastructure.yaml`, секреты через SOPS | `live/prod` (Terragrunt), `modules/`: `k8s-namespace`, `k8s-secret`, `k8s-secrets`, `kafka-topics-yc`, `rabbitmq`, `yc-database`, `yc-s3`, `yc-valkey-user` |
| [infra/terraform-contour](https://gitlab.sarex.io/infra/terraform-contour) ✅ | Тот же подход **для изолированного контура**: namespaces и секреты из Vault | `live/{namespace,vault-secrets}`, `modules/{k8s-namespace,vault-platform-secrets}` |
| [infra/terraform-contour-mirror](https://gitlab.sarex.io/infra/terraform-contour-mirror) ✅ | **Зеркало `infra/terraform` для контура** (ветка `contour`, единственная). Тот же README и структура + `Dockerfile` для запуска в закрытом периметре | `live/`, `modules/`, `scripts/`, `Dockerfile` |
@ -74,6 +74,7 @@ Superset, Trino, GoAlert, GlitchTip, argo-*, otel-*) **существуют об
| Репозиторий | Отвечает за |
|---|---|
| [infra/aero-deploy](https://gitlab.sarex.io/infra/aero-deploy) ✅ | **Изолированный контур aero на одном хосте**: ansible-роли (`base`, `docker`, `sarex_stack`) + `docker-compose.yaml` подложки — k3s тремя контейнерами, gitea как источник правды, Vault, bootstrap FluxCD. Манифесты берёт из `infra/iac` (`flux_src_root`) и синхронизирует в gitea контура. Выделен из `infra/iac` |
| [infra/kubespray](https://gitlab.sarex.io/infra/kubespray) ✅ | Форк upstream Kubespray — **развёртывание самих кластеров Kubernetes** на VM |
| [infra/k8s-provision](https://gitlab.sarex.io/infra/k8s-provision) ✅ | **Первичная обвязка свежего кластера**: `namespaces`, `cert_manager`, `istio` (+`ISTIO.md`), `ingress`, `dashboard`, `prometheus`, `postgres`/`ya_postgres`, `rabbitmq`, `pvc`, `gitlab`, `teamcity`, `migration` |
| [infra/ansible-playbooks](https://gitlab.sarex.io/infra/ansible-playbooks) ✅ | Зонтичный Ansible-репозиторий: каталоги `minio/`, `patroni/` |

View File

@ -369,6 +369,23 @@ flowchart LR
│ └── patches/ # Патчи поверх base
```
### Чего здесь нет: развёртывание контура aero
Машинерия подъёма самого хоста живёт в отдельном репозитории
[`infra/aero-deploy`](https://gitlab.sarex.io/infra/aero-deploy): ansible-роли,
`docker-compose.yaml` подложки (k3s, gitea, vault, bootstrap Flux),
конфигурация Vault и k3s-манифесты DNS-мостов. Здесь остаётся только то, что
раскатывает Flux.
Связь между репозиториями односторонняя: роль `sarex_stack` пакует отсюда
`clusters/aero` и его замыкание (переменная `gitea_sync_paths`) и коммитит в
gitea внутри контура, откуда это читает Flux. Путь к этому репозиторию роль
берёт из `flux_src_root`, по умолчанию — соседний каталог `../iac`.
Практическое следствие: **добавили компонент в `clusters/aero/controllers`
допишите его в `gitea_sync_paths`** в `aero-deploy`, иначе kustomize в кластере
упадёт на отсутствующем каталоге. Обратной ссылки нет, и забыть об этом легко.
## Как это работает
Flux отслеживает директорию `clusters/<имя-кластера>/`. Каждый кластер содержит два Flux Kustomization CRD верхнего уровня:

224
apps/bim/aero/bim-api.yaml Normal file
View File

@ -0,0 +1,224 @@
---
# BIM API — приложение из aero/bimbackend (Python, api + worker).
#
# ЭТО НЕ ТО ЖЕ, ЧТО apps/bim/base. В base описан platform/bim-backend-v2 —
# другой сервис, на Go, с единственным httpserver. Спутать их легко: ОБА
# пушат в реестр один и тот же образ bim-api, и различаются только тегами и
# содержимым. У bimbackend теги — полный SHA коммита (CI пушит
# bim-api:${CI_COMMIT_SHA} с master), у bim-backend-v2 — contour_*.
# Поэтому здесь ../base намеренно НЕ подключён.
#
# Манифесты обычные, а не HelmRelease с universal-chart: в репозитории этот
# сервис так и описан (apps/bim/dsinv), своего чарта у него нет, и заводить
# его ради контура значило бы поддерживать ещё одну сущность.
apiVersion: v1
kind: ServiceAccount
metadata:
name: bim-api-sa
namespace: bim
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: bim-api
namespace: bim
labels:
app: bim-api
spec:
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
app: bim-api
template:
metadata:
labels:
app: bim-api
annotations:
# Порт Vault исключён из перехвата sidecar'ом: сам Vault живёт вне
# кластера, и заворачивать обращения к нему в mesh незачем.
traffic.sidecar.istio.io/excludeOutboundPorts: "8200"
vault.hashicorp.com/agent-init-first: "true"
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-pre-populate-only: "true"
vault.hashicorp.com/auth-path: auth/kubernetes
vault.hashicorp.com/role: bim
# БД. Имена переменных — DB_*, как их читает settings.py bimbackend,
# а не POSTGRES_*, как у остальных приложений контура.
vault.hashicorp.com/agent-inject-secret-bim-db: secrets/data/apps/bim/postgres
vault.hashicorp.com/agent-inject-template-bim-db: |-
{{- with secret "secrets/data/apps/bim/postgres" -}}
DB_HOST={{ index .Data.data "host" }}
DB_PORT={{ index .Data.data "port" }}
DB_USER={{ index .Data.data "username" }}
DB_PASSWORD={{ index .Data.data "password" }}
{{- end -}}
vault.hashicorp.com/agent-inject-secret-bim-rabbitmq: secrets/data/rabbitmq/apps/bim
vault.hashicorp.com/agent-inject-template-bim-rabbitmq: |-
{{- with secret "secrets/data/rabbitmq/apps/bim" -}}
RABBIT_USER={{ index .Data.data "username" }}
RABBIT_PASSWORD={{ index .Data.data "password" }}
{{- end -}}
vault.hashicorp.com/agent-inject-secret-bim-s3: secrets/data/minio/apps/bim
vault.hashicorp.com/agent-inject-template-bim-s3: |-
{{- with secret "secrets/data/minio/apps/bim" -}}
AWS_ACCESS_KEY_ID={{ index .Data.data "access_key" }}
AWS_SECRET_ACCESS_KEY={{ index .Data.data "secret_key" }}
{{- end -}}
# Публичный ключ JWT отдаётся файлом. Оба флага аутентификации ниже
# выключены, то есть читаться он не должен, — но путь в переменной
# обязан существовать, иначе рискуем падением при импорте настроек.
vault.hashicorp.com/agent-inject-secret-jwt-public: secrets/data/vault/common/rsa_keys
vault.hashicorp.com/agent-inject-template-jwt-public: |-
{{- with secret "secrets/data/vault/common/rsa_keys" -}}
{{ index .Data.data "public_key" }}
{{- end -}}
spec:
serviceAccountName: bim-api-sa
imagePullSecrets:
- name: regcred
containers:
- name: bim-api
# Тег — полный SHA, как их пушит CI aero/bimbackend с master. Именно
# этот образ крутится в dsinv. contour_*-теги у образа bim-api брать
# НЕЛЬЗЯ: их собирает bim-backend-v2, это чужое приложение.
image: cr.yandex/crp3ccidau046kdj8g9q/bim-api:870965d1967cd761965029b0480884ab7fa7c951
imagePullPolicy: IfNotPresent
# Штатный entrypoint не умеет читать секреты из файлов, поэтому
# оборачиваем его: подгружаем отрендеренное vault-агентом и передаём
# управление. exec — чтобы приложение осталось процессом с PID 1 и
# корректно получало сигналы остановки.
command: ["/bin/sh", "-ec"]
args:
- |
set -a
[ -f /vault/secrets/bim-db ] && . /vault/secrets/bim-db
[ -f /vault/secrets/bim-rabbitmq ] && . /vault/secrets/bim-rabbitmq
[ -f /vault/secrets/bim-s3 ] && . /vault/secrets/bim-s3
set +a
exec ./entrypoint_api.sh
ports:
- containerPort: 5555
name: http
protocol: TCP
- containerPort: 9000
name: prom
protocol: TCP
env:
- name: JWT_PUBLIC_KEY_PATH
value: /vault/secrets/jwt-public
- name: FEATURE_ENABLE_JWT_AUTH
value: "false"
- name: FEATURE_ENABLE_SAREX_AUTH
value: "false"
- name: FEATURE_ENABLE_WORKSPACES
value: "true"
# Google-хранилища в контуре нет и не будет — флаг выключен.
# Том с ключом, который есть в dsinv, здесь не нужен, а сама
# переменная ниже нужна, см. комментарий к ней.
- name: FEATURE_ENABLE_GCLOUD
value: "false"
# Значение НЕ УДАЛЯТЬ, даже при выключенном флаге выше. settings.py
# делает check_settings(): проходит по всем ключам ENV_FIELDS и
# падает на первом же незаданном —
# Exception: ENV_VARIABLE NOT SET: GOOGLE_APPLICATION_CREDENTIALS
# Флаги проверяются позже, до них дело не доходит. Файла по этому
# пути нет и не будет: при FEATURE_ENABLE_GCLOUD=false его никто не
# открывает, важно лишь наличие самой переменной.
- name: GOOGLE_APPLICATION_CREDENTIALS
value: /etc/sarex/google-storage/service_account.json
# ВНИМАНИЕ: S3 в этом приложении НЕ НАСТРАИВАЕТСЯ на MinIO.
# В storage/s3.py адрес зашит в код:
# endpoint_url='https://storage.yandexcloud.net'
# переменной окружения для него нет. Из контура он недостижим,
# поэтому любые операции с файлами будут падать по таймауту —
# это принято осознанно, API и остальные эндпоинты живут.
# Починить можно только пересборкой образа с вынесенным endpoint
# либо переводом на BIM_CURRENT_STORAGE=local (тип есть в
# storage/storages.py).
- name: FEATURE_ENABLE_S3
value: "true"
- name: S3_BUCKET
value: sarex-media-storage
- name: BIM_CURRENT_STORAGE
value: s3
- name: AWS_DEFAULT_REGION
value: ru-central1
- name: FEATURE_ENABLE_PROCESSING
value: "1"
# Адреса смежных сервисов приведены к именам контура. В dsinv это
# workflows-service:8000 и workspaces-service:8000 — таких сервисов
# здесь нет, оба называются backend-svc и слушают 80.
- name: PROCESSING_API_URL
value: http://backend-svc.processing.svc.cluster.local:80/internal
- name: PROCESSING_C2M_VERSION
value: stable
- name: WORKSPACES_API_URL
value: http://backend-svc.workspaces.svc.cluster.local:80/internal
# Сервиса сравнений в контуре нет и не планируется, флага для его
# отключения приложение не предоставляет. Имя оставлено говорящим:
# в логах будет видно, куда именно оно не достучалось.
- name: COMPARATOR_API_URL
value: http://comparator-api-service.comparator.svc.cluster.local/internal
- name: BIM_API_EXTERNAL_HOST_SCHEMA
value: https
- name: BIM_API_EXTERNAL_HOST
value: bim.sarex.local.lonsdaleites.ru
- name: BIM_API_INTERNAL_HOST_SCHEMA
value: http
- name: BIM_API_INTERNAL_HOST
value: bim-api-service.bim.svc.cluster.local:5555
- name: WEBHOOK_URL
value: http://bim-api-service.bim.svc.cluster.local:5555
# Брокер общий, vhost по умолчанию. В dsinv vhost называется api,
# но в контуре отдельных vhost'ов нет — см. secrets/rabbitmq/*.
- name: RABBIT_HOST
value: rabbitmq.rabbitmq.svc.cluster.local:5672
- name: RABBIT_VHOST
value: "/"
- name: MESH_OPTIMIZER_PATH
value: gltfpack
- name: DEBUG
value: "0"
- name: STORAGE_SEGMENT
value: sarex-production-storage
- name: STORAGE_PROJECT
value: sarex-production
- name: TMP_DIR
value: /tmp
- name: prometheus_multiproc_dir
value: /tmp
- name: BUNDLE_VERSION
value: v1
- name: DISABLE_INSTANCING
value: "0"
resources:
requests:
cpu: 25m
memory: 200Mi
volumeMounts:
- mountPath: /tmp
name: tmp-volume
volumes:
- name: tmp-volume
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: bim-api-service
namespace: bim
labels:
app: bim-api
spec:
# ClusterIP, а не NodePort с прибитым портом 31352, как в dsinv: наружу
# сервис выходит через общий Gateway и VirtualService контура, отдельный
# порт на нодах ему не нужен.
type: ClusterIP
ports:
- name: http
port: 5555
targetPort: 5555
protocol: TCP
selector:
app: bim-api

View File

@ -0,0 +1,157 @@
---
# Воркер bimbackend. Крутит тот же образ и те же настройки, что api, —
# отличается только точкой входа (entrypoint_worker.sh) и отсутствием портов.
# Обоснования переменных не дублирую, они в bim-api.yaml.
apiVersion: v1
kind: ServiceAccount
metadata:
name: bim-worker-sa
namespace: bim
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: bim-worker
namespace: bim
labels:
app: bim-worker
spec:
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
app: bim-worker
template:
metadata:
labels:
app: bim-worker
annotations:
traffic.sidecar.istio.io/excludeOutboundPorts: "8200"
vault.hashicorp.com/agent-init-first: "true"
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-pre-populate-only: "true"
vault.hashicorp.com/auth-path: auth/kubernetes
vault.hashicorp.com/role: bim
vault.hashicorp.com/agent-inject-secret-bim-db: secrets/data/apps/bim/postgres
vault.hashicorp.com/agent-inject-template-bim-db: |-
{{- with secret "secrets/data/apps/bim/postgres" -}}
DB_HOST={{ index .Data.data "host" }}
DB_PORT={{ index .Data.data "port" }}
DB_USER={{ index .Data.data "username" }}
DB_PASSWORD={{ index .Data.data "password" }}
{{- end -}}
vault.hashicorp.com/agent-inject-secret-bim-rabbitmq: secrets/data/rabbitmq/apps/bim
vault.hashicorp.com/agent-inject-template-bim-rabbitmq: |-
{{- with secret "secrets/data/rabbitmq/apps/bim" -}}
RABBIT_USER={{ index .Data.data "username" }}
RABBIT_PASSWORD={{ index .Data.data "password" }}
{{- end -}}
vault.hashicorp.com/agent-inject-secret-bim-s3: secrets/data/minio/apps/bim
vault.hashicorp.com/agent-inject-template-bim-s3: |-
{{- with secret "secrets/data/minio/apps/bim" -}}
AWS_ACCESS_KEY_ID={{ index .Data.data "access_key" }}
AWS_SECRET_ACCESS_KEY={{ index .Data.data "secret_key" }}
{{- end -}}
vault.hashicorp.com/agent-inject-secret-jwt-public: secrets/data/vault/common/rsa_keys
vault.hashicorp.com/agent-inject-template-jwt-public: |-
{{- with secret "secrets/data/vault/common/rsa_keys" -}}
{{ index .Data.data "public_key" }}
{{- end -}}
spec:
serviceAccountName: bim-worker-sa
imagePullSecrets:
- name: regcred
containers:
- name: bim-worker
image: cr.yandex/crp3ccidau046kdj8g9q/bim-api:870965d1967cd761965029b0480884ab7fa7c951
imagePullPolicy: IfNotPresent
command: ["/bin/sh", "-ec"]
args:
- |
set -a
[ -f /vault/secrets/bim-db ] && . /vault/secrets/bim-db
[ -f /vault/secrets/bim-rabbitmq ] && . /vault/secrets/bim-rabbitmq
[ -f /vault/secrets/bim-s3 ] && . /vault/secrets/bim-s3
set +a
exec ./entrypoint_worker.sh
env:
- name: JWT_PUBLIC_KEY_PATH
value: /vault/secrets/jwt-public
- name: FEATURE_ENABLE_JWT_AUTH
value: "false"
- name: FEATURE_ENABLE_SAREX_AUTH
value: "false"
- name: FEATURE_ENABLE_WORKSPACES
value: "true"
- name: FEATURE_ENABLE_GCLOUD
value: "false"
# Значение НЕ УДАЛЯТЬ, даже при выключенном флаге выше. settings.py
# делает check_settings(): проходит по всем ключам ENV_FIELDS и
# падает на первом же незаданном —
# Exception: ENV_VARIABLE NOT SET: GOOGLE_APPLICATION_CREDENTIALS
# Флаги проверяются позже, до них дело не доходит. Файла по этому
# пути нет и не будет: при FEATURE_ENABLE_GCLOUD=false его никто не
# открывает, важно лишь наличие самой переменной.
- name: GOOGLE_APPLICATION_CREDENTIALS
value: /etc/sarex/google-storage/service_account.json
- name: FEATURE_ENABLE_S3
value: "true"
- name: S3_BUCKET
value: sarex-media-storage
- name: BIM_CURRENT_STORAGE
value: s3
- name: AWS_DEFAULT_REGION
value: ru-central1
- name: FEATURE_ENABLE_PROCESSING
value: "1"
- name: PROCESSING_API_URL
value: http://backend-svc.processing.svc.cluster.local:80/internal
- name: PROCESSING_C2M_VERSION
value: stable
- name: WORKSPACES_API_URL
value: http://backend-svc.workspaces.svc.cluster.local:80/internal
- name: COMPARATOR_API_URL
value: http://comparator-api-service.comparator.svc.cluster.local/internal
- name: BIM_API_EXTERNAL_HOST_SCHEMA
value: https
- name: BIM_API_EXTERNAL_HOST
value: bim.sarex.local.lonsdaleites.ru
- name: BIM_API_INTERNAL_HOST_SCHEMA
value: http
- name: BIM_API_INTERNAL_HOST
value: bim-api-service.bim.svc.cluster.local:5555
- name: WEBHOOK_URL
value: http://bim-api-service.bim.svc.cluster.local:5555
# В dsinv у воркера здесь rabbitmq-service.bim:5672 — брокера в
# namespace bim нет ни там, ни здесь; у api в том же файле адрес
# другой и правильный. Ставим общий брокер контура.
- name: RABBIT_HOST
value: rabbitmq.rabbitmq.svc.cluster.local:5672
- name: RABBIT_VHOST
value: "/"
- name: MESH_OPTIMIZER_PATH
value: gltfpack
- name: DEBUG
value: "0"
- name: STORAGE_SEGMENT
value: sarex-production-storage
- name: STORAGE_PROJECT
value: sarex-production
- name: TMP_DIR
value: /tmp
- name: prometheus_multiproc_dir
value: /tmp
- name: BUNDLE_VERSION
value: v1
- name: DISABLE_INSTANCING
value: "0"
resources:
requests:
cpu: 25m
memory: 200Mi
volumeMounts:
- mountPath: /tmp
name: tmp-volume
volumes:
- name: tmp-volume
emptyDir: {}

View File

@ -0,0 +1,11 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: bim
# ../base НЕ подключён намеренно: там описан platform/bim-backend-v2 — другое
# приложение, которое разворачивать в контуре не нужно. Подробности в шапке
# bim-api.yaml.
resources:
- namespace.yaml
- bim-api.yaml
- bim-worker.yaml

View File

@ -0,0 +1,7 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: bim
labels:
istio-injection: enabled

View File

@ -0,0 +1,387 @@
# Копия django-configmap из base с заменой домена на домен контура.
#
# Патчем этого не сделать: все настройки лежат ОДНИМ значением data.production.py
# (331 строка python), kustomize умеет заменить его только целиком. Ровно та же
# причина, что у nginx-configmap.yaml рядом.
#
# ЧТО ИЗМЕНЕНО — две строки про домен sarex.contour.infra.sarex.tech,
# принадлежащий ДРУГОМУ контуру:
#
# HOST — по нему backend строит АБСОЛЮТНЫЕ ссылки на медиа.
# Браузер получал
# https://sarex.contour.infra.sarex.tech/media/state/previews/....png
# и, разумеется, 404: чужой хост, наших файлов там нет.
# CSRF_TRUSTED_ORIGINS — без нашего домена django отклонял бы POST-формы
# админки с ошибкой проверки Referer.
#
# И один блок сверх того: CsrfExemptSessionAuthentication в
# DEFAULT_AUTHENTICATION_CLASSES вместо штатного SessionAuthentication —
# из-за него SPA получала 403 на любое удаление. Обоснование и цена решения
# расписаны прямо над классом.
#
# Больше в файле ничего не менялось: сравнивать с base удобно обычным diff.
apiVersion: v1
kind: ConfigMap
metadata:
name: django-configmap
namespace: django
data:
production.py: |
import ast
import os
from .base import *
from logging.handlers import SysLogHandler
from datetime import timedelta
def _load_env_file(path):
try:
with open(path, "r", encoding="utf-8") as f:
for raw_line in f:
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
key = key.strip()
value = value.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
try:
value = ast.literal_eval(value)
except (ValueError, SyntaxError):
value = value[1:-1]
if key and key not in os.environ:
os.environ[key] = value
except FileNotFoundError:
pass
def _read_secret_file(path, default=""):
try:
with open(path, "r", encoding="utf-8") as f:
return f.read().strip()
except FileNotFoundError:
return default
# Fallback for manage.py launched via `kubectl exec` (outside entrypoint),
# so Django can still read DB/JWT values from Vault-injected files.
_load_env_file("/vault/secrets/django-postgresql")
_load_env_file("/vault/secrets/django-rabbitmq")
_load_env_file("/vault/secrets/django-s3")
_load_env_file("/vault/secrets/django-kafka")
_load_env_file("/vault/secrets/django-common")
if not os.environ.get("JWT_PRIVATE_KEY"):
os.environ["JWT_PRIVATE_KEY"] = _read_secret_file("/vault/secrets/django-jwt-private")
if not os.environ.get("JWT_PUBLIC_KEY"):
os.environ["JWT_PUBLIC_KEY"] = _read_secret_file("/vault/secrets/django-jwt-public")
ALLOWED_HOSTS = ["*"]
FILE_UPLOAD_PERMISSIONS = 0o644
DEBUG = False
CSRF_COOKIE_SECURE = True
CSRF_TRUSTED_ORIGINS = ["https://sarex.local.lonsdaleites.ru", "http://sarex.local.lonsdaleites.ru"]
SESSION_COOKIE_SECURE = True
SECURE_SSL_REDIRECT = False
SECRET_KEY = 't2=9+($2f%7ptsdy4!rby$)mcfl1l%o2e@vs^d(g&(wwi&%k1v'
CORS_ORIGIN_ALLOW_ALL = True
SERVERSETTINGS.cache_enabled = True
INSTALLED_APPS = list(INSTALLED_APPS) + ['corsheaders']
CORS_ALLOW_METHODS = (
'DELETE',
'GET',
'OPTIONS',
'PATCH',
'POST',
'PUT',
)
BASIC_USER_ID = 2
CORS_ALLOW_HEADERS = (
'accept',
'accept-encoding',
'authorization',
'content-type',
'user-agent',
'x-csrftoken',
'x-requested-with',
'x-token',
'Bearer',
)
HOST = "https://sarex.local.lonsdaleites.ru"
POSTGRES_DATABASE = os.environ.get('DJANGO_POSTGRES_DATABASE')
POSTGRES_USER = os.environ.get('DJANGO_POSTGRES_USER')
POSTGRES_PASSWORD = os.environ.get('DJANGO_POSTGRES_PASSWORD')
POSTGRES_HOST = os.environ.get('DJANGO_POSTGRES_HOST')
POSTGRES_PORTS = os.environ.get('DJANGO_POSTGRES_PORTS', "5432")
DATABASES = {
'default': {
'ENGINE': 'django_prometheus.db.backends.postgresql',
'NAME': POSTGRES_DATABASE,
'USER': POSTGRES_USER,
'PASSWORD': POSTGRES_PASSWORD,
'HOST': POSTGRES_HOST,
'PORT': POSTGRES_PORTS,
}
}
MEASUREMENTSETTINGS = MeasurementSettings()
WORKFLOWSSETTINGS = WorkFlowsSettings()
S3SETTINGS = S3Settings()
GATEWAYSETTINGS = GateWaySetttings()
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'filters': {
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse',
}
},
'formatters': {
'verbose': {
'format': '[contactor] %(levelname)s %(asctime)s %(message)s',
},
},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
},
'sentry': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'logging.StreamHandler',
},
},
'loggers': {
'': {
'handlers': ['console', 'sentry'],
'level': 'INFO',
'propagate': False,
},
}
}
COMPARATOR_JWT = os.environ.get("COMPARATOR_JWT", "default_jwt")
COMPARATOR_URL = os.environ.get("COMPARATOR_URL", "https://wb.sarex.io/comparator")
COMPARATOR_SECTION = os.environ.get("COMPARATOR_SECTION", "sarex-production-storage")
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(hours=1),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'ROTATE_REFRESH_TOKENS': False,
'BLACKLIST_AFTER_ROTATION': True,
'UPDATE_LAST_LOGIN': False,
'ALGORITHM': 'RS512',
'SIGNING_KEY': os.environ.get("JWT_PRIVATE_KEY", "").replace("\\n", "\n"),
'VERIFYING_KEY': os.environ.get("JWT_PUBLIC_KEY", "").replace("\\n", "\n"),
'AUDIENCE': None,
'ISSUER': os.environ.get('SIMPLE_JWT_ISSUER', 'default_issuer'),
'AUTH_HEADER_TYPES': ('Bearer',),
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
'TOKEN_TYPE_CLAIM': 'token_type',
'JTI_CLAIM': 'jti',
'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
}
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
DEFAULT_FILE_STORAGE = 'sarex.core.storages.CustomS3Boto3Storage'
DATA_UPLOAD_MAX_MEMORY_SIZE = 268435456
if not os.environ.get('ISOLATED', False):
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
sentry_sdk.init(
dsn="https://3df2f4b8d3d14595a06c92e9d7c562cb@sentry.io/1501541",
integrations=[DjangoIntegration()],
environment=os.environ.get('SENTRY_ENVIRONMENT', 'production'),
send_default_pii=True,
)
COMPARISON_API_URL = f"{os.environ.get('WORKFLOWSSETTINGS_HOST')}/comparisons"
DOCUMENTATION_API_URL = f"{os.environ.get('WORKFLOWSSETTINGS_HOST')}/documentations"
PDM_FILES_API_URL = f"{os.environ.get('WORKFLOWSSETTINGS_HOST')}/files"
WORKFLOWS_TASKS = {
"update_orthomosaic_data": {
"image": f"{os.environ.get('WORKFLOWSSETTINGS_REGISTRY')}/update-orthomosaic-data:dev",
"service_requests": ["django-auth"],
"backoff_limit": 3,
},
}
# Сессионная аутентификация без проверки CSRF.
#
# SPA платформы аутентифицируется сессионной кукой и не прикладывает
# заголовок X-CSRFToken, а SessionAuthentication из DRF требует его на
# любом небезопасном методе. Наружу это выглядело так:
# DELETE /api/core/videos/<id>/ → 403
# {"detail": "CSRF Failed: CSRF token missing."}
#
# Проверено на контуре: тот же запрос с X-CSRFToken проходит, и он же с
# заголовком Authorization: Bearer проходит тоже — до SessionAuthentication
# дело в этом случае просто не доходит, потому что simple_jwt в списке ниже
# стоит раньше. То есть бэкенд исправен, чинить нужно ровно сочетание
# "кука без заголовка".
#
# ЧЕМ ПЛАТИМ: с запросов, авторизованных кукой, снимается защита от CSRF.
# Основной вектор закрыт самими куками — sessionid и csrftoken выставляются
# с SameSite=Lax, и браузер не пошлёт их при кросс-сайтовом DELETE. Щель
# остаётся одна: SameSite смотрит на site, а не на origin, поэтому соседи
# по домену (minio., rabbitmq., dashboard.) считаются "своими", а в MinIO
# лежит пользовательский контент.
#
# Импорт DRF прямо в модуле настроек безопасен НЕ всегда — обычно так и
# ловят AppRegistryNotReady. Здесь проверено запуском django.setup() внутри
# работающего пода: rest_framework.authentication тянет только
# django.contrib.auth и rest_framework.exceptions, и ни один из них к
# моделям на уровне модуля не обращается.
from rest_framework.authentication import SessionAuthentication
class CsrfExemptSessionAuthentication(SessionAuthentication):
def enforce_csrf(self, request):
return
REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': (
'rest_framework.pagination.LimitOffsetPagination' ),
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
'PAGE_SIZE': 1000, 'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend' ],
'DEFAULT_AUTHENTICATION_CLASSES': [
'sarex.authentication.backends.ZitadelJWTAuthentication',
'rest_framework.authentication.RemoteUserAuthentication',
'rest_framework_simplejwt.authentication.JWTAuthentication',
'rest_framework.authentication.BasicAuthentication',
'config.settings.production.CsrfExemptSessionAuthentication',
'sarex.authentication.backends.JWTAuthentication' ],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated', ] }
AUTHENTICATION_BACKENDS = [
'sarex.authentication.backends.CustomRemoteUserBackend',
'django.contrib.auth.backends.ModelBackend',
'guardian.backends.ObjectPermissionBackend',
]
MIDDLEWARE = [
'django_prometheus.middleware.PrometheusBeforeMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
#'django_keycloak.middlewares.AuthorizationHeaderMiddleware',
#'django_keycloak.middlewares.KeycloakSessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
#'django.contrib.auth.middleware.RemoteUserMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django_user_agents.middleware.UserAgentMiddleware',
'simple_history.middleware.HistoryRequestMiddleware',
'django_prometheus.middleware.PrometheusAfterMiddleware', ]
class KeyCloakSettings(BaseSettings):
client_id: str = "client_id"
client_secret: str = "client_secret"
discovery_url: str = "https://login.wb.sarex.io/realms/sarex/.well-known/openid-configuration"
staff: Optional[str] = "Sarex staff"
superuser: Optional[str] = "Sarex superusers"
sync_with_django: bool = True
sync_admin: bool = False
group_prefix: str = 'Sarex-Role'
company_prefix: str = 'Sarex-Company'
department_prefix: str = 'Sarex-Department'
position_prefix: str = 'Sarex-Position'
separator: str = '__'
sync_user_groups: bool = False
sync_user_positions: bool = False
sync_user_departments: bool = False
sync_user_companies: bool = False
use_redirect_logout: bool = False
logout_redirect_uri: str = "/"
default_group_name: Optional[str] = 'Тест'
default_company_name: Optional[str] = 'Брусника'
trusted_uri: List[str] = ['/api/core/orthophotos/', '/api/token', '/api/token/me']
trusted_uri: List[str] = []
class Config:
env_prefix = "KC_"
KEYCLOAKSETTINGS = KeyCloakSettings()
REMOTE_USER_DEFAULT_COMPANY_ID = 1
SAREX_MODULES = [
{
"name": "Замечания",
"uri": "/remarks"
},
# {
# "name": "Управление проектами",
# "uri": "/management/projects",
# },
{
"name": "Замечания V2",
"uri": "/issues"
},
{
"name": "Документация",
"uri": "/documentations",
},
{
"name": "Согласование документов",
"uri": "/reviews"
},
{
"name": "Рабочие процессы",
"uri": "/processes"
},
{
"name": "Запросы",
"uri": "/rfi"
},
{
"name": "Управление проектами",
"uri": "/management/projects"
},
# {
# "name": "Обзор",
# "uri": "/projects"
# },
{
"name": "Передача документации",
"uri": "/transmittal"
},
]
AUTH_SETTINGS = {
"refresh_token": False,
"refresh_token_uri": "/api/token/me",
"refresh_oauth_token": True,
"refresh_oauth_token_uri": "/oauth/token",
"refresh_time": 240,
}
DEBUG=True
WEB_APP_AUTH_MODE='jwt-session-based'
SAREX_MODULES_SETTINGS = {
"aero": {
"enable_new_media": True
},
"sso_logout_redirect": True
}

View File

@ -0,0 +1,262 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: django
resources:
- ../base
patches:
# Свой nginx.conf вместо базового: в base зашиты upstream'ы на namespace,
# которых в контуре нет, и nginx из-за этого не стартовал. Подробности —
# в шапке nginx-configmap.yaml.
#
# ИМЕННО ПАТЧ, а не resources. Объект с тем же kind+name уже пришёл из
# ../base, и добавление его вторым ресурсом валит сборку целиком:
# may not add resource with an already registered id:
# ConfigMap.v1.[noGrp]/nginx-configmap.django
# Перекрывать базовый ресурс можно только патчем.
- path: nginx-configmap.yaml
target:
kind: ConfigMap
name: nginx-configmap
# Настройки django: в base захардкожен домен ЧУЖОГО контура. По нему backend
# строил абсолютные ссылки на медиа, и браузер уходил на
# https://sarex.contour.infra.sarex.tech/media/state/previews/....png
# получая 404. Причина копии та же, что у nginx-configmap: настройки лежат
# одним значением data.production.py, заменить можно только целиком.
- path: django-configmap.yaml
target:
kind: ConfigMap
name: django-configmap
# --- celery: те же правки, что и у backend --------------------------------
# Worker крутит тот же образ и те же настройки, поэтому и расхождения с base
# у него ровно те же: нет Kafka, нет Zitadel, MinIO живёт внутри кластера.
# Обоснования — в блоке backend ниже, здесь не дублирую.
#
# SERVER_ZITADEL_ENABLED правки не требует: в base у celery он уже "False"
# (в отличие от backend).
- target: {kind: HelmRelease, name: celery}
patch: |
- op: remove
path: /spec/values/services/celery/podAnnotations/_default/vault.hashicorp.com~1agent-inject-secret-django-kafka
- op: remove
path: /spec/values/services/celery/podAnnotations/_default/vault.hashicorp.com~1agent-inject-template-django-kafka
- op: remove
path: /spec/values/services/celery/podAnnotations/_default/vault.hashicorp.com~1agent-inject-secret-django-common
- op: remove
path: /spec/values/services/celery/podAnnotations/_default/vault.hashicorp.com~1agent-inject-template-django-common
- op: replace
path: /spec/values/services/celery/podAnnotations/_default/vault.hashicorp.com~1agent-inject-template-django-s3
value: |-
{{- with secret "secrets/data/minio/apps/django" }}
AWS_S3_ENDPOINT_URL=http://minio.minio.svc.cluster.local:9000
S3_HOST=http://minio.minio.svc.cluster.local:9000
{{- $buckets := index .Data.data "buckets" }}
S3_BUCKET={{ if gt (len $buckets) 0 }}{{ index (index $buckets 0) "name" }}{{ else }}django{{ end }}
S3_LOGIN={{ index .Data.data "access_key" }}
S3_PASSWORD={{ index .Data.data "secret_key" }}
{{- end }}
# Стартовый скрипт без подключения django-kafka: этого секрета в контуре
# нет. migrate и создание администратора сюда НЕ добавляются — схемой
# владеет backend, а два процесса, катящих миграции наперегонки при
# одновременном старте, — это гонка на ровном месте.
- op: replace
path: /spec/values/services/celery/deployment/args/_default/0
value: |
set -a
[ -f /vault/secrets/django-postgresql ] && . /vault/secrets/django-postgresql
[ -f /vault/secrets/django-rabbitmq ] && . /vault/secrets/django-rabbitmq
[ -f /vault/secrets/django-s3 ] && . /vault/secrets/django-s3
[ -f /vault/secrets/django-jwt-private ] && export JWT_PRIVATE_KEY="$(cat /vault/secrets/django-jwt-private)"
[ -f /vault/secrets/django-jwt-public ] && export JWT_PUBLIC_KEY="$(cat /vault/secrets/django-jwt-public)"
set +a
exec celery -A config worker -B -l info -E -Q default -n default_worker.%h --concurrency=2
# Адреса на wb.sarex.io — чужой контур, из aero недостижим. В base их
# четыре, и все четыре у celery: у backend те же переменные уже указывают
# внутрь кластера. WORKFLOWS_USE=1, так что оставить как есть — значит
# получить зависания на недоступном домене вместо явной ошибки.
- op: test
path: /spec/values/services/celery/envs/15/name
value: SERVER_API_HOST
- op: replace
path: /spec/values/services/celery/envs/15/value/_default
value: https://sarex.local.lonsdaleites.ru
- op: test
path: /spec/values/services/celery/envs/16/name
value: SERVER_HOST
- op: replace
path: /spec/values/services/celery/envs/16/value/_default
value: https://sarex.local.lonsdaleites.ru
- op: test
path: /spec/values/services/celery/envs/17/name
value: WORKFLOWS_HOST
- op: replace
path: /spec/values/services/celery/envs/17/value/_default
value: http://backend-svc.processing.svc.cluster.local:80
- op: test
path: /spec/values/services/celery/envs/18/name
value: WORKFLOWS_BASE_HOST
- op: replace
path: /spec/values/services/celery/envs/18/value/_default
value: http://backend-svc.django.svc.cluster.local:80
# Тег образа выравнен с backend. В base у celery production_a96dead0, а у
# backend production_f813140d — worker и web крутили бы разный код, и
# рассинхрон вылезал бы не при старте, а на конкретной задаче: worker
# десериализует аргументы по своей версии модели.
- op: replace
path: /spec/values/services/celery/image/name/_default
value: cr.yandex/crp3ccidau046kdj8g9q/backend:production_f813140d
# --- s3-proxy: S3 внутри кластера ------------------------------------------
# В base адрес MinIO зашит ВНУТРИ vault-шаблона:
# AWS_API_ENDPOINT=https://minio.contour.infra.sarex.tech
# Это чужой контур. Через values не перекрыть — значение внутри шаблона
# аннотации, поэтому заменяем аннотацию целиком, как у backend и celery.
#
# Проявлялось не отказом в соединении, а тем, что s3-proxy ДОСТУЧАЛСЯ до
# чужого MinIO и получил от него настоящий ответ S3:
# SignatureDoesNotMatch: The request signature we calculated does not
# match the signature you provided
# наружу это выглядело как 500 на каждую плитку /media/orthophotos/...
# Наши ключи в чужом хранилище, разумеется, не подходят — и хорошо, что так:
# иначе контур молча читал бы и писал в чужой S3.
- target: {kind: HelmRelease, name: s3-proxy}
patch: |
- op: replace
path: /spec/values/services/s3-proxy/podAnnotations/_default/vault.hashicorp.com~1agent-inject-template-s3
value: |
{{- with secret "secrets/data/minio/apps/django" }}
AWS_API_ENDPOINT=http://minio.minio.svc.cluster.local:9000
{{- $buckets := index .Data.data "buckets" }}
AWS_S3_BUCKET={{ if gt (len $buckets) 0 }}{{ index (index $buckets 0) "name" }}{{ else }}django{{ end }}
AWS_ACCESS_KEY_ID={{ index .Data.data "access_key" }}
AWS_SECRET_ACCESS_KEY={{ index .Data.data "secret_key" }}
{{- end }}
# --- backend: убираем зависимости, которых в контуре нет ------------------
- target: {kind: HelmRelease, name: backend}
patch: |
# Kafka и Zitadel в контуре не развёрнуты. Само приложение без них живёт:
# * SERVER_KAFKA_ENABLED в base уже "False", а в sarex-backend продюсер
# создаётся только под флагом (config/settings/deps/kafka.py);
# * zitadel_enabled читается ровно в одном месте — update_ams_user()
# в sarex/core/utils.py, которая при выключенном флаге сразу выходит.
# К аутентификации Zitadel отношения не имеет.
#
# Но аннотации инжекции убрать ОБЯЗАТЕЛЬНО: vault-agent-init не отрендерит
# шаблон для несуществующего секрета, упадёт, и под навсегда останется в
# Init:Error — приложение до своих флагов даже не дойдёт.
#
# В JSON Pointer слэш внутри ключа экранируется как ~1, поэтому
# vault.hashicorp.com/agent-... записан как vault.hashicorp.com~1agent-...
- op: remove
path: /spec/values/services/backend/podAnnotations/_default/vault.hashicorp.com~1agent-inject-secret-django-kafka
- op: remove
path: /spec/values/services/backend/podAnnotations/_default/vault.hashicorp.com~1agent-inject-template-django-kafka
- op: remove
path: /spec/values/services/backend/podAnnotations/_default/vault.hashicorp.com~1agent-inject-secret-django-common
- op: remove
path: /spec/values/services/backend/podAnnotations/_default/vault.hashicorp.com~1agent-inject-template-django-common
# S3 внутри кластера, а не через внешний домен. В base шаблон прибит к
# https://minio.contour.infra.sarex.tech — чужому домену, который отсюда
# не резолвится. Через values это не перекрыть: адрес зашит ВНУТРИ
# vault-шаблона аннотации, поэтому заменяем аннотацию целиком.
# Ходим напрямую в сервис MinIO, как это делал compose-стек.
- op: replace
path: /spec/values/services/backend/podAnnotations/_default/vault.hashicorp.com~1agent-inject-template-django-s3
value: |-
{{- with secret "secrets/data/minio/apps/django" }}
AWS_S3_ENDPOINT_URL=http://minio.minio.svc.cluster.local:9000
S3_HOST=http://minio.minio.svc.cluster.local:9000
{{- $buckets := index .Data.data "buckets" }}
S3_BUCKET={{ if gt (len $buckets) 0 }}{{ index (index $buckets 0) "name" }}{{ else }}django{{ end }}
S3_LOGIN={{ index .Data.data "access_key" }}
S3_PASSWORD={{ index .Data.data "secret_key" }}
{{- end }}
# Выключаем Zitadel. Операция test перед replace — страховка от смещения
# индекса: если в base порядок envs изменится, патч упадёт явно, вместо
# того чтобы молча перезаписать соседнюю переменную.
- op: test
path: /spec/values/services/backend/envs/2/name
value: SERVER_ZITADEL_ENABLED
- op: replace
path: /spec/values/services/backend/envs/2/value/_default
value: "False"
# Адреса на чужой контур. У celery те же две переменные поправлены выше,
# у backend они оставались с sarex.contour.infra.sarex.tech.
- op: test
path: /spec/values/services/backend/envs/17/name
value: SERVER_API_HOST
- op: replace
path: /spec/values/services/backend/envs/17/value/_default
value: https://sarex.local.lonsdaleites.ru
- op: test
path: /spec/values/services/backend/envs/18/name
value: SERVER_HOST
- op: replace
path: /spec/values/services/backend/envs/18/value/_default
value: https://sarex.local.lonsdaleites.ru
# --- Администратор платформы --------------------------------------------
# Учётка приезжает из Vault под именами, которые ждёт штатная
# createsuperuser --noinput. Секрет кладёт ansible
# (vault_app_secrets, secrets/apps/django/superuser).
- op: add
path: /spec/values/services/backend/podAnnotations/_default/vault.hashicorp.com~1agent-inject-secret-django-superuser
value: secrets/data/apps/django/superuser
- op: add
path: /spec/values/services/backend/podAnnotations/_default/vault.hashicorp.com~1agent-inject-template-django-superuser
value: |-
{{- with secret "secrets/data/apps/django/superuser" -}}
DJANGO_SUPERUSER_USERNAME={{ index .Data.data "username" }}
DJANGO_SUPERUSER_EMAIL={{ index .Data.data "email" }}
DJANGO_SUPERUSER_PASSWORD={{ index .Data.data "password" }}
{{- end -}}
# Стартовый скрипт целиком, а не точечная правка: JSON6902 умеет заменять
# только элемент списка, а вся логика запуска — один shell-блок args[0].
# Отличия от base:
# * не подключается django-kafka — этого секрета в контуре нет;
# * добавлены migrate и создание администратора.
#
# Почему migrate здесь, хотя его же делает /opt/sarex/entrypoint.sh:
# там он запущен без set -e, и его падение НИКАК не мешает uwsgi
# стартовать. Именно так контур несколько часов отдавал 200 на /admin/
# с пустой схемой БД, пока backend не мог аутентифицироваться. Здесь
# команда выполняется под set -e из command базы (/bin/sh -ec), поэтому
# неудача валит под в CrashLoopBackOff — то есть становится видимой.
# Повторный прогон внутри entrypoint.sh при уже накатанной схеме
# отрабатывает вхолостую за секунды.
- op: replace
path: /spec/values/services/backend/deployment/args/_default/0
value: |
set -a
[ -f /vault/secrets/django-postgresql ] && . /vault/secrets/django-postgresql
[ -f /vault/secrets/django-rabbitmq ] && . /vault/secrets/django-rabbitmq
[ -f /vault/secrets/django-s3 ] && . /vault/secrets/django-s3
[ -f /vault/secrets/django-superuser ] && . /vault/secrets/django-superuser
[ -f /vault/secrets/django-jwt-private ] && export JWT_PRIVATE_KEY="$(cat /vault/secrets/django-jwt-private)"
[ -f /vault/secrets/django-jwt-public ] && export JWT_PUBLIC_KEY="$(cat /vault/secrets/django-jwt-public)"
set +a
python manage.py migrate --noinput
# Идемпотентно по построению: команда отрабатывает при КАЖДОМ старте
# пода, а на уже существующем логине штатно возвращает
# "That username is already taken" с ненулевым кодом. Ошибка ожидаема
# и гасится веткой else, иначе set -e ронял бы под на втором запуске.
if [ -n "${DJANGO_SUPERUSER_USERNAME:-}" ]; then
if python manage.py createsuperuser --noinput; then
echo "entrypoint: администратор создан"
else
echo "entrypoint: администратор уже существует, пропускаем"
fi
fi
exec /opt/sarex/entrypoint.sh

View File

@ -0,0 +1,194 @@
# Копия nginx-configmap из base с правками под контур aero.
#
# Патчем это сделать нельзя: конфиг лежит одной строкой в data.nginx.conf,
# и kustomize умеет только заменить значение целиком.
#
# ЧТО ИЗМЕНЕНО ОТНОСИТЕЛЬНО base — три вещи:
#
# 1. Убран location /api/pm/. Сервис pm в контуре не нужен и не планируется.
#
# 2. Для workspaces и processing адрес вынесен в переменную и добавлен
# resolver. Причина: nginx резолвит все upstream'ы ПРИ СТАРТЕ и падает
# целиком, если хоть один не найден:
# nginx: [emerg] host not found in upstream "backend-svc.pm..."
# Из-за этого весь frontend уходил в CrashLoopBackOff, хотя не работал
# лишь один маршрут. С переменной nginx откладывает резолвинг до запроса:
# сейчас эти пути отдают 502, а когда сервисы приедут — заработают сами,
# без правки конфига и перезапуска.
#
# ПОРЯДОК ДИРЕКТИВ ВНУТРИ ТАКОГО location ЗНАЧИМ: set обязан идти ДО
# rewrite ... break. Обе директивы принадлежат ngx_http_rewrite_module, а
# флаг break прекращает обработку его директив в этом location — set после
# него просто не выполняется, переменная остаётся пустой, и запрос падает с
# [error] no host in upstream ":80"
# отдавая наружу 500. Именно так и было: / работал, а /workspaces-v2/... и
# /workflows/... возвращали 500, пока set стоял ниже rewrite.
#
# 3. backend-svc оставлен прямой ссылкой: он в этом же namespace и существует
# всегда, откладывать его резолвинг незачем — наоборот, ошибка в имени
# обнаружится сразу при старте.
#
# Адрес resolver'а — CoreDNS кластера. В k3s он называется kube-dns
# (имя сохранено для совместимости), namespace kube-system.
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-configmap
namespace: django
data:
nginx.conf: |
worker_processes auto;
pid /var/run/nginx.pid;
events {
use epoll;
worker_connections 1024;
}
http {
# Basic Settings
large_client_header_buffers 8 128k;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 300;
types_hash_max_size 2048;
client_max_body_size 5000M;
client_header_buffer_size 5M;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging Settings
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# GZIP Settings
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
# Резолвер нужен для upstream'ов, заданных переменной (см. ниже).
resolver kube-dns.kube-system.svc.cluster.local valid=30s ipv6=off;
server {
listen 80;
listen [::]:80;
root /opt/react_client/;
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' '*' always;
add_header 'Access-Control-Allow-Headers' '*' always;
location = /static/index.bundle.js {
add_header Cache-Control 'no-store no-cache, must-revalidate, proxy-revalidate, max-age=0';
if_modified_since off;
expires off;
}
# Выбор способа аутентификации. Фронтенд на старте делает
# fetch("/static/config.json") и, если там есть auth_type,
# БЕЗУСЛОВНО перекрывает им режим, зашитый в сборку
# (src/Model/api/http-service.ts). Значения ровно два:
# "zitadel" и "original"; original — классический вход по
# логину и паролю с JWT, ради которого backend и получает из
# Vault ключи RS512.
#
# В образе sarex-frontend-dev:contour_* этот файл лежит со
# значением zitadel и хостом zitadel.contour.infra.sarex.tech —
# чужой контур, из aero недоступен. Из-за него платформа
# редиректила на /oauth/v2/authorize и получала
# invalid_request: The requested redirect_uri is missing
# in the client configuration
# то есть войти было нельзя в принципе.
#
# Отдаём файл прямо из nginx, не подкладывая том: сам
# static/config.json в sarex-frontend лежит в .gitignore и
# заполняется на каждом стенде своим — это штатная точка
# расширения, а не обход. В апстримном nginx.conf под него уже
# заведён такой же location =, только читающий с диска.
#
# Подменить через localStorage нельзя: оверрайд оттуда
# действует лишь для endpoint'ов stage, preprod и local,
# contour в этом списке нет.
#
# ВАЖНО ПРИ ПРОВЕРКЕ: правка НЕ видна с первой перезагрузки.
# Класс Config (src/shared/config/config.ts) держит копию в
# localStorage под ключом "config" и на старте берёт СИНХРОННО
# кеш, а файл сверяет уже асинхронно:
# if (this.cacheData) { this.data = this.cacheData;
# this.invalidateCache(); }
# а http-service.ts читает auth_type в момент импорта модуля.
# Поэтому первая загрузка после правки лишь обновляет кеш, а
# применяется он со второй. Ускорить —
# localStorage.removeItem('config')
# Консоль удобно открыть на /static/config.json: тот же origin,
# но JS не выполняется и редиректа на Zitadel не будет.
location = /static/config.json {
default_type application/json;
add_header Cache-Control 'no-store no-cache, must-revalidate, proxy-revalidate, max-age=0';
if_modified_since off;
expires off;
return 200 '{"auth_type":"original"}';
}
# location /api/pm/ из base удалён: сервис pm контуру не нужен.
location ~^/(api|admin)/ {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_pass http://backend-svc.django.svc.cluster.local:80;
}
# workspaces появится позже — резолвим лениво, через переменную.
location ~^/workspaces-v2/(.+).js {
proxy_http_version 1.1;
proxy_set_header Connection "";
set $up_workspaces frontend-svc.workspaces.svc.cluster.local;
rewrite /workspaces-v2/(.+) /$1 break;
proxy_pass http://$up_workspaces:80;
}
location ~^/workspaces-v2/(.+)\.wasm$ {
proxy_http_version 1.1;
proxy_set_header Connection "";
set $up_workspaces_wasm frontend-svc.workspaces.svc.cluster.local;
rewrite ^/workspaces-v2/(.+) /$1 break;
proxy_pass http://$up_workspaces_wasm:80;
}
location @index {
add_header Cache-Control 'no-cache, must-revalidate, proxy-revalidate, max-age=0';
if_modified_since off;
expires off;
try_files /static/index.html =404;
}
# processing появится позже — тоже лениво.
location ~^/workflows/(.+).js {
proxy_http_version 1.1;
proxy_set_header Connection "";
set $up_processing frontend-svc.processing.svc.cluster.local;
rewrite /workflows/(.+) /$1 break;
proxy_pass http://$up_processing:80;
}
location /service-worker.js {
try_files /static/$uri @index;
}
location / {
try_files $uri @index;
}
}
}

View File

@ -0,0 +1,41 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: measurements
resources:
- ../base
patches:
- target: {kind: HelmRelease, name: measurements}
patch: |
# Имя сервиса приводим к measurements-service, как в brusnika-*.
#
# В base сервис называется measurements-svc, а django ходит на
# MEASUREMENTS_HOST=http://measurements-service.measurements.svc.cluster.local:8000/api
# (apps/django/base/backend.yaml). Переименовать сервис дешевле, чем
# патчить переменную в двух релизах django (backend и celery), и заодно
# совпадает с тем, как это сделано в остальных кластерах.
- op: replace
path: /spec/values/services/backend/service/name/_default
value: measurements-service
# S3: бакет берём из секрета, а не из шаблона.
#
# В base имя бакета ЗАШИТО прямо в vault-шаблоне — "buckets":["measurements"].
# Из-за этого measurements складывал файлы в собственный бакет, тогда как
# медиахранилище контура одно и называется sarex-media-storage: туда пишет
# django (S3_BUCKET) и оттуда же раздаёт s3-proxy на маршруте /media/.
# Разные бакеты означали, что ссылка на файл измерения, отданная через
# django, вела бы в пустоту.
#
# Endpoint в base уже приезжает из секрета (.Data.data.client.endpoint) —
# достраиваем до той же схемы и бакет, чтобы имя задавалось в ОДНОМ месте
# (vault_app_secrets в aero/roles/sarex_stack/defaults/main.yml), а не
# дублировалось в манифесте. Заменить можно только аннотацию целиком:
# значение зашито внутри шаблона, через values его не перекрыть.
- op: replace
path: /spec/values/services/backend/podAnnotations/_default/vault.hashicorp.com~1agent-inject-template-measurements-s3
value: |-
{{- with secret "secrets/data/minio/apps/measurements" -}}
S3_JSON_SETTINGS='{"host":"{{ index .Data.data.client "endpoint" }}","login":"{{ index .Data.data "access_key" }}","password":"{{ index .Data.data "secret_key" }}","verify":false,"buckets":["{{ index .Data.data "bucket" }}"]}'
{{- end -}}

View File

@ -0,0 +1,309 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: processing
resources:
- ../base
patches:
# --- Состав: только api и engine-low ---------------------------------------
# engine удалён: в base у него replicaCount 0, то есть он и там выключен, а
# образ прибит к тегу с опечаткой в имени (workflows-endigne_prod). Рабочий
# обработчик — engine-low, он же на более свежем образе contour_*.
- target: {kind: HelmRelease, name: engine}
patch: |
$patch: delete
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: engine
namespace: processing
# frontend идёт из base без патчей. Адреса бэкендов вшиты в бандл на этапе
# сборки (константа __BUILD_ENV__), но сборка ugok2 обращается к тому же
# origin относительными путями — иначе в остальных кластерах не понадобился
# бы отдельный маршрут /workflows/api/ перед /workflows/. Поэтому фронтенд
# публикуется на домене платформы, а не на своём поддомене: маршруты в
# infrastructure/istio-config/aero, VirtualService platform.
# --- workflows-api ----------------------------------------------------------
- target: {kind: HelmRelease, name: workflows-api}
patch: |
# S3 приезжает из Vault отдельным ФАЙЛОМ, а не парой переменных:
# приложение читает по этому пути целый JSON. В base путь ведёт в
# /etc/sarex/yc-s3/, куда в контуре ничего не смонтировано.
- op: test
path: /spec/values/services/backend/envs/3/name
value: S3_SERVICE_ACCOUNT
- op: replace
path: /spec/values/services/backend/envs/3/value/_default
value: /vault/secrets/processing-s3
- op: add
path: /spec/values/services/backend/podAnnotations/_default/vault.hashicorp.com~1agent-inject-secret-processing-s3
value: secrets/data/minio/apps/processing
- op: add
path: /spec/values/services/backend/podAnnotations/_default/vault.hashicorp.com~1agent-inject-template-processing-s3
value: |-
{{- with secret "secrets/data/minio/apps/processing" -}}
{
"host": "{{ index .Data.data "host" }}",
"bucket": "{{ index .Data.data "bucket" }}",
"access_key_id": "{{ index .Data.data "access_key_id" }}",
"secret_access_key": "{{ index .Data.data "secret_access_key" }}",
"region": "{{ index .Data.data "region" }}",
"verify": false
}
{{- end -}}
# --- engine-low -------------------------------------------------------------
# Патчи адресуются по индексу, поэтому перед каждым replace идёт op: test на
# имя переменной. Если в base порядок envs изменится, сборка упадёт явно,
# вместо того чтобы молча перезаписать соседнее значение.
- target: {kind: HelmRelease, name: engine-low}
patch: |
# Sentry — внешний домен, из контура недостижим. Значение прокидывается
# ещё и в каждый под задачи, поэтому зануляем в одном месте.
- op: test
path: /spec/values/services/backend/envs/2/name
value: WORKFLOWS_SENTRY_DSN
- op: replace
path: /spec/values/services/backend/envs/2/value/_default
value: ""
# В base адрес django неверный даже для кластеров, где он есть: сервис
# называется backend-svc и слушает 80, а не backend:8000. У api то же
# значение уже правильное.
- op: test
path: /spec/values/services/backend/envs/5/name
value: DJANGO_HOST
- op: replace
path: /spec/values/services/backend/envs/5/value/_default
value: http://backend-svc.django.svc.cluster.local:80
# S3 — как у api выше.
- op: test
path: /spec/values/services/backend/envs/6/name
value: S3_SERVICE_ACCOUNT
- op: replace
path: /spec/values/services/backend/envs/6/value/_default
value: /vault/secrets/processing-s3
# SMTP выключаем целиком: почтового релея в контуре нет. Вместе с флагом
# убираем аннотацию инжекции secrets/vault/common/smtp_auth — секрета в
# Vault нет, а vault-agent-init на несуществующем секрете не отрендерит
# шаблон и оставит под в Init:Error, до флага дело не дойдёт.
- op: test
path: /spec/values/services/backend/envs/23/name
value: ENABLE_SMTP
- op: replace
path: /spec/values/services/backend/envs/23/value/_default
value: "0"
- op: remove
path: /spec/values/services/backend/podAnnotations/_default/vault.hashicorp.com~1agent-inject-secret-processing-smtp
- op: remove
path: /spec/values/services/backend/podAnnotations/_default/vault.hashicorp.com~1agent-inject-template-processing-smtp
# Смежные сервисы, которых в контуре нет: documentations (PDM,
# filestream), resources, bim-api, workspace, issues, comparisons,
# mailgun. Оставляем только S3 — единственное внешнее хранилище, которое
# в aero действительно есть.
- op: test
path: /spec/values/services/backend/envs/22/name
value: ENABLE_PDM_STORAGE
- op: replace
path: /spec/values/services/backend/envs/22/value/_default
value: "0"
- op: test
path: /spec/values/services/backend/envs/26/name
value: ENABLE_BIM_API_V2_DB
- op: replace
path: /spec/values/services/backend/envs/26/value/_default
value: "0"
- op: test
path: /spec/values/services/backend/envs/27/name
value: ENABLE_WORKSPACE_API_DB
- op: replace
path: /spec/values/services/backend/envs/27/value/_default
value: "0"
- op: test
path: /spec/values/services/backend/envs/28/name
value: ENABLE_ISSUE_API_DB
- op: replace
path: /spec/values/services/backend/envs/28/value/_default
value: "0"
- op: test
path: /spec/values/services/backend/envs/29/name
value: ENABLE_RESOURCES_API
- op: replace
path: /spec/values/services/backend/envs/29/value/_default
value: "0"
- op: test
path: /spec/values/services/backend/envs/31/name
value: ENABLE_PDM_API_DB
- op: replace
path: /spec/values/services/backend/envs/31/value/_default
value: "0"
- op: test
path: /spec/values/services/backend/envs/32/name
value: ENABLE_COMPARISONS_API_DB
- op: replace
path: /spec/values/services/backend/envs/32/value/_default
value: "0"
- op: test
path: /spec/values/services/backend/envs/33/name
value: ENABLE_MAIL_GUN
- op: replace
path: /spec/values/services/backend/envs/33/value/_default
value: "0"
# Планирование подов задач. engine работает kubernetes-исполнителем и сам
# создаёт Job'ы, подставляя им nodeSelector dedicated=processing и
# соответствующий toleration. Выделенных нод в контуре нет — все три
# равнозначны, — поэтому поды задач висли бы в Pending с
# didn't match Pod's node affinity/selector
# ровно как это было у postgresql (см. postRenderers в его релизе).
#
# ENABLE_TOLERATION ЗДЕСЬ НЕ ВЫКЛЮЧАТЬ. Сначала я поставил его в "0" — и
# это тихо сломало запуск задач совсем: в движке классы ресурсов
# регистрируются ПОД ЭТИМ ЖЕ ФЛАГОМ,
# if resCfg.EnableToleration { services[HighResources] = ... }
# (pkg/kube_services/services.go), поэтому задача с service_request
# "high-resources" падала не на планировании, а раньше — на разборе:
# ERROR create k8s job: create k8s job: unknown service
# Ни одного пода при этом не создавалось, и в логе не было ни слова про
# nodeSelector, так что связь с этим флагом неочевидна.
#
# Вместо выключения переводим селектор на метку, которая есть на всех
# нодах, — kubernetes.io/os=linux. Класс сервисов остаётся
# зарегистрированным, nodeSelector совпадает с любой нодой, а toleration
# на несуществующий taint безвреден. Запрашиваемые ресурсы трогать не
# надо: в base это 1 CPU и 1Gi.
#
# IGNORE_TAINTS_AND_NODE_SELECTOR (появился в base из master) полагаться
# на него не стоит: в master workflows-engine такой переменной нет. Образ
# при этом contour_a03d37da-dirty2 — ручная сборка, поднятая тем же
# коммитом master, что добавил переменную, так что она там, вероятно,
# поддержана. Проверить нечем, поэтому селектор задан явно, а не через
# неё; вреда от того, что она выставлена в base, нет.
- op: test
path: /spec/values/services/backend/envs/53/name
value: TOLERATION_KEY
- op: replace
path: /spec/values/services/backend/envs/53/value/_default
value: kubernetes.io/os
- op: test
path: /spec/values/services/backend/envs/54/name
value: TOLERATION_VALUE
- op: replace
path: /spec/values/services/backend/envs/54/value/_default
value: linux
- op: test
path: /spec/values/services/backend/envs/55/name
value: TOLERATION_KEY_HIGH_MEM
- op: replace
path: /spec/values/services/backend/envs/55/value/_default
value: kubernetes.io/os
- op: test
path: /spec/values/services/backend/envs/56/name
value: TOLERATION_VALUE_HIGH_MEM
- op: replace
path: /spec/values/services/backend/envs/56/value/_default
value: linux
- op: test
path: /spec/values/services/backend/envs/57/name
value: TOLERATION_KEY_PERSISTENT
- op: replace
path: /spec/values/services/backend/envs/57/value/_default
value: kubernetes.io/os
- op: test
path: /spec/values/services/backend/envs/58/name
value: TOLERATION_VALUE_PERSISTENT
- op: replace
path: /spec/values/services/backend/envs/58/value/_default
value: linux
- op: test
path: /spec/values/services/backend/envs/70/name
value: DEFAULT_NODE_SELECTOR_KEY
- op: replace
path: /spec/values/services/backend/envs/70/value/_default
value: kubernetes.io/os
- op: test
path: /spec/values/services/backend/envs/71/name
value: DEFAULT_NODE_SELECTOR_VALUE
- op: replace
path: /spec/values/services/backend/envs/71/value/_default
value: linux
# --- Vault для ПОДОВ ЗАДАЧ, а не для самого движка ----------------------
# В сборке contour_a03d37da-dirty2 движок умеет не монтировать в Job
# готовый Secret, а вешать на него аннотации vault-agent — тогда файл
# с кредами кладёт агент, и Secret в namespace задач не нужен вовсе.
# Именно так это работает в UGMK, где никакого yc-s3 в namespace нет.
#
# В master workflows-engine этого кода нет (сборка с невлитой ветки),
# поэтому набор переменных восстановлен по строкам бинарника:
# VAULT_ROLE, VAULT_SA, VAULT_MOUNT_PATH
# vault.hashicorp.com/{role,secret-volume-path,agent-inject-file-*,
# agent-inject-secret-*,agent-inject-template-*}
# yc-s3, yc-s3-service-account.json, /etc/sarex/yc-s3
#
# В base задан только VAULT_USE=true — остальных трёх нет НИ В ОДНОМ
# кластере репозитория, поэтому аннотации выходили неполными, агент в под
# задачи ничего не клал, и файл читался как пустой:
# ValueError: SERVICE_S3 is not valid
# Expecting value: line 1 column 1 (char 0)
#
# Переменных нет в base, поэтому op: add с "-" (добавление в конец
# списка), а не replace по индексу.
- op: add
path: /spec/values/services/backend/envs/-
value:
name: VAULT_ROLE
value:
_default: processing
- op: add
path: /spec/values/services/backend/envs/-
value:
name: VAULT_SA
value:
_default: processing-vault
# VAULT_MOUNT_PATH — вопреки имени НЕ корень KV, а ПОЛНЫЙ путь секрета:
# движок подставляет значение в шаблон целиком. Проверено по аннотациям
# созданного пода задачи:
# agent-inject-secret-yc-s3: <VAULT_MOUNT_PATH>
# agent-inject-template-yc-s3:
# {{- with secret "<VAULT_MOUNT_PATH>" -}}
# {{ index .Data.data "yc-s3-service-account.json" }}
# Отсюда два следствия: путь пишется в форме чтения KV v2 (с /data/), а в
# секрете обязан быть ключ с именем ФАЙЛА.
#
# Путь ОДИН НА ВСЕ ФАЙЛЫ, которые движок кладёт в поды задач: он
# подставляет VAULT_MOUNT_PATH в каждую аннотацию, меняя только имя
# ключа. Так, для django-auth аннотация ссылается на этот же путь и ищет
# в нём ключ django-auth.json. Поэтому имя пути нейтральное (job-files),
# а не по имени одного из файлов, — иначе оно врёт о содержимом.
# Сам секрет кладёт ansible, vault_app_secrets.
- op: add
path: /spec/values/services/backend/envs/-
value:
name: VAULT_MOUNT_PATH
value:
_default: secrets/data/apps/processing/job-files
# S3 из Vault, как у api.
- op: add
path: /spec/values/services/backend/podAnnotations/_default/vault.hashicorp.com~1agent-inject-secret-processing-s3
value: secrets/data/minio/apps/processing
- op: add
path: /spec/values/services/backend/podAnnotations/_default/vault.hashicorp.com~1agent-inject-template-processing-s3
value: |-
{{- with secret "secrets/data/minio/apps/processing" -}}
{
"host": "{{ index .Data.data "host" }}",
"bucket": "{{ index .Data.data "bucket" }}",
"access_key_id": "{{ index .Data.data "access_key_id" }}",
"secret_access_key": "{{ index .Data.data "secret_access_key" }}",
"region": "{{ index .Data.data "region" }}",
"verify": false
}
{{- end -}}

View File

@ -0,0 +1,86 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: workspaces
resources:
- ../base
patches:
# --- workspaces-api ---------------------------------------------------------
# Патчи адресуются по индексу, поэтому перед каждым replace идёт op: test на
# имя переменной. Если в base порядок envs изменится, сборка упадёт явно,
# вместо того чтобы молча перезаписать соседнее значение.
- target: {kind: HelmRelease, name: backend}
patch: |
# В base адрес django неверный ДВАЖДЫ: сервиса backend не существует
# (он называется backend-svc), и порт 8000 — это targetPort контейнера,
# а сам сервис принимает на 80. Проверено в кластере:
# backend-svc...:80 -> HTTP 200
# backend-svc...:8000 -> таймаут
# backend...:8000 -> имя не резолвится
# Ровно та же ошибка была в base у processing/engine-low.
- op: test
path: /spec/values/services/backend/envs/11/name
value: DJANGO_HOST
- op: replace
path: /spec/values/services/backend/envs/11/value/_default
value: http://backend-svc.django.svc.cluster.local:80
# ENVIRONMENT/ORIGINATOR в base говорят «prod» — это контур, а не прод.
# Значение уезжает в Sentry и в заголовки запросов к смежным сервисам.
- op: test
path: /spec/values/services/backend/envs/10/name
value: ENVIRONMENT
- op: replace
path: /spec/values/services/backend/envs/10/value/_default
value: aero
- op: test
path: /spec/values/services/backend/envs/12/name
value: DJANGO_ORIGINATOR
- op: replace
path: /spec/values/services/backend/envs/12/value/_default
value: aero_ws
# documentations в контуре нет и не будет. Выключить интеграцию целиком
# нечем: DOCUMENTATION_LOGGER_FEATURE (уже "0" в base) гасит только
# логирование, а GetDocumentByWS вызывается из обработчика GET workspace
# безусловно. Поэтому адрес оставлен «говорящим»: в логах будет видно имя
# несуществующего сервиса, а не молчаливый таймаут в никуда.
# Практическое следствие: карточка воркспейса отдаёт данные без документа.
- op: test
path: /spec/values/services/backend/envs/7/name
value: DOCUMENTATION_HOST
- op: replace
path: /spec/values/services/backend/envs/7/value/_default
value: http://documentations-api-svc.documentations.svc.cluster.local:80
# --- frontend ---------------------------------------------------------------
# Идёт из base без единого патча — и это осознанно, а не «руки не дошли».
#
# Фронтенд workspaces — это Module Federation remote с именем srx_workspaces,
# точка входа /module/remoteEntry.js. Его подгружает главный фронтенд
# платформы, поэтому наружу он НЕ публикуется: маршрут ему не нужен, нужен
# проксирующий location внутри nginx главного фронтенда. Он уже есть —
# apps/django/aero/nginx-configmap.yaml, location ~^/workspaces-v2/(.+).js,
# rewrite снимает префикс и проксирует в frontend-svc.workspaces:80.
#
# Версия — именно v2, хотя в репозитории есть и v1 (workspaces-frontend-static
# в apps/workspaces/dsinv). Так решает не выбор, а собранный бандл платформы.
# Проверено по трём независимым источникам:
#
# 1. В образе sarex-frontend-dev:contour_5.22.0 из 18 упоминаний
# remoteEntry.js воркспейсовое ровно одно — /workspaces-v2/module/...,
# а из имён ремоутов встречается только srx_workspacesV2
# (у v1 имя без суффикса — srx_workspaces, вхождений ноль).
# 2. Отдаваемый контуром /module/remoteEntry.js объявляет srx_workspacesV2.
# 3. В cr.yandex у образа workspaces-v2-frontend 61 тег contour_*,
# у workspaces-frontend (v1) — ни одного, только preprod и stage.
# Репозитория workspace-frontend в реестре нет вовсе.
#
# Тег в base — contour_2a4ce3fd — и есть САМАЯ СВЕЖАЯ contour-сборка:
# по датам создания образов в реестре она от 2026-07-24, следующая за ней
# contour_09b9baba от 2026-07-19. Собираются такие образы джобой
# build_contour из generic/build-contour-frontend, и её правило —
# ветка master при ENABLE_BUILD_IMAGE_CONTOUR=true, то есть contour_* по
# определению приезжает с мастера. Поэтому тег здесь не переопределяется:
# base уже указывает на нужное, и bump в base автоматически приедет сюда.

24
clusters/aero/apps.yaml Normal file
View File

@ -0,0 +1,24 @@
# Слой приложений. Отделён от инфраструктуры по той же причине, что controllers
# от configs (см. infrastructure.yaml): приложениям нужны уже готовые CRD и
# работающие вебхуки.
#
# dependsOn: infra-controllers ставит istiod и vault-agent-injector, а под
# тестового приложения без них не поднимется — MutatingWebhook инжектора
# перехватывает создание пода, и пока вебхук не готов, под не создастся.
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: apps
namespace: flux-system
spec:
interval: 2m0s
path: ./clusters/aero/apps
prune: true
wait: true
timeout: 10m0s
dependsOn:
- name: infra-controllers
sourceRef:
kind: GitRepository
name: flux-system

View File

@ -0,0 +1,17 @@
# Слой 3: прикладные сервисы контура.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../../apps/django/aero
# Сервис измерений. Django обращается к нему при MEASUREMENTS_USE_MEASUREMENTS=1
# (значение по умолчанию в base), поэтому без него часть API отвечает ошибкой.
- ../../../apps/measurements/aero
# Движок workflow. Развёрнут частично: только api и engine-low, без
# собственного фронтенда — подробности в apps/processing/aero.
- ../../../apps/processing/aero
# Воркспейсы: api и фронтенд-ремоут. Фронтенд наружу не публикуется — его
# подгружает главный фронтенд платформы через свой nginx (/workspaces-v2/).
- ../../../apps/workspaces/aero
# BIM: api и worker из aero/bimbackend. Не путать с apps/bim/base — там
# другое приложение (platform/bim-backend-v2), в контуре оно не нужно.
- ../../../apps/bim/aero

View File

@ -0,0 +1,8 @@
# Слой 2: ресурсы, которым нужны CRD из слоя controllers.
# Применяется только после того, как infra-controllers дошёл до Ready
# (dependsOn в clusters/aero/infrastructure.yaml).
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
# Внутренний удостоверяющий центр контура: selfsigned → корневой CA → издатель.
- ../../../infrastructure/cert-manager/aero/configs

View File

@ -0,0 +1,31 @@
# Слой 1: чарты инфраструктуры. Только HelmRelease и Namespace — никаких
# ресурсов, чьи CRD ставятся этими же чартами (см. clusters/aero/infrastructure.yaml).
#
# Порядок внутри слоя обеспечивают dependsOn в самих HelmRelease:
# istiod и ingressgateway ждут istio-base, istio-config ждёт всех троих.
#
# local-path-provisioner подключён ЯВНО, а встроенный в k3s — отключён флагом
# --disable=local-storage (см. docker-compose.yaml). Два провижинера с одним
# именем StorageClass дрались бы за одни и те же PVC, поэтому ровно один из них
# должен быть активен. Каталог данных не изменился: чарту задан тот же путь
# /var/lib/rancher/k3s/storage, что смонтирован с хоста.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
# Хранилище идёт первым: PVC rabbitmq и minio без StorageClass не создадутся.
- ../../../infrastructure/local-path-provisioner/aero
- ../../../infrastructure/cert-manager/aero
- ../../../infrastructure/istio-base/aero
- ../../../infrastructure/istio-pilot/aero
- ../../../infrastructure/istio-gateway/aero
- ../../../infrastructure/istio-config/aero
- ../../../infrastructure/dashboard/aero
# Только Vault Agent Injector: сам Vault живёт в compose, а не в кластере.
- ../../../infrastructure/vault-injector/aero
# СУБД, брокер и объектное хранилище. Креды берут из Vault через injector,
# поэтому идут после него. Админки rabbitmq и minio выставлены наружу через
# istio-config/aero; postgresql наружу не публикуется — доступ только изнутри
# кластера.
- ../../../infrastructure/postgresql/aero
- ../../../infrastructure/rabbitmq/aero
- ../../../infrastructure/minio/aero

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
# This manifest was generated by flux. DO NOT EDIT.
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: flux-system
namespace: flux-system
spec:
interval: 1m0s
ref:
branch: master
secretRef:
name: flux-system
url: http://gitea.flux-system.svc.cluster.local:3000/infra/iac.git
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: flux-system
namespace: flux-system
spec:
interval: 10m0s
path: ./clusters/aero
prune: true
sourceRef:
kind: GitRepository
name: flux-system

View File

@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- gotk-components.yaml
- gotk-sync.yaml

View File

@ -0,0 +1,14 @@
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: yc-oci-charts
namespace: flux-system
spec:
type: oci
interval: 10m0s
url: oci://cr.yandex/crp3ccidau046kdj8g9q/charts
# Секрет создаёт роль sarex_stack из ключа сервисного аккаунта
# (см. aero/roles/sarex_stack/templates/registry-secret.yaml.j2).
secretRef:
name: yc-cr-auth

View File

@ -0,0 +1,67 @@
# Инфраструктура разделена на два слоя, и это не косметика.
#
# Flux делает dry-run всей Kustomization целиком перед применением. Если
# положить cert-manager (который СТАВИТ CRD) и ClusterIssuer/Certificate
# (которые ЭТИ CRD используют) в один слой, dry-run падает с
# no matches for kind "Certificate" in version "cert-manager.io/v1"
# и не применяется НИЧЕГО — включая сам cert-manager. Дедлок: CRD никогда не
# появятся, потому что слой не может пройти проверку.
#
# Поэтому: controllers ставят чарты (и CRD), configs содержат только
# пользовательские ресурсы.
#
# ПОЧЕМУ У infra-configs НЕТ dependsOn (было — и создавало дедлок).
#
# В configs лежат ClusterIssuer'ы. Чарты из слоя controllers создают
# Certificate, а Flux после установки релиза ждёт готовности его ресурсов.
# С dependsOn получался круг:
# чарт с Certificate ждёт выпуска
# → выпуск ждёт ClusterIssuer
# → ClusterIssuer в configs
# → configs ждёт готовности controllers
# → а там висит тот самый чарт
# Наступали на это дважды: сначала istio-config, следом rabbitmq (тот провисел
# в "Running 'install' action" полчаса).
#
# Жёсткий порядок заменён сходимостью через повторы: пока cert-manager не
# поставил CRD, configs будет падать на dry-run и переприменяться каждые
# 10 минут, а как только CRD появятся — применится сам. Издатели при этом
# больше не зависят от здоровья чартов, которые в них нуждаются.
#
# apps по-прежнему ждёт controllers, и это правильно: без istiod и
# vault-agent-injector его поды физически не поднимутся.
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: infra-controllers
namespace: flux-system
spec:
# 2 минуты, а не дефолтные 10: интервал определяет, как быстро слой заметит
# новую ревизию и как быстро повторит попытку после неудачи. С 10 минутами
# цикл «правка → проверка» растягивался до получаса, а сломанный релиз
# держал слой всё это время.
interval: 2m0s
path: ./clusters/aero/controllers
prune: true
wait: true
timeout: 10m0s
sourceRef:
kind: GitRepository
name: flux-system
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: infra-configs
namespace: flux-system
spec:
interval: 2m0s
path: ./clusters/aero/configs
prune: true
# dependsOn намеренно отсутствует — см. комментарий в начале файла.
# Повторные попытки до появления CRD дешевле, чем дедлок.
retryInterval: 1m0s
sourceRef:
kind: GitRepository
name: flux-system

View File

@ -0,0 +1,32 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./flux-system
- ./helm-repositories.yaml
# Два слоя инфраструктуры с dependsOn — см. комментарий внутри файла.
- ./infrastructure.yaml
# Слой приложений. Пока пуст — сюда приедут сервисы при переносе из compose.
- ./apps.yaml
# Корневая Kustomization применяет этот же каталог, поэтому здесь она правит
# сама себя — и это единственный способ задать ей интервал.
#
# Файл flux-system/gotk-sync.yaml генерирует `flux bootstrap` и помечает
# "DO NOT EDIT": правка руками затрётся на следующем прогоне. Флаг
# --interval=2m у bootstrap (см. docker-compose.yaml) тоже не помогает — он
# задаёт периодичность только GitRepository, а Kustomization оставляет
# дефолтные 10 минут. Патч же применяется поверх сгенерированного файла и
# переживает регенерацию.
#
# Без этого определения самих слоёв (infrastructure.yaml, apps.yaml)
# доезжали бы до кластера с задержкой до 10 минут, даже когда все нижние
# слои уже проверяются раз в 2 минуты.
patches:
- target:
kind: Kustomization
name: flux-system
namespace: flux-system
patch: |
- op: replace
path: /spec/interval
value: 2m0s

View File

@ -0,0 +1,499 @@
# workflows-engine через k3s (планирование Job'ов) — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Добавить в контур `workflows-engine` как сервис docker-compose, который через kubeconfig-контекст создаёт Job'ы в k3s; Job-поды резолвят `postgres`/`minio` из compose-сети через k8s DNS-мост.
**Architecture:** engine — обычный compose-контейнер (до `workflow_db` ходит по compose-DNS), в k3s ходит out-of-cluster (`KUBE_CONFIG`+`KUBE_ADDR`). В namespace `processing` k3s заводятся Service+Endpoints `postgres`/`minio` на статические IP compose-контейнеров, чтобы Job-поды достукивались до БД и S3. Поднимаем только `k3s-server`.
**Tech Stack:** Docker Compose, k3s (rancher/k3s v1.36), kubectl, Ansible (роль `sarex_stack`), Go-сервис workflows-engine (конфиг через env, cleanenv).
**Спека:** `docs/superpowers/specs/2026-07-30-processing-engine-k3s-scheduling-design.md`
## Global Constraints
- Целевой хост — RedOS 8 (SELinux → volume-флаги `:z`/`:ro,z`), деплой из WSL через ansible роль `sarex_stack`.
- Все образы параметризуются `${VAR:-default}`; секреты — через `.env`, генерятся ролью.
- Только `k3s-server` (без `k3s-worker`). Downstream `ENABLE_*` engine выключены, кроме `ENABLE_S3_STORAGE`.
- Статические IP в compose: `postgres` = `172.28.0.10`, `minio` = `172.28.0.11`, subnet `172.28.0.0/16`.
- Имена в k8s = compose-имена: `postgres`, `minio`, namespace `processing`.
- Образ engine: `${SAREX_WORKFLOWS_ENGINE_IMAGE:-cr.yandex/crp3ccidau046kdj8g9q/workflows-endigne_prod:075fc0}`.
- Проверки инфраструктуры — не pytest: валидация `docker compose config -q`, `kubectl --dry-run`, коннект-чек по логам. Каждая задача завершается коммитом.
## File Structure
- `iac/docker-compose.yaml` (Modify) — top-level `networks.default` IPAM; static IP у `postgres`/`minio`; сервисы `engine`, `processing-k8s-init`.
- `iac/.env.example` (Modify) — `SAREX_WORKFLOWS_ENGINE_IMAGE`.
- `iac/k3s/manifests/processing/namespace.yaml` (Create) — namespace `processing`.
- `iac/k3s/manifests/processing/bridge-postgres.yaml` (Create) — Service+Endpoints `postgres`.
- `iac/k3s/manifests/processing/bridge-minio.yaml` (Create) — Service+Endpoints `minio`.
- `iac/backend/yc-s3-service-account.json` → нет; S3 SA кладём в `iac/engine/yc-s3-service-account.json` (Create, плейсхолдер на `http://minio:9000`).
- `iac/aero/roles/sarex_stack/tasks/main.yml` (Modify) — копирование `k3s/manifests` и `engine/`.
- `iac/aero/roles/sarex_stack/defaults/main.yml` (Modify) — `sarex_services` += `k3s-server`, `processing-k8s-init`, `engine`.
- `iac/aero/pyproject.toml` (Modify) — poe-таск проверки k8s-моста (опц.).
---
### Task 1: Параметр образа engine + S3 SA-плейсхолдер
**Files:**
- Modify: `iac/.env.example`
- Create: `iac/engine/yc-s3-service-account.json`
**Interfaces:**
- Produces: env-переменная `SAREX_WORKFLOWS_ENGINE_IMAGE`; файл SA `iac/engine/yc-s3-service-account.json`, монтируемый в engine на `/etc/sarex/yc-s3/yc-s3-service-account.json`.
- [ ] **Step 1: Добавить переменную образа в `.env.example`**
В секцию образов `iac/.env.example` добавить строку:
```dotenv
# workflows-engine (processing) — оркестратор Job'ов в k3s
SAREX_WORKFLOWS_ENGINE_IMAGE=cr.yandex/crp3ccidau046kdj8g9q/workflows-endigne_prod:075fc0
```
- [ ] **Step 2: Создать плейсхолдер S3 service-account JSON**
`iac/engine/yc-s3-service-account.json` (endpoint на прямой MinIO; app-креды подставит рантайм/ansible — тут плейсхолдер для планирования):
```json
{
"host": "http://minio:9000",
"bucket": "sarex-media-storage",
"access_key_id": "sarex-app",
"secret_access_key": "sarex-app-secret",
"region": "us-east-1",
"verify": false
}
```
- [ ] **Step 3: Проверка — файл валидный JSON**
Run: `python -c "import json;json.load(open('iac/engine/yc-s3-service-account.json'));print('ok')"`
Expected: `ok`
- [ ] **Step 4: Commit**
```bash
git add iac/.env.example iac/engine/yc-s3-service-account.json
git commit -m "feat(engine): образ workflows-engine + S3 SA-плейсхолдер"
```
---
### Task 2: Compose-сеть со статическими IP для postgres/minio
**Files:**
- Modify: `iac/docker-compose.yaml`
**Interfaces:**
- Produces: `postgres` доступен по `172.28.0.10`, `minio` по `172.28.0.11` в сети `default` (subnet `172.28.0.0/16`). На эти IP ссылаются k8s-Endpoints (Task 4).
- [ ] **Step 1: Добавить top-level `networks` c IPAM**
В конец `iac/docker-compose.yaml` (рядом с `volumes:`) добавить:
```yaml
networks:
default:
ipam:
config:
- subnet: 172.28.0.0/16
```
- [ ] **Step 2: Пин статического IP у `postgres`**
В сервис `postgres` добавить блок `networks`:
```yaml
networks:
default:
ipv4_address: 172.28.0.10
```
- [ ] **Step 3: Пин статического IP у `minio`**
В сервис `minio` добавить:
```yaml
networks:
default:
ipv4_address: 172.28.0.11
```
- [ ] **Step 4: Проверка — конфиг валиден и IP на месте**
Run (в `iac/`): `docker compose --env-file .env.example config | grep -A3 -E "ipv4_address|subnet"`
Expected: видно `subnet: 172.28.0.0/16`, `ipv4_address: 172.28.0.10` и `172.28.0.11`.
(Если docker недоступен локально — выполнить на сервере в `deploy_dir` после копирования.)
- [ ] **Step 5: Commit**
```bash
git add iac/docker-compose.yaml
git commit -m "feat(net): статические IP postgres/minio для k8s-моста"
```
---
### Task 3: k8s-манифесты DNS-моста (namespace + Service/Endpoints)
**Files:**
- Create: `iac/k3s/manifests/processing/namespace.yaml`
- Create: `iac/k3s/manifests/processing/bridge-postgres.yaml`
- Create: `iac/k3s/manifests/processing/bridge-minio.yaml`
**Interfaces:**
- Consumes: статические IP из Task 2 (`172.28.0.10`, `172.28.0.11`).
- Produces: в namespace `processing` резолвятся имена `postgres:5432` и `minio:9000` (→ compose-контейнеры).
- [ ] **Step 1: namespace**
`iac/k3s/manifests/processing/namespace.yaml`:
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: processing
```
- [ ] **Step 2: мост postgres (Service без селектора + Endpoints)**
`iac/k3s/manifests/processing/bridge-postgres.yaml`:
```yaml
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: processing
spec:
ports:
- name: pg
port: 5432
targetPort: 5432
protocol: TCP
---
apiVersion: v1
kind: Endpoints
metadata:
name: postgres
namespace: processing
subsets:
- addresses:
- ip: 172.28.0.10
ports:
- name: pg
port: 5432
protocol: TCP
```
- [ ] **Step 3: мост minio**
`iac/k3s/manifests/processing/bridge-minio.yaml`:
```yaml
apiVersion: v1
kind: Service
metadata:
name: minio
namespace: processing
spec:
ports:
- name: s3
port: 9000
targetPort: 9000
protocol: TCP
---
apiVersion: v1
kind: Endpoints
metadata:
name: minio
namespace: processing
subsets:
- addresses:
- ip: 172.28.0.11
ports:
- name: s3
port: 9000
protocol: TCP
```
- [ ] **Step 4: Проверка — YAML валиден (клиентский dry-run)**
Run (при наличии kubectl; иначе отложить на сервер):
`kubectl apply --dry-run=client -f iac/k3s/manifests/processing/`
Expected: `namespace/processing created (dry run)`, `service/postgres ...`, `endpoints/postgres ...`, `service/minio ...`, `endpoints/minio ...`.
Fallback без kubectl: `python -c "import glob,yaml; [list(yaml.safe_load_all(open(f))) for f in glob.glob('iac/k3s/manifests/processing/*.yaml')]; print('ok')"``ok`.
- [ ] **Step 5: Commit**
```bash
git add iac/k3s/manifests/processing/
git commit -m "feat(k3s): DNS-мост postgres/minio в namespace processing"
```
---
### Task 4: Сервис `processing-k8s-init` (применение манифестов)
**Files:**
- Modify: `iac/docker-compose.yaml`
**Interfaces:**
- Consumes: манифесты `./k3s/manifests/processing/` (Task 3), kubeconfig `./k3s/kubeconfig.yaml` (пишется `k3s-server`).
- Produces: applied namespace + мост в k3s; сервис завершается успешно (barrier для engine).
- [ ] **Step 1: Добавить сервис-хелпер (переиспользуем образ k3s — в нём есть kubectl)**
В `iac/docker-compose.yaml` (после `k3s-worker`/до `gitea` или рядом с processing-сервисами) добавить:
```yaml
# Одноразовый провижининг k3s: namespace processing + DNS-мост postgres/minio.
# Образ k3s содержит kubectl; server/tls перекрываем на k3s-server:6443.
processing-k8s-init:
image: ${K3S_IMAGE:-rancher/k3s:v1.36.2-k3s1}
container_name: processing-k8s-init
restart: "no"
depends_on:
k3s-server:
condition: service_healthy
entrypoint: ["/bin/sh", "-ec"]
command:
- |
kubectl --kubeconfig /kube/config \
--server https://k3s-server:6443 --insecure-skip-tls-verify \
apply -f /manifests/
volumes:
- ./k3s/kubeconfig.yaml:/kube/config:ro,z
- ./k3s/manifests/processing:/manifests:ro,z
```
- [ ] **Step 2: Проверка — конфиг валиден**
Run (в `iac/`): `docker compose --env-file .env.example config --services | grep processing-k8s-init`
Expected: `processing-k8s-init`.
- [ ] **Step 3: Commit**
```bash
git add iac/docker-compose.yaml
git commit -m "feat(k3s): processing-k8s-init — применение DNS-моста через kubectl"
```
---
### Task 5: Сервис `engine` в docker-compose
**Files:**
- Modify: `iac/docker-compose.yaml`
**Interfaces:**
- Consumes: `k3s-server` (kube-API), `processing-k8s-init` (мост applied), `postgres`/`postgres-init` (`workflow_db`), kubeconfig `./k3s/kubeconfig.yaml`, S3 SA `./engine/yc-s3-service-account.json` (Task 1).
- Produces: работающий оркестратор, создающий Job'ы в namespace `processing`.
- [ ] **Step 1: Добавить сервис `engine`**
В `iac/docker-compose.yaml` рядом с processing-сервисами:
```yaml
# workflows-engine — оркестратор: читает workflow_db, создаёт Job'ы в k3s.
# Ходит в k3s out-of-cluster (KUBE_CONFIG+KUBE_ADDR). K8s-исполнитель; AMQP выкл.
engine:
image: ${SAREX_WORKFLOWS_ENGINE_IMAGE:-cr.yandex/crp3ccidau046kdj8g9q/workflows-endigne_prod:075fc0}
container_name: engine
restart: unless-stopped
entrypoint: ["/engine"]
environment:
APP_NAME: workflows-engine
LOG_LEVEL: info
ENVIRONMENT: contour
# исполнители
ENABLE_KUBERNETES_EXECUTOR: "1"
ENABLE_AMQP_EXECUTOR: "0"
COUNT_RUNNING_WORKERS: "1"
COUNT_CANCELING_WORKERS: "1"
COUNT_HANDLE_JOB_WORKERS: "1"
WORKFLOW_PRIORITY: low
MAX_WORKFLOWS_LIMIT: "5"
# доступ в k3s (out-of-cluster)
KUBE_CONFIG: /kube/config
KUBE_CONTEXT: default
KUBE_ADDR: https://k3s-server:6443
JOBS_NAMESPACE: processing
# свой доступ к workflow_db (compose-DNS)
POSTGRES_ADDRESS: postgres
POSTGRES_PORT: "5432"
POSTGRES_DB: ${SAREX_PROCESSING_DB:-workflow_db}
POSTGRES_USER: ${SAREX_PROCESSING_DB_USER:-processing}
POSTGRES_PASSWORD: ${SAREX_PROCESSING_DB_PASSWORD:-processing-secret}
POSTGRES_POOL_SIZE: "20"
POSTGRES_SSL_USE: "0"
# хранилища: только S3, напрямую в minio
ENABLE_S3_STORAGE: "1"
S3_SERVICE_ACCOUNT: /etc/sarex/yc-s3/yc-s3-service-account.json
# дефолты планирования подов
DEFAULT_IMAGE_PULL_POLICY: IfNotPresent
DEFAULT_CPU_REQUESTS: 100m
DEFAULT_MEMORY_REQUESTS: 64Mi
volumes:
- ./k3s/kubeconfig.yaml:/kube/config:ro,z
- ./engine/yc-s3-service-account.json:/etc/sarex/yc-s3/yc-s3-service-account.json:ro,z
depends_on:
postgres:
condition: service_healthy
postgres-init:
condition: service_completed_successfully
k3s-server:
condition: service_healthy
processing-k8s-init:
condition: service_completed_successfully
```
- [ ] **Step 2: Проверка — конфиг рендерится, образ и зависимости на месте**
Run (в `iac/`): `docker compose --env-file .env.example config | grep -A2 "container_name: engine"`
Expected: сервис `engine` с образом `workflows-endigne_prod`.
Run: `docker compose --env-file .env.example config | grep -E "KUBE_ADDR|JOBS_NAMESPACE"`
Expected: `KUBE_ADDR: https://k3s-server:6443`, `JOBS_NAMESPACE: processing`.
- [ ] **Step 3: Commit**
```bash
git add iac/docker-compose.yaml
git commit -m "feat(engine): сервис workflows-engine (k8s-исполнитель через kubeconfig)"
```
---
### Task 6: Интеграция в Ansible-роль
**Files:**
- Modify: `iac/aero/roles/sarex_stack/defaults/main.yml`
- Modify: `iac/aero/roles/sarex_stack/tasks/main.yml`
**Interfaces:**
- Consumes: файлы Task 1-5 в репозитории `iac/`.
- Produces: `poe stack`/`install` копирует манифесты и `engine/`, поднимает `k3s-server`, `processing-k8s-init`, `engine`.
- [ ] **Step 1: Добавить сервисы в `sarex_services`**
В `iac/aero/roles/sarex_stack/defaults/main.yml`, список `sarex_services`, добавить (порядок не критичен — `depends_on` рулит; но добавим осмысленно):
```yaml
- k3s-server
- processing-k8s-init
```
и после `celery` (или рядом с processing) добавить:
```yaml
- engine
```
- [ ] **Step 2: Копировать каталог `k3s/manifests` на хост**
В `iac/aero/roles/sarex_stack/tasks/main.yml`, рядом с копированием nginx/backend, добавить задачу:
```yaml
- name: Скопировать k3s-манифесты (DNS-мост processing)
ansible.builtin.copy:
src: "{{ deploy_src_root }}/k3s/manifests/"
dest: "{{ deploy_dir }}/k3s/manifests/"
mode: "0644"
```
- [ ] **Step 3: Копировать каталог `engine/` (S3 SA) на хост**
Там же добавить:
```yaml
- name: Скопировать конфиги engine (S3 service-account)
ansible.builtin.copy:
src: "{{ deploy_src_root }}/engine/"
dest: "{{ deploy_dir }}/engine/"
mode: "0644"
```
- [ ] **Step 4: Проверка — YAML роли валиден и сервисы в списке**
Run (в `iac/aero/`): `uv run python -c "import yaml;d=yaml.safe_load(open('roles/sarex_stack/defaults/main.yml'));assert {'k3s-server','processing-k8s-init','engine'} <= set(d['sarex_services']);print('services ok')"`
Expected: `services ok`.
Run: `uv run python -c "import yaml;list(yaml.safe_load_all(open('roles/sarex_stack/tasks/main.yml')));print('tasks ok')"`
Expected: `tasks ok`.
- [ ] **Step 5: Commit**
```bash
git add iac/aero/roles/sarex_stack/defaults/main.yml iac/aero/roles/sarex_stack/tasks/main.yml
git commit -m "feat(ansible): деплой engine + k3s DNS-моста в роли sarex_stack"
```
---
### Task 7: Развёртывание и проверка планирования на сервере
**Files:** (нет изменений кода — деплой и верификация)
**Interfaces:**
- Consumes: всё выше, ветка `aero`, доступ к хосту через ansible из WSL.
- [ ] **Step 1: Задеплоить файлы и поднять стек**
Run (в `iac/aero/`, WSL): `uv run poe stack`
(копирует compose/манифесты/.env, `docker compose up -d` включая `k3s-server`, `processing-k8s-init`, `engine`.)
Expected: playbook завершается без ошибок; `changed`.
- [ ] **Step 2: Проверить, что k3s-server поднялся**
Run: `uv run poe ps` (или ad-hoc `docker compose ps`)
Expected: `k3s-server` в статусе `Up (healthy)`; `processing-k8s-init``Exited (0)`; `engine``Up`.
- [ ] **Step 3: Проверить DNS-мост в k3s**
Run (ad-hoc на хосте): `docker exec k3s-server kubectl -n processing get svc,endpoints`
Expected: Service `postgres`, `minio` и Endpoints с адресами `172.28.0.10:5432`, `172.28.0.11:9000`.
- [ ] **Step 4: Проверить коннект engine к workflow_db и kube-API**
Run: `uv run poe logs engine --tail 120`
Expected: в логах — успешное подключение к Postgres и к kube-API, нет паник (`failed to parse`, `connection refused`, `panic`).
Если engine падает на старте — сверить с риском №2 спеки (downstream `ENABLE_*`) и №1 (`KUBE_ADDR` vs правка `server:` в kubeconfig).
- [ ] **Step 5: Проверить резолв имён из тестового пода в k3s**
Run:
```bash
docker exec k3s-server kubectl -n processing run netcheck --rm -it --restart=Never \
--image=busybox:1.36 -- sh -c "nslookup postgres; nc -z -w3 postgres 5432 && echo PG_OK; nc -z -w3 minio 9000 && echo S3_OK"
```
Expected: `postgres` резолвится в ClusterIP, `PG_OK`, `S3_OK` (под достучался до compose-postgres/minio через мост).
- [ ] **Step 6: Проверить планирование Job'а**
Инициировать workflow (через processing-api/фронт или тестовую запись в `workflow_db`), затем:
Run: `docker exec k3s-server kubectl -n processing get jobs,pods`
Expected: появляется Job-объект, под запланирован на ноду (статус `Running`/`ContainerCreating`/`ImagePullBackOff` — любой из них подтверждает планирование; успешный прогон конвертации — вне рамок).
- [ ] **Step 7: Зафиксировать результат**
Никаких изменений кода; при необходимости — записать в память проекта факт готовности планирования и оставшиеся follow-up (валидный S3 SA, downstream-сервисы, образы задач).
---
## Self-Review
**1. Spec coverage:**
- Компонент A (engine compose) → Task 1, 5. ✓
- Компонент B (только k3s-server) → Task 6 (`sarex_services`), Task 7 (проверка). ✓
- Компонент C (DNS-мост postgres/minio) → Task 3, 4. ✓
- Сеть/статические IP → Task 2. ✓
- Ansible-интеграция → Task 6. ✓
- Порядок провижининга → `depends_on` (Task 4, 5) + Task 7. ✓
- Риски (KUBE_ADDR, стартовые проверки, S3 SA, образ задачи, cgroup/SELinux) → Task 7 Step 4/6 отсылают к рискам спеки. ✓
- Проверка результата (svc/endpoints, логи, jobs) → Task 7. ✓
**2. Placeholder scan:** S3 SA — намеренный плейсхолдер (задокументирован в спеке как приемлемый для планирования), не «TODO в требовании». Остальное — конкретный контент. ✓
**3. Type consistency:** имена согласованы сквозняком — `172.28.0.10`/`172.28.0.11`, namespace `processing`, Service/Endpoints `postgres`/`minio`, env `KUBE_ADDR=https://k3s-server:6443`, `JOBS_NAMESPACE=processing`, образ `workflows-endigne_prod:075fc0`. ✓
**Открытый нюанс для исполнителя:** если engine НЕ принимает `KUBE_ADDR` как override (риск №1), fallback — в `processing-k8s-init`/отдельном шаге sed-правкой заменить `server: https://127.0.0.1:6443` на `https://k3s-server:6443` в `./k3s/kubeconfig.yaml` и снять `KUBE_ADDR`, оставив `KUBE_CONFIG`+`KUBE_CONTEXT`.

View File

@ -0,0 +1,155 @@
# Дизайн: workflows-engine в контуре через k3s (планирование Job'ов)
Дата: 2026-07-30
Ветка: `aero`
Статус: согласован, готов к плану реализации
## Цель и рамки
Добавить в контур **workflows-engine** — фоновый оркестратор processing-подсистемы,
который вычитывает workflow из `workflow_db` и запускает задачи как **Kubernetes Job'ы**.
**Успех = планирование Job'ов:** engine стартует, подключается к `workflow_db`,
получает доступ к kube-API k3s и создаёт Job-объекты в namespace `processing`;
Job-поды планируются на ноду и достукиваются до `postgres`/`minio` из compose-сети.
**Вне рамок (принятые ограничения):**
- Полное end-to-end выполнение конвертаций НЕ гарантируется: engine тянет множество
downstream-сервисов (documentations/PDM, resources, bim-api-v2, workspace-api,
issue-api, comparisons, SMTP/Mailgun), которых в контуре нет. Все их `ENABLE_*`
выключаем; включаем только `ENABLE_S3_STORAGE`.
- Валидный S3 service-account для рантайма джоб не требуется для *планирования*
(S3 нужен на этапе выполнения задачи, не создания Job-объекта) — допускается плейсхолдер.
- Только одна нода k3s (`k3s-server`), без `k3s-worker`.
## Ключевые находки (обоснование дизайна)
Источники: `apps/processing/base/{api,engine,engine-low}.yaml`,
`apps/processing/workflows-engine.CONFIGURATION.md`.
1. **Джобы НЕ ходят в `workflow_db`.** В списке env, прокидываемых engine в Job-поды
(`pkg/kube_services/services.go`, CONFIGURATION.md §«…пробрасываемые в под'ы задач»),
`POSTGRES_*` отсутствует. В `workflow_db` ходит только сам engine. Джобам достаётся
S3 (через yc-s3 SA JSON при `ENABLE_S3_STORAGE`) и опционально JSON-конфиги прикладных
БД (bim/workspace/…) — только при соответствующих `ENABLE_*`.
2. **Доступ compose→k3s штатный.** У engine есть `KUBE_CONFIG` (путь; пусто = in-cluster,
задан = out-of-cluster), `KUBE_CONTEXT`, `KUBE_ADDR` (адрес apiserver + InsecureSkipTLSVerify),
`JOBS_NAMESPACE`. RBAC/ServiceAccount не нужны — аутентификация кредами из kubeconfig.
3. **RabbitMQ не нужен.** `pkg/rabbitmq` используется только при `ENABLE_AMQP_EXECUTOR=1`.
При K8s-исполнителе брокер не дёргается.
4. **`postgres:12-alpine`** генерит `pg_hba` как `host all all all md5` → пускает с любого
адреса по паролю. SNAT-источник (IP ноды k3s) не блокируется — нужен лишь верный
`processing`/пароль.
5. **`k3s-server` в compose уже готов:** `privileged`, `cgroup: host`, `--disable=traefik`,
kubeconfig пишется в `./k3s/kubeconfig.yaml`, в общей (default) compose-сети с `postgres`/`minio`.
## Архитектура
Три части, все в существующем `iac/docker-compose.yaml` + новые k8s-манифесты.
### A. engine — сервис docker-compose
Образ: `${SAREX_WORKFLOWS_ENGINE_IMAGE:-cr.yandex/crp3ccidau046kdj8g9q/workflows-endigne_prod:075fc0}`.
Ключевой env:
- Исполнители: `ENABLE_KUBERNETES_EXECUTOR=1`, `ENABLE_AMQP_EXECUTOR=0`.
- Воркеры: `COUNT_RUNNING_WORKERS=1`, `COUNT_CANCELING_WORKERS=1`, `COUNT_HANDLE_JOB_WORKERS=1`
(иначе задачи не разбираются).
- Доступ в k3s: `KUBE_CONFIG=/kube/config`, `KUBE_CONTEXT=default`,
`KUBE_ADDR=https://k3s-server:6443`, `JOBS_NAMESPACE=processing`.
(`--tls-san=k3s-server` уже задан у k3s-server; при использовании `KUBE_ADDR` TLS
проверка отключается, креды берутся из смонтированного kubeconfig.)
- Свой доступ к БД: `POSTGRES_ADDRESS=postgres`, `POSTGRES_PORT=5432`,
`POSTGRES_DB=${SAREX_PROCESSING_DB:-workflow_db}`, `POSTGRES_USER=${SAREX_PROCESSING_DB_USER:-processing}`,
`POSTGRES_PASSWORD=${SAREX_PROCESSING_DB_PASSWORD}`, `POSTGRES_SSL_USE=0`.
- Хранилища: `ENABLE_S3_STORAGE=1`, `S3_SERVICE_ACCOUNT=/etc/sarex/yc-s3/yc-s3-service-account.json`
(JSON указывает на `http://minio:9000` — прямой доступ к S3, мимо s3-proxy).
Остальные `ENABLE_*` = 0.
- Прочее по умолчанию: `WORKFLOW_PRIORITY=low`, `MAX_WORKFLOWS_LIMIT=5`, `MAX_RUNNING_JOBS`
дефолт, ресурсные `DEFAULT_*_REQUESTS`.
Монтирования:
- `./k3s/kubeconfig.yaml:/kube/config:ro,z` — kubeconfig из k3s-server.
- yc-s3 SA JSON (плейсхолдер/рабочий) → `/etc/sarex/yc-s3/yc-s3-service-account.json`.
`depends_on`:
- `postgres` (service_healthy), `postgres-init` (service_completed_successfully),
- `k3s-server` (service_healthy),
- `processing-k8s-init` (service_completed_successfully) — DNS-мост применён до старта engine.
### B. k3s — только сервер
Поднимаем существующий `k3s-server` (несёт kubelet → поды планируются на него).
`k3s-worker` НЕ поднимаем. Добавляем `k3s-server``processing-k8s-init`, `engine`)
в список поднимаемых сервисов ансибла.
### C. DNS-мост `postgres` + `minio` в k3s
Чтобы Job-поды резолвили те же имена, что и в compose:
- Namespace `processing`.
- `Service` (ClusterIP, без селектора) `postgres` и `minio` + ручные `Endpoints`,
указывающие на статические IP compose-контейнеров.
- Бареме-имена `postgres`/`minio` резолвятся в подах namespace `processing` через
search-domain `…processing.svc.cluster.local`.
Применение — одноразовый helper-контейнер **`processing-k8s-init`** (по образцу `postgres-init`):
образ с `kubectl`, монтирует `./k3s/kubeconfig.yaml` и каталог манифестов
(`./k3s/manifests/processing/`), выполняет `kubectl --kubeconfig … apply -f`.
Идемпотентно, `restart: "no"`. Ждёт `k3s-server` (service_healthy).
Манифесты (новый каталог `iac/k3s/manifests/processing/`):
- `namespace.yaml` — namespace `processing`.
- `bridge-postgres.yaml` — Service+Endpoints `postgres``172.28.0.10:5432`.
- `bridge-minio.yaml` — Service+Endpoints `minio``172.28.0.11:9000`.
## Сеть и стабильность адресов
Решение: **статические IP в compose** (прочтение «адреса как в docker-compose»).
- Верхнеуровневой `networks.default` задаём IPAM с фиксированным subnet, напр. `172.28.0.0/16`.
- `postgres``172.28.0.10`, `minio``172.28.0.11` (пиним только их; остальным IP динамический из того же subnet — их не трогаем).
- k8s `Endpoints` ссылаются на эти статические IP → не «плывут» при рестарте, декларативно.
Маршрут Job-под → `postgres`/`minio`:
под выходит через ноду `k3s-server` (её интерфейс в той же `172.28.0.0/16`),
трафик доходит до контейнера напрямую/по SNAT ноды. Аутентификация postgres —
по паролю с любого хоста (см. находку 4). S3 — по app-кредам MinIO.
## Интеграция с Ansible (`aero/roles/sarex_stack`)
- `sarex_services`: добавить `k3s-server`, `processing-k8s-init`, `engine`
(в правильном порядке; `depends_on` обеспечит последовательность).
- Копирование `iac/k3s/manifests/` на хост рядом с compose (в `deploy_dir`).
- Секреты: переиспользуем `SAREX_PROCESSING_DB_PASSWORD`, `SAREX_MINIO_APP_USER/PASSWORD`.
Новый образ — переменная `SAREX_WORKFLOWS_ENGINE_IMAGE` в `.env.example`.
- kubeconfig генерится k3s-server'ом в `./k3s/kubeconfig.yaml` при первом старте —
helper и engine его переиспользуют (server-адрес перекрывается `KUBE_ADDR`).
## Порядок провижининга
1. `k3s-server` up → healthy (apiserver `/readyz`).
2. `processing-k8s-init``kubectl apply` namespace + DNS-мост (Service/Endpoints).
3. `postgres` healthy + `postgres-init` completed (роль/БД `workflow_db`/`processing`).
4. `engine` up → коннектится к `workflow_db` + kube-API, пуллит workflow, создаёт Job'ы.
## Риски и точки проверки при реализации
1. **`KUBE_ADDR` vs правка server-URL.** Проверить, что engine с `KUBE_ADDR=https://k3s-server:6443`
+ кредами из kubeconfig реально ходит в apiserver (InsecureSkipTLSVerify). Fallback —
sed-правка `server:` в kubeconfig на `https://k3s-server:6443` (SAN уже покрывает имя).
2. **Стартовые проверки engine.** Убедиться, что engine поднимается с выключенными downstream
`ENABLE_*` и не падает на отсутствии их конфигов/URL при старте.
3. **Формат yc-s3 SA JSON.** Точная схема, которую ждёт S3-либа engine/джоб. Для *планирования*
не критично (S3 нужен в рантайме джобы) — плейсхолдер допустим, помечаем как follow-up.
4. **Образ Job-задачи.** Поды задач тянут приватные образы конверторов; при их отсутствии
под уйдёт в `ImagePullBackOff`. Для критерия «планирование» это допустимо — важно, что
Job-объект создан и под запланирован.
5. **cgroup/SELinux на RedOS.** `k3s-server` уже сконфигурен (`privileged`, `cgroup: host`),
но первый реальный старт k3s на хосте надо проверить (dmesg/логи kubelet).
## Проверка результата
- `kubectl --kubeconfig ./k3s/kubeconfig.yaml -n processing get svc,endpoints``postgres`, `minio` присутствуют.
- Логи engine: успешный коннект к `workflow_db` и kube-API, нет паник.
- Создать/инициировать workflow → `kubectl -n processing get jobs,pods` показывает созданный Job.
- (follow-up) end-to-end выполнение — отдельная задача с поднятием downstream-сервисов.

27
hack/entrypoint.sh Normal file
View File

@ -0,0 +1,27 @@
#!/bin/sh
# Точка входа образа с собранными манифестами.
#
# list — что собралось
# cat [префикс] — весь контур (или его часть) одним потоком
# export [путь] — скопировать манифесты в примонтированный каталог
# <команда> — выполнить как есть
set -eu
case "${1:-list}" in
list)
ls -1 /manifests
;;
cat)
shift
cat /manifests/"${1:-}"*.yaml
;;
export)
dst="${2:-/export}"
[ -d "$dst" ] || { echo "Каталог ${dst} не примонтирован" >&2; exit 1; }
cp /manifests/*.yaml "$dst"/
echo "Выгружено в ${dst}"
;;
*)
exec "$@"
;;
esac

61
hack/render-contour.sh Normal file
View File

@ -0,0 +1,61 @@
#!/bin/sh
# Рендер всех kustomization'ов, относящихся к одному контуру.
#
# render-contour.sh <контур> <каталог-результата>
#
# Используется из Dockerfile, но работает и локально — нужен только kustomize.
set -eu
contour="${1:?не задан контур}"
out="${2:?не задан каталог результата}"
if [ ! -d "clusters/${contour}" ]; then
echo "Неизвестный контур: ${contour}" >&2
echo "Доступные:" >&2
ls clusters | sed 's/^/ - /' >&2
exit 1
fi
mkdir -p "$out"
build() {
dir="$1"
# Имя файла повторяет путь: clusters/aero/apps -> clusters.aero.apps.yaml
name=$(echo "$dir" | tr '/' '.')
echo " build ${dir}"
# --load-restrictor LoadRestrictionsNone: слои ссылаются на apps/ и
# infrastructure/ через ../../.., то есть выходят за корень kustomization.
kustomize build --load-restrictor LoadRestrictionsNone "$dir" > "${out}/${name}.yaml"
}
# Каталоги контура: сам clusters/<contour> и все его слои (controllers, configs,
# apps, infrastructure — набор у контуров разный, поэтому ищем, а не перечисляем).
# flux-system исключён: gotk-components.yaml генерирует `flux bootstrap`,
# он и так лежит в репозитории готовым.
echo "Контур ${contour}: слои Flux"
find "clusters/${contour}" -name kustomization.yaml -not -path '*/flux-system/*' \
| sed 's#/kustomization.yaml$##' | sort > /tmp/layers.txt
while read -r dir; do
build "$dir"
done < /tmp/layers.txt
# Оверлеи приложений и инфраструктуры. Через слои они уже отрендерились, но
# отдельные файлы дают поресурсный diff и ловят оверлей, который в слои пока
# не подключён, — такой ломался бы уже при подключении.
for root in apps infrastructure; do
[ -d "$root" ] || continue
echo "Контур ${contour}: оверлеи ${root}/*/${contour}"
for dir in "$root"/*/"$contour"; do
[ -f "${dir}/kustomization.yaml" ] || continue
build "$dir"
done
done
# Пустой результат почти наверняка означает опечатку в имени контура —
# лучше упасть на сборке, чем отдать пустой образ.
if [ -z "$(ls -A "$out")" ]; then
echo "Для контура ${contour} не собрано ни одного манифеста" >&2
exit 1
fi
echo "Готово: $(ls -1 "$out" | wc -l) файлов в ${out}"

View File

@ -0,0 +1,53 @@
# Внутренний удостоверяющий центр контура aero.
#
# Зачем отдельный издатель: в репозитории все ClusterIssuer — ACME (Let's Encrypt)
# с решателем http01 и ingress class nginx. Контур закрытый, публичного DNS и
# входящего HTTP от ACME-сервера нет, поэтому ACME здесь принципиально нерабочий.
#
# Схема стандартная для внутренней PKI, в три шага:
# 1. selfsigned-issuer — самоподписывающий издатель, нужен только чтобы
# выпустить корневой сертификат;
# 2. aero-ca — сам корневой сертификат (isCA: true). Его приватный
# ключ попадает в secret aero-ca-key-pair в namespace cert-manager;
# 3. aero-ca-issuer — издатель, который подписывает этим корнем сертификаты
# для сервисов кластера (istio-gateway, dashboard и далее).
#
# Корневой сертификат нужно раздать клиентам (браузеры, curl), иначе они будут
# ругаться на недоверенный CA. Выгрузить его можно так:
# kubectl -n cert-manager get secret aero-ca-key-pair -o jsonpath='{.data.tls\.crt}' | base64 -d
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: selfsigned-issuer
spec:
selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: aero-ca
# Namespace обязателен и должен совпадать с cert-manager --cluster-resource-namespace
# (по умолчанию cert-manager): именно там ClusterIssuer ищет свой секрет.
namespace: cert-manager
spec:
isCA: true
commonName: aero-ca
secretName: aero-ca-key-pair
duration: 87600h # 10 лет — корень контура, ротация вручную
renewBefore: 720h # 30 суток
privateKey:
algorithm: ECDSA
size: 256
issuerRef:
name: selfsigned-issuer
kind: ClusterIssuer
group: cert-manager.io
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: aero-ca-issuer
spec:
ca:
secretName: aero-ca-key-pair

View File

@ -0,0 +1,47 @@
# Боевой издатель контура: Let's Encrypt через ACME.
#
# Раньше здесь был только внутренний CA (clusterissuer-ca.yaml) — исходили из
# того, что контур закрытый и до ACME-сервера не достучаться. Это оказалось не
# так: домены контура резолвятся в публичном DNS
# sarex.local.lonsdaleites.ru → 158.160.38.249
# *.sarex.local.lonsdaleites.ru → 158.160.38.249
# а порты 80/443 хоста опубликованы наружу (см. docker-compose.yaml, k3s-server).
# Значит http01-проверка проходит, и сертификаты можно брать настоящие.
#
# ВАЖНО ПРО WILDCARD: решатель http01 принципиально НЕ выдаёт wildcard-
# сертификаты — Let's Encrypt требует для них dns01. Поэтому сертификат контура
# перечисляет имена явно (см. infrastructure/istio-config/aero). Wildcard
# остался только в hosts самого Gateway, где сертификат не нужен.
#
# Внутренний CA (aero-ca-issuer) НЕ удалён: он остаётся запасным вариантом на
# случай, когда ACME недоступен, и для внутренних имён, которых нет в публичном
# DNS.
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-issuer-istio
spec:
acme:
# На этот адрес Let's Encrypt шлёт предупреждения об истечении.
email: "emelinda1990@gmail.com"
# ОТДЕЛЬНОЕ имя секрета, НЕ общепринятое в репозитории letsencrypt-secret-key.
#
# Тот секрет создаёт сам чарт cert-manager-infra (метки managed-by=Helm,
# аннотация helm.sh/resource-policy: keep) и кладёт в поле tls.key
# ЗАГЛУШКУ — невалидный PEM. cert-manager читает её как ключ ACME-аккаунта
# и падает:
# Account private key is invalid: error decoding private key PEM block
# Reason: ErrVerifyACMEAccount
# Удалить заглушку нельзя: чарт пересоздаст её на следующем апгрейде и
# затрёт настоящий ключ аккаунта. Поэтому издателю выдан собственный
# секрет — его создаёт и владеет им cert-manager, Helm его не трогает.
privateKeySecretRef:
name: letsencrypt-issuer-istio-account-key
server: "https://acme-v02.api.letsencrypt.org/directory"
solvers:
# class istio — challenge-Ingress обслуживает istio-ingressgateway,
# тот самый, что занимает hostPort 80/443 на k3s-server.
- http01:
ingress:
class: istio

View File

@ -0,0 +1,10 @@
# Пользовательские ресурсы cert-manager: применяются слоем infra-configs,
# после того как чарт из слоя infra-controllers поставил CRD.
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
# Внутренний CA — запасной издатель для имён, которых нет в публичном DNS.
- clusterissuer-ca.yaml
# Боевой издатель: Let's Encrypt, http01 через istio-ingressgateway.
- clusterissuer-letsencrypt.yaml

View File

@ -0,0 +1,32 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
# Здесь только чарт и namespace. Корневой CA (clusterissuer-ca.yaml) лежит
# рядом, но подключается СЛОЕМ ВЫШЕ — из clusters/aero/configs: его ресурсы
# требуют CRD, которые ставит этот же чарт.
#
# ACME-издатели из base выключены удалением, а не форком base: overlay
# остаётся патчем и не расходится при обновлении base. Плюс без этого они
# ломали бы dry-run слоя controllers — их CRD тоже ещё не существует.
patches:
- target:
kind: ClusterIssuer
name: letsencrypt-issuer
patch: |
$patch: delete
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-issuer
- target:
kind: ClusterIssuer
name: letsencrypt-issuer-istio
patch: |
$patch: delete
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-issuer-istio

View File

@ -0,0 +1,59 @@
# Учётка для входа в Kubernetes Dashboard.
#
# Чарт дашборда своего пользователя НЕ создаёт: он лишь показывает форму
# ввода токена и ходит в API от имени того, чей токен введён. Без этих трёх
# объектов дашборд открывается, но войти в него нечем.
#
# --- Про бессрочность токена -------------------------------------------------
# Начиная с Kubernetes 1.24 ServiceAccount больше не получает вечный токен
# автоматически, а `kubectl create token` выдаёт короткоживущий (по умолчанию
# час). Единственный штатный способ получить токен без срока действия —
# завести Secret типа kubernetes.io/service-account-token с аннотацией
# kubernetes.io/service-account.name: контроллер токенов сам положит в него
# поле token, и это будет legacy-токен, действующий, пока существует Secret.
#
# Значение поля token в манифесте НЕ указывается и в репозиторий не попадает:
# его проставляет кластер. Прочитать —
# kubectl -n kubernetes-dashboard get secret dashboard-admin-token \
# -o go-template='{{.data.token | base64decode}}'
#
# --- Про объём прав ----------------------------------------------------------
# Привязка идёт к встроенной роли cluster-admin: дашборд задуман как
# инструмент оператора — смотреть логи, заходить в контейнеры, перезапускать
# нагрузки, — и с меньшими правами половина его экранов отдаёт ошибки доступа.
#
# Плата за это честная: токен бессрочный, прав максимум, а дашборд опубликован
# наружу на dashboard.sarex.local.lonsdaleites.ru. Для закрытого тестового
# контура приемлемо; если контур перестанет быть одноразовым — либо сузить
# roleRef до встроенной view (дашборд станет read-only), либо удалить Secret,
# и токен мгновенно перестанет действовать.
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: dashboard-admin
namespace: kubernetes-dashboard
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: dashboard-admin
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
# Встроенная роль, своей не заводим: повторять cluster-admin вручную —
# это гарантированно отстать от списка ресурсов при обновлении кластера.
name: cluster-admin
subjects:
- kind: ServiceAccount
name: dashboard-admin
namespace: kubernetes-dashboard
---
apiVersion: v1
kind: Secret
metadata:
name: dashboard-admin-token
namespace: kubernetes-dashboard
annotations:
kubernetes.io/service-account.name: dashboard-admin
type: kubernetes.io/service-account-token

View File

@ -0,0 +1,38 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: dashboard
namespace: kubernetes-dashboard
spec:
interval: 2m
timeout: 10m
values:
# Kong слушает 8000 открытым HTTP и ничего не знает про istio. Без этого
# DestinationRule sidecar инициирует к нему mTLS, Kong видит сырое TLS-
# рукопожатие на плейнтекстовом порту, отвечает 400 — а наружу istio отдаёт
# 503. В логах Kong это выглядит как "\x16\x03\x01..." с SNI
# outbound_.80_._.dashboard-kong-proxy...
# tlsMode: DISABLE переводит обращение к нему в обычный HTTP.
destinationRule:
enabled: true
host: "dashboard-kong-proxy"
tlsMode: "DISABLE"
# Собственные VirtualService и Gateway чарта выключены: маршрутизация контура
# описана централизованно в infrastructure/istio-config/aero. Плюс родной VS
# чарта всё равно нерабочий — он ссылается на сервис
# kubernetes-dashboard-kong-proxy, которого не существует (реальное имя —
# dashboard-kong-proxy), и на домен dashboard.preprod.sarex.io, к контуру
# отношения не имеющий.
virtualService:
enabled: false
gateway:
enabled: false
# Образы лежат в приватном cr.yandex.
app:
image:
pullSecrets:
- regcred
kong:
image:
pullSecrets:
- regcred

View File

@ -0,0 +1,13 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
# ServiceAccount + ClusterRoleBinding + Secret с бессрочным токеном.
# Своего пользователя чарт дашборда не создаёт — обоснование в шапке файла.
- admin-user.yaml
patches:
- path: dashboard.yaml
target:
kind: HelmRelease
name: dashboard

View File

@ -0,0 +1,5 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base

View File

@ -0,0 +1,237 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: istio-config
namespace: default
spec:
interval: 2m
timeout: 10m
# disableWait РАЗРЫВАЕТ ДЕДЛОК, не украшательство.
#
# Чарт создаёт Certificate, а Flux после установки релиза ждёт готовности его
# ресурсов — в том числе этого сертификата. Выпустить его нельзя, пока нет
# ClusterIssuer, а издатели живут в слое infra-configs, который по dependsOn
# ждёт готовности infra-controllers, где и находится этот самый релиз. Круг:
# istio-config → Certificate → ClusterIssuer → infra-configs
# → infra-controllers → istio-config
# На практике это выглядело как вечное "Running 'upgrade' action" при
# Released: True и Certificate в состоянии
# "Issuing certificate as Secret does not exist".
#
# Ждать здесь всё равно нечего: чарт разворачивает только конфигурационные
# объекты (Gateway, VirtualService, Certificate), а не рабочие нагрузки.
# Сертификат выпустится асинхронно, как только появится издатель.
install:
disableWait: true
upgrade:
disableWait: true
# Список продублирован из base целиком: для CRD kustomize заменяет списки,
# а не сливает, поэтому частичный патч потерял бы зависимости от istio.
# cert-manager добавлен сверх base — чарт istio-config рендерит Certificate,
# и без готовых CRD установка падает.
dependsOn:
- name: istio-base
namespace: istio-system
- name: istiod
namespace: istio-system
- name: ingressgateway
namespace: istio-system
- name: cert-manager
namespace: cert-manager
values:
global:
env: aero
environments:
aero:
namespaces: []
# Блок обязателен, даже пустой: шаблоны gateway.yaml и virtualservice.yaml
# разыменовывают $env.istio.gateways / .virtualServices без проверки на
# nil и падают с "nil pointer evaluating interface {}".
istio:
# Один общий Gateway на весь контур вместо отдельного на каждый сервис.
# Так сделано потому, что у контура есть wildcard-домен: сертификат и
# Gateway покрывают *.sarex.local.lonsdaleites.ru целиком, а новый
# сервис публикуется добавлением ОДНОГО VirtualService — без правки
# сертификатов и Gateway. В прод-кластерах домены выписаны поштучно,
# там wildcard'а нет.
gateways:
contour:
name: contour-gateway
namespace: gateway
servers:
# ПОРЯДОК ЗНАЧИМ: первым обязан идти хост без звёздочки.
# Чарт собирает из hosts[0] имя порта
# name: {{ printf "%s-https-443" (index $server.hosts 0 ...) }}
# и НЕ заключает результат в кавычки. Хост "*.domain" дал бы
# токен "*-domain-https-443", который YAML читает как ссылку на
# якорь, и Helm падает с
# MalformedYAMLError: yaml: unknown anchor ... referenced
# Сами hosts чарт квотирует, поэтому wildcard ниже безопасен.
- hosts:
- sarex.local.lonsdaleites.ru
- "*.sarex.local.lonsdaleites.ru"
tls:
credentialName: contour-tls
# ВНИМАНИЕ: имя ресурса VirtualService чарт берёт из КЛЮЧА map
# ({{ $vsName }}), а поле name внутри — игнорирует, в отличие от
# gateways, где .name учитывается. Поэтому здесь его нет: ключ и есть
# имя. В остальных кластерах репозитория name у virtualServices
# присутствует, но ни на что не влияет.
virtualServices:
# Kubernetes Dashboard. Свой VirtualService чарт дашборда создаёт на
# домен dashboard.preprod.sarex.io — он к контуру aero отношения не
# имеет и никуда не резолвится. Этот выводит дашборд на домен
# контура; точка входа — kong-proxy, как во всех остальных кластерах.
dashboard:
namespace: gateway
hosts:
- dashboard.sarex.local.lonsdaleites.ru
gateways:
- gateway/contour-gateway
routes:
- path:
prefix: /
service: dashboard-kong-proxy.kubernetes-dashboard.svc.cluster.local
port: 80
# Платформа sarex: frontend в корне, backend на /api/ и /admin/.
# Порядок маршрутов значим — более специфичные префиксы идут
# первыми, иначе их перехватит правило для /.
platform:
namespace: gateway
hosts:
- sarex.local.lonsdaleites.ru
gateways:
- gateway/contour-gateway
routes:
# Медиафайлы отдаёт s3-proxy: он добавляет CORS и поддерживает
# Range-запросы, которых у S3 API «как есть» нет. Backend
# формирует ссылки вида /media/<key>, поэтому маршрут должен
# идти ПЕРЕД правилом для /.
#
# rewrite ОБЯЗАТЕЛЕН. s3-proxy не срезает префикс сам — он
# отображает путь URL прямо в ключ объекта. Без rewrite он
# искал бы media/orthophotos/... , тогда как в бакете лежит
# orthophotos/... , и отвечал бы
# NoSuchKey: The specified key does not exist (404)
# Проверено на живом объекте: /orthophotos/... отдаёт 200,
# /media/orthophotos/... — 404.
- path:
prefix: /media/
rewrite: /
service: s3-proxy-svc.django.svc.cluster.local
port: 80
# API микрофронтенда workflows. Висит на домене платформы, а
# не на отдельном поддомене: адреса вшиты в JS-бандл на этапе
# сборки и указывают на тот же origin относительными путями.
#
# Маршрут живёт в ЭТОМ VirtualService, а не в своём: Istio не
# сливает несколько VirtualService, привязанных к одной паре
# хост+gateway, — часть правил молча потерялась бы.
#
# ЗДЕСЬ НЕТ МАРШРУТА /workflows/ НА frontend-svc.processing, и
# это осознанно. Такой маршрут был и ломал навигацию: адрес
# вида /workflows/<uuid> — это путь ВНУТРИ SPA платформы,
# который должен отдать её index.html, а istio уводил его в
# nginx микрофронтенда, где такого файла нет:
# 404 Not Found — nginx/1.19.6
# Статика микрофронтенда при этом доезжает и без него: nginx
# платформы сам проксирует /workflows/*.js в processing
# (apps/django/aero/nginx-configmap.yaml). Проверено — 200.
- path:
prefix: /workflows/api/
rewrite: /api/
service: backend-svc.processing.svc.cluster.local
port: 80
- path:
prefix: /api/
service: backend-svc.django.svc.cluster.local
port: 80
- path:
prefix: /admin/
service: backend-svc.django.svc.cluster.local
port: 80
- path:
prefix: /
service: frontend-svc.django.svc.cluster.local
port: 80
# BIM API (aero/bimbackend). Отдельный поддомен, а не префикс на
# домене платформы: свой фронтенд у сервиса отсутствует, а его
# пути на общем домене конфликтовали бы с маршрутом /api/ django.
# Тот же адрес приложение знает про себя само —
# BIM_API_EXTERNAL_HOST в apps/bim/aero, оно строит по нему ссылки.
#
# Имя добавлено в dnsNames сертификата ниже: без этого браузер
# получит сертификат, в котором его нет.
bim:
namespace: gateway
hosts:
- bim.sarex.local.lonsdaleites.ru
gateways:
- gateway/contour-gateway
routes:
- path:
prefix: /
service: bim-api-service.bim.svc.cluster.local
port: 5555
# Management UI RabbitMQ.
rabbitmq:
namespace: gateway
hosts:
- rabbitmq.sarex.local.lonsdaleites.ru
gateways:
- gateway/contour-gateway
routes:
- path:
prefix: /
service: rabbitmq.rabbitmq.svc.cluster.local
port: 15672
# MinIO: консоль на /console/, S3 API в корне. Порядок маршрутов
# значим — более специфичный префикс должен идти первым, иначе его
# перехватит правило для /.
minio:
namespace: gateway
hosts:
- minio.sarex.local.lonsdaleites.ru
gateways:
- gateway/contour-gateway
routes:
- path:
prefix: /console/
service: minio-console.minio.svc.cluster.local
port: 9001
- path:
prefix: /
service: minio.minio.svc.cluster.local
port: 9000
requestAuthentications: {}
authorizationPolicies: {}
certManager:
certificates:
# Издатель — внутренний CA контура, а не ACME: до Let's Encrypt
# из закрытого контура не достучаться.
# См. infrastructure/cert-manager/aero/clusterissuer-ca.yaml
#
# Домены продублированы из aero/roles/sarex_stack/defaults/main.yml
# (platform_domain и производные). Единого источника нет — так же
# захардкожено во всех остальных кластерах репозитория.
#
# Один сертификат на весь контур, общий для всех его сервисов.
contour-tls:
# Имена перечислены ЯВНО, а не wildcard'ом: решатель http01
# принципиально не выдаёт wildcard-сертификаты, для них Let's
# Encrypt требует dns01, а webhook DNS-провайдера в контуре не
# настроен. Wildcard остался в hosts Gateway — там он про
# маршрутизацию, а не про TLS.
#
# Публикуешь новый сервис — добавь его имя сюда, иначе браузер
# получит сертификат, в котором этого имени нет.
dnsNames:
- sarex.local.lonsdaleites.ru
- dashboard.sarex.local.lonsdaleites.ru
- rabbitmq.sarex.local.lonsdaleites.ru
- minio.sarex.local.lonsdaleites.ru
- bim.sarex.local.lonsdaleites.ru
issuerRef:
# Боевой издатель, см. infrastructure/cert-manager/aero/configs.
name: letsencrypt-issuer-istio
kind: ClusterIssuer

View File

@ -0,0 +1,10 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
patches:
- path: istio-config.yaml
target:
kind: HelmRelease
name: istio-config

View File

@ -0,0 +1,17 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: ingressgateway
namespace: istio-system
spec:
interval: 5m
timeout: 10m
values:
# Ключ _internal_defaults_do_not_set — так значения именует сам чарт
# istio-gateway, это не ошибка.
_internal_defaults_do_not_set:
# Дефолт чарта — 3 реплики, но больше одной не влезает физически:
# под занимает hostPort 80/443, а nodeAffinity разрешает только
# control-plane, и такая нода в контуре одна (k3s-server). Остальные
# реплики висли бы в Pending навсегда. То же значение в yc-k8s-test.
replicaCount: 1

View File

@ -0,0 +1,10 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
patches:
- path: istio-gateway.yaml
target:
kind: HelmRelease
name: ingressgateway

View File

@ -0,0 +1,44 @@
# Ожидание готовности sidecar'а перед стартом приложения.
#
# ЗАЧЕМ. Envoy перехватывает исходящий трафик пода через iptables-правила,
# которые ставит init-контейнер istio-init. Правила появляются РАНЬШЕ, чем
# сам envoy начинает слушать, и приложение, дёрнувшее сеть в первые секунды,
# получает не таймаут, а connection refused: пакет уже завёрнут на порт
# envoy, а там ещё никого нет.
#
# Так падал workflows-api: контейнер стартовал и умирал в ту же секунду —
# INFO Starting workflows migrations
# INFO init database connection...
# FATAL [dial tcp 10.43.195.4:5432: connect: connection refused]
# при restartCount=0 у istio-proxy. Kubernetes перезапускал его, со второй
# попытки envoy успевал подняться, и дальше всё работало. То есть симптом —
# ровно один рестарт на старте, легко принимаемый за случайность.
#
# ПОЧЕМУ НА УРОВНЕ MESH, А НЕ АННОТАЦИЕЙ НА ПОДЕ. Уязвимы все, кто ходит в
# БД или брокер сразу при запуске, а это почти каждый бэкенд контура:
# django катит migrate первым делом, celery подключается к rabbitmq,
# workspaces-api и engine-low — к postgres. Они не падали не потому, что
# устроены иначе, а потому что успевали. Чинить это по одному поду —
# значит ждать, пока каждый однажды не повезёт.
#
# ЦЕНА. Каждый под стартует на время готовности envoy дольше (обычно 1-2 с):
# istio добавляет в sidecar postStart-хук, который блокирует запуск
# остальных контейнеров. Для контура это несопоставимо дешевле, чем
# ложные падения на старте.
#
# На Job'ы не влияет: у всех, что есть в контуре, инжекция отключена
# аннотацией sidecar.istio.io/inject: "false" — иначе envoy не давал бы им
# завершиться.
#
# Настройка действует В МОМЕНТ ИНЖЕКЦИИ, поэтому на уже запущенные поды не
# распространяется: хук появится у них при следующем пересоздании.
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: istiod
namespace: istio-system
spec:
values:
meshConfig:
defaultConfig:
holdApplicationUntilProxyStarts: true

View File

@ -0,0 +1,8 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
patches:
# holdApplicationUntilProxyStarts — обоснование в шапке файла.
- path: istio-pilot.yaml

View File

@ -0,0 +1,10 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
patches:
- path: local-path-provisioner.yaml
target:
kind: HelmRelease
name: local-path-provisioner

View File

@ -0,0 +1,62 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: local-path-provisioner
namespace: local-path-provisioner
spec:
interval: 2m
timeout: 10m
values:
replicaCount: 1
image:
repository: cr.yandex/crp3ccidau046kdj8g9q/contour/local-path-provisioner-nn/local-path-provisioner
tag: v0.0.24
pullPolicy: IfNotPresent
helperImage:
repository: cr.yandex/crp3ccidau046kdj8g9q/contour/local-path-provisioner-nn/busybox
tag: latest
imagePullSecrets:
- name: regcred
defaultSettings:
registrySecret: null
privateRegistry:
registryUrl: null
registryUser: null
registryPasswd: null
storageClass:
create: true
# Класс по умолчанию: встроенный в k3s провижинер отключён
# (--disable=local-storage в docker-compose.yaml), и default-класса в
# кластере иначе не остаётся — PVC без явного storageClassName висели бы
# в Pending.
defaultClass: true
name: local-path
reclaimPolicy: Delete
# Путь ВНУТРИ контейнера ноды. Взят тот же, что использовал встроенный
# провижинер, потому что он уже смонтирован с хоста:
# ./k3s-storage/<нода> -> /var/lib/rancher/k3s/storage
# Благодаря этому данные PVC переживают пересоздание контейнеров нод и
# доступны для бэкапа обычными средствами хоста. Дефолт чарта
# (/opt/local-path-provisioner) никуда не смонтирован — данные исчезли бы
# вместе с контейнером ноды.
nodePathMap:
- node: DEFAULT_PATH_FOR_NON_LISTED_NODES
paths:
- /var/lib/rancher/k3s/storage
rbac:
create: true
serviceAccount:
create: true
name: ""
resources: {}
nodeSelector: {}
tolerations: []
affinity: {}
configmap:
name: local-path-config
setup: |-
set -eu
mkdir -m 0777 -p "$VOL_DIR"
teardown: |-
set -eu
rm -rf "$VOL_DIR"

View File

@ -0,0 +1,87 @@
---
# Заведение бакетов. Чарт MinIO этого не умеет — форк не принимает ни buckets,
# ни provisioning, — а приложения ждут бакет готовым: django падает на загрузке
# файлов, measurements — на старте, если бакета из его vault-шаблона нет.
#
# До этого Job'а бакеты заводились руками через mc. Такой бакет не переживает
# пересоздание контура и не виден в репозитории: развернув всё с нуля, ты
# получишь работающий MinIO и приложения, падающие на отсутствующем бакете.
#
# Идемпотентность даёт mc mb --ignore-existing: Job гоняется при каждом
# применении релиза (ttl удаляет завершившийся), повторный запуск на готовых
# бакетах отрабатывает вхолостую и завершается нулём.
apiVersion: batch/v1
kind: Job
metadata:
name: minio-buckets
namespace: minio
annotations:
# Пересоздавать Job при правке: spec.template у Job неизменяем, обычный
# apply на изменившемся списке бакетов падает с "field is immutable".
#
# ЗНАЧЕНИЕ ИМЕННО enabled. Аннотации Flux — force, prune, ssa — принимают
# enabled/disabled, а не булево. Написанное здесь раньше "true" Flux молча
# игнорировал: пока Job'а не существовало, обычный apply проходил и всё
# выглядело рабочим, а на первой же правке скрипта слой infra-controllers
# встал целиком —
# Job/minio/minio-buckets dry-run failed (Invalid): field is immutable
# и вместе с ним встал зависящий от него слой apps.
kustomize.toolkit.fluxcd.io/force: enabled
spec:
# Job живёт час после завершения и удаляется. Не 0: если он упадёт, логи
# должны остаться доступными для разбора.
ttlSecondsAfterFinished: 3600
backoffLimit: 6
template:
metadata:
annotations:
# Sidecar istio не даёт Job завершиться: контейнер задачи выходит, а
# envoy продолжает работать, и под навсегда остаётся Running.
sidecar.istio.io/inject: "false"
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-init-first: "true"
vault.hashicorp.com/agent-pre-populate-only: "true"
vault.hashicorp.com/auth-path: auth/kubernetes
vault.hashicorp.com/role: minio
vault.hashicorp.com/agent-inject-secret-admin: secrets/data/minio/admin
vault.hashicorp.com/agent-inject-template-admin: |-
{{- with secret "secrets/data/minio/admin" -}}
MINIO_ROOT_USER={{ index .Data.data "rootUser" }}
MINIO_ROOT_PASSWORD={{ index .Data.data "rootPassword" }}
{{- end -}}
spec:
restartPolicy: OnFailure
serviceAccountName: minio-sa
imagePullSecrets:
- name: regcred
containers:
- name: mc
# Тот же образ, которым бакеты заводил compose-стек (SAREX_MC_IMAGE
# в docker-compose.yaml). Тег фиксированный: latest в приватном
# реестре не публикуется.
image: cr.yandex/crp3ccidau046kdj8g9q/mc:250724
command: ["/bin/sh", "-ec"]
args:
- |
. /vault/secrets/admin
# Ждём готовности API: Job стартует вместе с релизом и обгоняет
# готовность самого MinIO.
until mc alias set contour http://minio.minio.svc.cluster.local:9000 \
"$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null 2>&1; do
echo "жду MinIO..."
sleep 5
done
# Бакет в контуре ОДИН: в него пишет django, из него раздаёт
# s3-proxy на маршруте /media/, туда же переведён measurements.
# Раньше у measurements в vault-шаблоне был зашит собственный
# бакет, и файл, положенный им, по ссылке из django не отдавался.
#
# Ранее созданный бакет measurements этой правкой НЕ удаляется:
# Job умеет только mc mb --ignore-existing. Убирать его — отдельное
# осознанное решение, здесь мы лишь перестаём его заводить.
for b in sarex-media-storage; do
mc mb --ignore-existing "contour/$b"
echo "бакет $b готов"
done

View File

@ -0,0 +1,12 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
# Заведение бакетов: чарт этого не умеет, см. шапку файла.
- buckets-job.yaml
patches:
- path: minio.yaml
target:
kind: HelmRelease
name: minio

View File

@ -0,0 +1,40 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: minio
namespace: minio
spec:
interval: 2m
timeout: 10m
values:
nameOverride: "minio"
mode: standalone
replicas: 1
drivesPerNode: 1
imagePullSecrets:
- name: regcred
environment:
# Внешние адреса: консоль отдаётся с /console/ того же домена,
# маршрутизацию задаёт infrastructure/istio-config/aero.
MINIO_SERVER_URL: "https://minio.sarex.local.lonsdaleites.ru"
MINIO_BROWSER_REDIRECT_URL: "https://minio.sarex.local.lonsdaleites.ru/console/"
MINIO_API_CORS_ALLOW_ORIGIN: "https://minio.sarex.local.lonsdaleites.ru"
# Root-креды приезжают из Vault через vault-agent-injector: секрет кладёт
# ansible (роль sarex_stack, vault_app_secrets), роль minio в auth/kubernetes
# создаётся там же. В манифестах и в git паролей нет.
vaultRoot:
enabled: true
role: minio
authPath: auth/kubernetes
secretPath: secrets/data/minio/admin
rootUserKey: rootUser
rootPasswordKey: rootPassword
# В yc-k8s-test minio прибит к выделенным нодам (nodeSelector/tolerations
# dedicated=s3). В контуре aero таких нод нет — всего три, и планировщику
# ничего ограничивать не нужно.
persistence:
storageClass: local-path
size: 20Gi
resources:
requests:
memory: 512Mi

View File

@ -0,0 +1,10 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
patches:
- path: postgresql.yaml
target:
kind: HelmRelease
name: postgresql

View File

@ -0,0 +1,187 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: postgresql
namespace: postgresql
spec:
interval: 2m
# Разворачиваем пустой инстанс: базы создаются с нуля, дампы не
# восстанавливаются. Поэтому таймауты обычные, а не двухчасовые как в
# yc-k8s-test — там они заданы под restoreFromDump.
#
# 10 минут, а не 20: таймаут — это ещё и цена одной неудачной итерации.
# Пока стояло 20, цикл «install → таймаут → откат → install» занимал по
# 20 минут на круг, и правка, приехавшая в середине круга, подхватывалась
# только на следующем. Здоровый инстанс поднимается примерно за две минуты,
# так что 10 — с большим запасом.
timeout: 10m
install:
timeout: 10m
remediation:
retries: 3
upgrade:
timeout: 10m
remediation:
retries: 3
# Снимаем nodeSelector, который чарт ставит по умолчанию.
#
# В дефолтах чарта (primary.nodeSelector) прописано dedicated: sts — расчёт на
# кластер с выделенным пулом нод под StatefulSet'ы. В контуре aero таких нод
# нет, все три равнозначны, и под навсегда повисал в Pending:
# 0/3 nodes are available: 3 node(s) didn't match Pod's node affinity/selector
# а следом застревал PVC, потому что у local-path режим WaitForFirstConsumer
# и том создаётся только после планирования пода.
#
# Через values это не снять: Helm сливает карты по ключам, поэтому и {}, и
# любая другая метка оставят dedicated: sts на месте. В yc-k8s-test дефолт не
# убирают, а перекрывают на dedicated: db — там выделенные ноды есть.
# Тут остаётся убрать ключ из уже отрендеренного манифеста.
#
# Tolerations чарта не трогаем: они разрешают taint dedicated=sts, которого на
# наших нодах нет, и ни на что не влияют.
postRenderers:
- kustomize:
patches:
- target:
kind: StatefulSet
name: postgresql
patch: |
- op: remove
path: /spec/template/spec/nodeSelector
values:
global:
security:
allowInsecureImages: true
defaultStorageClass: local-path
imagePullSecrets:
- regcred
postgresql:
auth:
# Пусто осознанно: пользователя и базу заводит не чарт-бутстрап
# bitnami, а contour-логика ниже (contour.databases). Оставь здесь
# значения — получишь лишнюю базу-пустышку.
username: ""
database: ""
secretKeys:
userPasswordKey: "postgres-password"
auth:
username: ""
database: ""
secretKeys:
userPasswordKey: "postgres-password"
image:
registry: cr.yandex/crp3ccidau046kdj8g9q
repository: contour/postgresql
tag: 17.0.7
pullPolicy: IfNotPresent
metrics:
enabled: false
prometheusRule:
enabled: false
primary:
# Токен ServiceAccount нужен vault-agent'у для логина в Vault.
automountServiceAccountToken: true
containerSecurityContext:
readOnlyRootFilesystem: false
persistence:
storageClass: local-path
size: 20Gi
# В yc-k8s-test здесь nodeSelector/tolerations dedicated=db. В контуре
# aero выделенных под БД нод нет — все три равнозначны, ограничивать
# планировщик нечем и незачем.
customLivenessProbe:
exec:
command:
- /bin/sh
- -c
- exec pg_isready -U "postgres" -d postgres -h 127.0.0.1 -p 5432
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 6
customReadinessProbe:
exec:
command:
- /bin/sh
- -c
- exec pg_isready -U "postgres" -d postgres -h 127.0.0.1 -p 5432
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 6
customStartupProbe:
exec:
command:
- /bin/sh
- -c
- exec pg_isready -U "postgres" -d postgres -h 127.0.0.1 -p 5432
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 6
# contour-логика: создаёт роли, базы и расширения по списку ниже, пароли
# берёт из Vault через vault-agent-injector.
contour:
enabled: true
adminUser: "postgres"
vault:
enabled: true
role: postgresql
authPath: auth/kubernetes
# Пароль администратора.
secretPath: secrets/data/postgresql/admin
secretKey: postgres-password
# Пароли пользователей баз: ключи внутри секрета сопоставляются с
# passwordKey каждой базы. Чарт требует этот путь при vault.enabled.
# Секрет кладёт ansible (роль sarex_stack, vault_app_secrets).
usersSecretPath: secrets/data/postgresql/users
# timescaledb включён заранее, хотя нашим четырём базам он не нужен:
# shared_preload_libraries читается только при старте, и добавить его
# позже — значит перезапустить СУБД. Сервисы, которым он понадобится
# (issues, subscriptions, system-log), уже есть в списке на перенос.
sharedPreloadLibraries: "timescaledb,pg_stat_statements"
# Набор баз повторяет то, что сейчас провижинит postgres-init в
# docker-compose.yaml, — контур переезжает в k3s тем же составом.
# passwordKey должен совпадать с ключом в секрете Vault
# secrets/postgresql/users.
databases:
- name: sarex_db
user: django
passwordKey: django
extensions:
- ltree
restoreFromDump: false
- name: workflow_db
user: processing
passwordKey: processing
# Роль processing не суперпользователь, а её миграции делают
# CREATE EXTENSION — поэтому расширения создаёт бутстрап заранее.
extensions:
- uuid-ossp
- ltree
- hstore
restoreFromDump: false
# Имя ОБЯЗАНО быть bimapidb: в aero/bimbackend оно захардкожено и
# переменной не задаётся — и в entrypoint_api.sh (yoyo apply
# ... /bimapidb), и в models/database.py (PooledPostgresqlExtDatabase
# ('bimapidb', ...)). С любым другим именем приложение падает на
# старте: FATAL: database "bimapidb" does not exist.
- name: bimapidb
user: bim
passwordKey: bim
# ltree обязателен: models/database.py при инициализации вызывает
# register_ltree(conn) и падает с
# psycopg2.ProgrammingError: ltree type not found in the database
# Роль bim не суперпользователь и CREATE EXTENSION сама не сделает,
# поэтому расширение заводит бутстрап — как у sarex_db и workflow_db.
extensions:
- ltree
restoreFromDump: false
- name: workspace_db
user: workspace
passwordKey: workspace
extensions: []
restoreFromDump: false

View File

@ -0,0 +1,14 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
patches:
- path: rabbitmq.yaml
target:
kind: HelmRelease
name: rabbitmq
# JSON6902-патча с null здесь БОЛЬШЕ НЕТ — он не работал.
# kustomize нормализовал null обратно в {}, а пустую карту Helm сливает с
# дефолтами чарта, возвращая их целиком. Отключение перенесено в postRenderers
# внутри rabbitmq.yaml, где ресурсы вырезаются уже из готового манифеста.

View File

@ -0,0 +1,117 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: rabbitmq
namespace: rabbitmq
spec:
interval: 2m
timeout: 10m
# Вырезаем ресурсы ПОСЛЕ рендера чарта. Через values это недостижимо:
# * `null` до Helm не доезжает — kustomize нормализует его в {};
# * `{}` Helm сливает с дефолтом (coalesceMaps), и дефолт возвращается.
# postRenderers работает уже с готовым манифестом, где коалесинг позади.
#
# Что вырезаем: собственные Gateway/VirtualService/Certificate чарта на
# посторонний домен rabbitmq.infra.sarex.io. Маршрутизация контура описана
# централизованно в infrastructure/istio-config/aero.
#
# Особенно важен rmq-tls: домен не наш, ACME отдаёт 503 и проверка не пройдёт
# никогда. Пока он был в релизе, Helm ждал его выпуска, install падал по
# таймауту, срабатывал откат — и весь слой infra-controllers стоял.
postRenderers:
- kustomize:
patches:
- target:
kind: Gateway
name: rmq-gateway
patch: |
$patch: delete
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: rmq-gateway
namespace: gateway
- target:
kind: VirtualService
name: rmq-virt-service
patch: |
$patch: delete
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: rmq-virt-service
namespace: rabbitmq
- target:
kind: Certificate
name: rmq-tls
patch: |
$patch: delete
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: rmq-tls
namespace: gateway
values:
global:
security:
allowInsecureImages: true
# Здесь НЕТ ключей virtualService/gateway/certificate, и это намеренно:
# через values дефолты чарта не убираются вообще никак.
#
# Helm : удалить дефолт можно ТОЛЬКО значением null;
# {} он глубоко сливает с дефолтом, и дефолт остаётся;
# kustomize : в strategic-merge-патче null означает УДАЛИТЬ КЛЮЧ,
# в JSON6902 — нормализуется обратно в {}.
#
# То есть до Helm настоящий null не доходит ни одним путём. Поэтому ресурсы
# вырезаются postRenderers'ом выше, уже из отрендеренного манифеста.
metrics:
serviceMonitor:
enabled: false
default:
enabled: false
perObject:
enabled: false
detailed:
enabled: false
extraServiceMonitors: []
replicaCount: 1
persistence:
storageClass: local-path
size: 8Gi
resources:
requests:
memory: 512Mi
# Логин/пароль приезжают из Vault через vault-agent-injector: секрет кладёт
# ansible (роль sarex_stack, vault_app_secrets), роль rabbitmq в
# auth/kubernetes создаётся там же.
auth:
securePassword: true
# Без этого администратор пускается только с localhost, и любое
# подключение из кластера отбивается на аутентификации:
# PLAIN login refused: user 'rabbit' can only connect via localhost
# Диагностика сбивает с толку: клиент видит 403 ACCESS_REFUSED, будто
# не сошёлся пароль, хотя пароль верный — rabbitmqctl authenticate_user
# с тем же значением проходит. Так у celery не поднимался worker, и по
# той же причине в management UI нельзя было войти снаружи.
enableLoopbackUser: false
existingPasswordSecret: ""
vault:
enabled: true
role: rabbitmq
authPath: auth/kubernetes
secretPath: secrets/data/rabbitmq/auth
usernameKey: username
passwordKey: password
# Дублирует enableLoopbackUser: false намеренно, тем же способом, что и
# d8-ugmk-prod. Одного флага мало: он лишь заменяет в rabbitmq.conf строку
# loopback_users.<user> на "= false", а брокер её не применяет — в рантайме
# rabbitmqctl eval "application:get_env(rabbit, loopback_users)." всё равно
# отдавал {ok,[<<"rabbit">>]}. advanced.config разбирается позже и задаёт
# значение явным списком, поэтому именно он и решает.
advancedConfiguration: |-
[
{rabbit, [
{loopback_users, []}
]}
].

View File

@ -0,0 +1,5 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base

View File

@ -0,0 +1,49 @@
# Vault Agent Injector контура aero.
#
# Тот же чарт vault-contour, что и в остальных кластерах (это официальный
# hashicorp/vault-helm, перепубликованный в cr.yandex), но в режиме «внешний
# Vault»: сервер Vault здесь живёт в docker-compose, а в кластер ставится
# ТОЛЬКО injector.
#
# global.externalVaultAddr сам по себе отключает деплой vault-сервера — это
# документированное поведение чарта, отдельный server.enabled=false не нужен.
# Адрес указывает на DNS-мост (Service vault в namespace vault), который
# заводит ansible: k3s/manifests/vault/bridge-vault.yaml.
#
# Имя релиза — vault-injector, а не vault (как в прод-кластерах): чарт
# составляет имена ресурсов из имени релиза, и релиз `vault` рисковал бы
# перекрыть Service `vault` моста, оставив Vault недоступным.
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: vault-injector
namespace: vault
spec:
interval: 2m
chart:
spec:
chart: vault-contour
version: "0.2.3"
sourceRef:
kind: HelmRepository
name: yc-oci-charts
namespace: flux-system
interval: 10m
install:
createNamespace: true
remediation:
retries: 3
upgrade:
remediation:
retries: 3
values:
global:
# Отключает vault-сервер и задаёт адрес внешнего Vault для агентов.
externalVaultAddr: http://vault.vault.svc.cluster.local:8200
# Образы чарта лежат в приватном cr.yandex; секрет создаёт роль
# sarex_stack (registry_secrets), см. registry-secret.yaml.j2.
imagePullSecrets:
- name: regcred
tlsDisable: true
injector:
enabled: true

View File

@ -0,0 +1,11 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helmrelease.yaml
# namespace vault здесь намеренно НЕ объявлен: его создаёт ansible вместе с
# DNS-мостом и token reviewer'ом (k3s/manifests/vault/), потому что Service
# моста нужно куда-то положить ещё до того, как Flux дойдёт до этого слоя.
# Дублировать объект в двух источниках правды — значит спорить за владение,
# поэтому чарту достаточно install.createNamespace на случай чистого кластера.