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

236 lines
10 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]
- name: Создать/обновить Django-суперпользователя в backend
when: sarex_create_superuser | default(false) | bool
ansible.builtin.command:
argv:
- "{{ compose_cmd.split()[0] }}"
- exec
- -e
- "SU_NAME={{ superuser_name }}"
- -e
- "SU_PASS={{ superuser_password }}"
- -e
- "SU_EMAIL={{ superuser_name }}@{{ platform_domain }}"
- sarex-backend
- 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')
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]