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

271 lines
12 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]
# 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-суперпользователь: генерация логина+пароля, создание -----------
# Только по явной команде (uv run poe superuser передаёт sarex_create_superuser=true),
# чтобы обычный deploy не дёргал backend. Логин/пароль персистятся в secrets_store
# (переиспользуются при повторе — идемпотентно).
- name: Сгенерировать логин суперпользователя
ansible.builtin.set_fact:
superuser_name: "{{ lookup('password', secrets_store ~ '/' ~ inventory_hostname ~ '/SAREX_SUPERUSER_NAME chars=ascii_lowercase,digits length=10') }}"
when: sarex_create_superuser | default(false) | bool
no_log: true
tags: [superuser]
- name: Сгенерировать пароль суперпользователя
ansible.builtin.set_fact:
superuser_password: "{{ lookup('password', secrets_store ~ '/' ~ inventory_hostname ~ '/SAREX_SUPERUSER_PASSWORD chars=ascii_letters,digits length=' ~ secret_length) }}"
when: sarex_create_superuser | default(false) | bool
no_log: true
tags: [superuser]
# Backend теперь живёт в k3s, а не в compose, поэтому идём через kubectl exec
# внутри контейнера k3s-server, а не через docker compose exec.
#
# Пароль передаётся переменной окружения (env в kubectl exec), а не аргументом
# командной строки: аргументы видны в ps любому, кто имеет доступ к ноде.
# Метка именно app=backend: universal-chart не проставляет
# app.kubernetes.io/instance, и селектор по нему возвращает пустой список.
- name: Найти под backend в k3s
when: sarex_create_superuser | default(false) | bool
ansible.builtin.command:
cmd: >-
{{ compose_cmd }} exec -T k3s-server
kubectl -n {{ django_namespace }} get pods -l app=backend
--field-selector=status.phase=Running
-o jsonpath={.items[0].metadata.name}
chdir: "{{ deploy_dir }}"
register: backend_pod
changed_when: false
tags: [superuser]
# argv, а не shell: аргументы передаются процессу напрямую, без разбора
# шеллом. Иначе python-однострочник попадает внутрь двойных кавычек внутри
# sh -ec '...' и разваливается на вложенном экранировании.
#
# Пароль идёт переменной окружения, а не аргументом: аргументы видны в ps
# любому, у кого есть доступ к ноде.
- name: Создать/обновить Django-суперпользователя в backend (k3s)
when: sarex_create_superuser | default(false) | bool
ansible.builtin.command:
argv:
- "{{ compose_cmd.split()[0] }}"
- compose
- exec
- -T
- k3s-server
- kubectl
- -n
- "{{ django_namespace }}"
- exec
- "{{ backend_pod.stdout | trim }}"
- -c
- backend
- --
- env
- "SU_NAME={{ superuser_name }}"
- "SU_PASS={{ superuser_password }}"
- "SU_EMAIL={{ superuser_name }}@{{ platform_domain }}"
- python
- manage.py
- shell
- -c
- >-
import os;
from django.contrib.auth import get_user_model;
U = get_user_model();
u, c = U.objects.get_or_create(username=os.environ['SU_NAME'], defaults={'email': os.environ['SU_EMAIL']});
u.is_staff = True; u.is_superuser = True; u.set_password(os.environ['SU_PASS']); u.save();
print('created' if c else 'updated')
chdir: "{{ deploy_dir }}"
register: superuser_result
changed_when: true
no_log: true
tags: [superuser]
- name: Показать учётные данные суперпользователя
ansible.builtin.debug:
msg: "Django superuser → login: {{ superuser_name }} password: {{ superuser_password }}"
when: sarex_create_superuser | default(false) | bool
tags: [superuser]