iac/aero/roles/sarex_stack/tasks/main.yml
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

220 lines
11 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
# --- Предпосылки на хосте -------------------------------------------------
# Загружаем ДО подъёма k3s: istio-init внутри подов правит iptables через ядро
# хоста, и без этих модулей поды в namespace с istio-injection не стартуют.
# persistent: present закрепляет загрузку в /etc/modules-load.d, иначе после
# перезагрузки хоста контур снова развалился бы.
- name: Загрузить модули ядра, нужные istio-init
community.general.modprobe:
name: "{{ item }}"
state: present
persistent: present
loop: "{{ aero_kernel_modules }}"
tags: [kernel]
# --- Каталоги и файлы ----------------------------------------------------
- name: Создать каталог развёртывания
ansible.builtin.file:
path: "{{ deploy_dir }}"
state: directory
owner: root
group: root
mode: "0750"
tags: [env]
- name: Скопировать docker-compose.yaml
ansible.builtin.copy:
src: "{{ deploy_src_root }}/docker-compose.yaml"
dest: "{{ deploy_dir }}/docker-compose.yaml"
mode: "0640"
# nginx монтирует ./nginx/templates и ./nginx/certs — без шаблонов nginx не
# стартует, поэтому копируем конфигурацию рядом с compose.
# Изменение шаблона → handler пересоздаёт nginx (envsubst рендерит на старте).
- name: Скопировать шаблоны nginx
ansible.builtin.copy:
src: "{{ deploy_src_root }}/nginx/templates/"
dest: "{{ deploy_dir }}/nginx/templates/"
mode: "0644"
notify: Recreate nginx
# backend монтирует ./backend/uwsgi.ini (http-socket + 4 воркера) и
# ./backend/production.py (классическая JWT-аутентификация).
# Изменение конфигов → handler пересоздаёт backend и celery.
- name: Скопировать конфиги backend (uwsgi.ini, production.py)
ansible.builtin.copy:
src: "{{ deploy_src_root }}/backend/"
dest: "{{ deploy_dir }}/backend/"
mode: "0644"
notify: Recreate backend and celery
# k3s-манифесты DNS-моста (namespace processing + Service/Endpoints postgres/minio).
# processing-k8s-init монтирует ./k3s/manifests/processing и применяет их kubectl'ом.
- name: Скопировать k3s-манифесты (DNS-мост processing)
ansible.builtin.copy:
src: "{{ deploy_src_root }}/k3s/manifests/"
dest: "{{ deploy_dir }}/k3s/manifests/"
mode: "0644"
# engine монтирует ./engine/yc-s3-service-account.json (доступ Job-подов к S3).
- name: Скопировать конфиги engine (S3 service-account)
ansible.builtin.copy:
src: "{{ deploy_src_root }}/engine/"
dest: "{{ deploy_dir }}/engine/"
mode: "0644"
# vault монтирует ./vault/config как :ro — без конфига сервер не стартует.
- name: Скопировать конфигурацию Vault
ansible.builtin.copy:
src: "{{ deploy_src_root }}/vault/config/"
dest: "{{ deploy_dir }}/vault/config/"
mode: "0644"
# Каталоги PVC-данных k3s — по одному на ноду. Создаём заранее: иначе docker
# создаст их от root с umask демона, а local-path пишет туда от имени подов.
- name: Создать каталоги PVC-хранилища k3s (по ноде)
ansible.builtin.file:
path: "{{ deploy_dir }}/k3s-storage/{{ item }}"
state: directory
mode: "0755"
loop: "{{ aero_k3s_nodes }}"
# --- Секреты: генерация с персистом на control-node (идемпотентно) --------
- name: Сгенерировать/загрузить пароли (persist в secrets_store, gitignored)
ansible.builtin.set_fact:
sarex_secrets: >-
{{ sarex_secrets | default({}) | combine({
item: lookup('password',
secrets_store ~ '/' ~ inventory_hostname ~ '/' ~ item
~ ' length=' ~ secret_length ~ ' chars=ascii_letters,digits')
}) }}
loop: "{{ sarex_secret_vars }}"
no_log: true
tags: [env]
# Учётка администратора платформы. Живёт отдельно от общего цикла выше по двум
# причинам: у логина свой алфавит (Django не примет username со спецсимволами)
# и оба значения не должны попасть в .env — compose-стек их не использует,
# потребитель один, и это Vault.
#
# Генерируется всегда, а не по флагу sarex_create_superuser: значения уезжают
# в secrets/apps/django/superuser (см. vault_app_secrets), откуда их забирает
# стартовый скрипт пода backend. Без них vault-agent не отрендерит шаблон и
# под встанет в Init:Error. lookup('password') идемпотентен — на повторных
# прогонах возвращает уже сохранённое значение, а не генерирует новое.
- name: Сгенерировать/загрузить логин администратора платформы
ansible.builtin.set_fact:
superuser_name: "{{ lookup('password', secrets_store ~ '/' ~ inventory_hostname ~ '/SAREX_SUPERUSER_NAME chars=ascii_lowercase,digits length=10') }}"
no_log: true
tags: [env, superuser]
- name: Сгенерировать/загрузить пароль администратора платформы
ansible.builtin.set_fact:
superuser_password: "{{ lookup('password', secrets_store ~ '/' ~ inventory_hostname ~ '/SAREX_SUPERUSER_PASSWORD chars=ascii_letters,digits length=' ~ secret_length) }}"
no_log: true
tags: [env, superuser]
# RSA-ключи JWT (RS512): генерируем на control-node, persist в secrets_store.
- name: Сгенерировать приватный RSA-ключ JWT
ansible.builtin.command: "openssl genrsa -out {{ jwt_private_key_path }} 4096"
args:
creates: "{{ jwt_private_key_path }}"
delegate_to: localhost
become: false
tags: [env]
- name: Извлечь публичный RSA-ключ JWT
ansible.builtin.command: "openssl rsa -in {{ jwt_private_key_path }} -pubout -out {{ jwt_public_key_path }}"
args:
creates: "{{ jwt_public_key_path }}"
delegate_to: localhost
become: false
tags: [env]
- name: Собрать словарь переопределений для .env (пароли + домены + JWT)
ansible.builtin.set_fact:
env_overrides: >-
{{ sarex_secrets | combine({
'PLATFORM_DOMAIN': platform_domain,
'ADMIN_DOMAIN': admin_domain,
'MINIO_DOMAIN': minio_domain,
'SAREX_MINIO_CONSOLE_URL': 'https://' ~ minio_domain,
'GITEA_ADMIN_USER': gitea_admin_user,
'GITEA_ADMIN_EMAIL': gitea_admin_email,
'GITEA_ORG': gitea_org,
'GITEA_REPO': gitea_repo,
'AERO_GIT_BRANCH': aero_git_branch,
'AERO_FLUX_PATH': aero_flux_path,
'SAREX_JWT_PRIVATE_KEY': "'" ~ (lookup('file', jwt_private_key_path) | replace('\n', '\\n')) ~ "'",
'SAREX_JWT_PUBLIC_KEY': "'" ~ (lookup('file', jwt_public_key_path) | replace('\n', '\\n')) ~ "'",
}) }}
no_log: true
tags: [env]
- name: Отрендерить .env из .env.example с переопределениями
ansible.builtin.template:
src: env.j2
dest: "{{ deploy_dir }}/.env"
owner: root
group: root
mode: "0600"
no_log: true
tags: [env]
# --- Самоподписанный TLS-сертификат для трёх доменов ----------------------
- name: Создать каталог для сертификатов
ansible.builtin.file:
path: "{{ cert_dir }}"
state: directory
mode: "0750"
- name: Сгенерировать самоподписанный сертификат (SAN на все домены)
ansible.builtin.shell:
# umask 077 → приватный ключ создаётся сразу с правами 0600
cmd: >-
umask 077 && openssl req -x509 -nodes -newkey rsa:2048 -days {{ cert_days }}
-keyout {{ cert_dir }}/selfsigned.key
-out {{ cert_dir }}/selfsigned.crt
-subj "/CN={{ platform_domain }}"
-addext "subjectAltName=DNS:{{ platform_domain }},DNS:{{ admin_domain }},DNS:{{ minio_domain }}"
creates: "{{ cert_dir }}/selfsigned.crt"
# --- Доступ к приватному реестру (docker на хосте + registries.yaml нод) ---
# Обязательно ДО подъёма k3s: registries.yaml монтируется в ноды, и k3s читает
# его только при старте.
- name: Настроить доступ к приватному реестру
ansible.builtin.include_tasks: registry.yml
tags: [login, registry]
# --- GitOps-подложка: k3s + gitea + vault + Flux --------------------------
# Отдельный тег и отдельный флаг: подложка поднимается публичными образами и
# не зависит от docker login, поэтому её можно накатывать независимо от стека.
- name: Поднять GitOps-подложку контура
ansible.builtin.include_tasks: platform.yml
when: aero_platform_up | bool
tags: [platform]
# --- Поднять сервисы приложения ------------------------------------------
- name: Поднять сервисы приложения (docker compose up -d)
ansible.builtin.command:
cmd: "{{ compose_cmd }} up -d {{ sarex_services | join(' ') }}"
chdir: "{{ deploy_dir }}"
when: sarex_compose_up | bool
changed_when: true
tags: [up]
# --- Django-суперпользователь ----------------------------------------------
# Самого создания здесь больше нет — его делает под backend при каждом старте
# (apps/django/aero/kustomization.yaml, args стартового скрипта). Так учётка
# появляется вместе с приложением, а не отдельной командой оператора, и
# переживает пересоздание пода без ручного вмешательства.
#
# Ansible остаётся владельцем самих значений: генерирует их выше (тег env) и
# кладёт в Vault. Здесь — только показать логин по явному запросу
# (uv run poe superuser передаёт sarex_create_superuser=true).
- name: Показать учётные данные администратора платформы
ansible.builtin.debug:
msg: "Django superuser → login: {{ superuser_name }} password: {{ superuser_password }}"
when: sarex_create_superuser | default(false) | bool
tags: [superuser]