Создание администратора платформы перенесено в под 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.
This commit is contained in:
emelinda 2026-08-08 18:10:19 +03:00
parent ee1881e516
commit bddcc955fe
4 changed files with 100 additions and 83 deletions

View File

@ -44,7 +44,8 @@ platform = "ansible-playbook deploy.yml -e aero_platform_up=true"
gen-env = "ansible-playbook deploy.yml --tags env"
# docker login в приватный реестр по ключу json_key
login = "ansible-playbook deploy.yml --tags login"
# Сгенерировать логин+пароль и создать Django-суперпользователя в backend
# Показать логин+пароль администратора платформы. Саму учётку создаёт под
# backend при старте (apps/django/aero), эта задача только печатает креды.
superuser = "ansible-playbook deploy.yml -e sarex_create_superuser=true --tags superuser"
lint = "ansible-lint"

View File

@ -263,6 +263,15 @@ vault_app_secrets:
secret_key: "{{ sarex_secrets.SAREX_MINIO_ROOT_PASSWORD }}"
buckets:
- name: "{{ sarex_django_s3_bucket }}"
# Учётка администратора платформы. Её разбирает не приложение, а стартовый
# скрипт пода backend: он экспортирует ключи как DJANGO_SUPERUSER_* — ровно
# те имена, которые ждёт штатная manage.py createsuperuser --noinput.
# См. apps/django/aero/kustomization.yaml.
- path: secrets/apps/django/superuser
data:
username: "{{ superuser_name }}"
password: "{{ superuser_password }}"
email: "{{ superuser_name }}@{{ platform_domain }}"
# JWT-ключи RS512. Те же самые, что роль генерирует для compose-стека, —
# контур переезжает, и токены должны остаться совместимыми.
- path: secrets/vault/common/rsa_keys

View File

@ -92,6 +92,28 @@
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"
@ -181,89 +203,16 @@
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.
# --- Django-суперпользователь ----------------------------------------------
# Самого создания здесь больше нет — его делает под backend при каждом старте
# (apps/django/aero/kustomization.yaml, args стартового скрипта). Так учётка
# появляется вместе с приложением, а не отдельной командой оператора, и
# переживает пересоздание пода без ручного вмешательства.
#
# Пароль передаётся переменной окружения (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 остаётся владельцем самих значений: генерирует их выше (тег 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

View File

@ -83,3 +83,61 @@ patches:
- op: replace
path: /spec/values/services/backend/envs/2/value/_default
value: "False"
# --- Администратор платформы --------------------------------------------
# Учётка приезжает из 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