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.
This commit is contained in:
emelinda 2026-08-08 16:49:16 +03:00
parent 8e14cd1e0b
commit ee1881e516
6 changed files with 351 additions and 16 deletions

View File

@ -83,6 +83,8 @@ registry_secrets:
- {name: regcred, namespace: postgresql} - {name: regcred, namespace: postgresql}
- {name: regcred, namespace: rabbitmq} - {name: regcred, namespace: rabbitmq}
- {name: regcred, namespace: minio} - {name: regcred, namespace: minio}
# Образы backend и frontend лежат в приватном cr.yandex.
- {name: regcred, namespace: django}
# Поднимать стек (docker compose up -d). Требует запущенной службы docker и # Поднимать стек (docker compose up -d). Требует запущенной службы docker и
# docker login в cr.yandex — по умолчанию выключено. # docker login в cr.yandex — по умолчанию выключено.
@ -125,6 +127,8 @@ gitea_sync_paths:
- infrastructure/postgresql - infrastructure/postgresql
- infrastructure/rabbitmq - infrastructure/rabbitmq
- infrastructure/minio - infrastructure/minio
# Прикладной слой: django (backend + frontend).
- apps/django
# Рабочая копия репозитория gitea на хосте (клон, живёт между прогонами). # Рабочая копия репозитория gitea на хосте (клон, живёт между прогонами).
gitea_sync_workdir: "{{ deploy_dir }}/gitea-sync" gitea_sync_workdir: "{{ deploy_dir }}/gitea-sync"
@ -231,6 +235,40 @@ vault_app_secrets:
processing: "{{ sarex_secrets.SAREX_PROCESSING_DB_PASSWORD }}" processing: "{{ sarex_secrets.SAREX_PROCESSING_DB_PASSWORD }}"
bim: "{{ sarex_secrets.SAREX_BIM_DB_PASSWORD }}" bim: "{{ sarex_secrets.SAREX_BIM_DB_PASSWORD }}"
workspace: "{{ sarex_secrets.SAREX_WORKSPACE_DB_PASSWORD }}" workspace: "{{ sarex_secrets.SAREX_WORKSPACE_DB_PASSWORD }}"
# --- Секреты приложения django ---------------------------------------------
# Пути и имена ключей заданы vault-шаблонами в apps/django/base/backend.yaml —
# менять их в отрыве от манифестов нельзя. Отсутствие любого из них означает
# не «пустая переменная», а падение vault-agent-init и под в Init:Error.
- path: secrets/apps/django/postgres
data:
host: postgresql.postgresql.svc.cluster.local
port: "5432"
database: "{{ sarex_postgres_django_db }}"
username: "{{ sarex_postgres_django_user }}"
password: "{{ sarex_secrets.SAREX_DJANGO_DB_PASSWORD }}"
# В k3s-инстансе RabbitMQ отдельного пользователя под django нет, поэтому
# используются админские креды и vhost по умолчанию. Это осознанный
# временный компромисс: завести отдельного пользователя нечем — чарт такой
# возможности не даёт, а руками это выпадет из GitOps.
- path: secrets/rabbitmq/apps/django
data:
username: "{{ sarex_rabbitmq_k8s_user }}"
password: "{{ sarex_secrets.SAREX_RABBITMQ_PASSWORD }}"
vhost: "/"
# Аналогично MinIO: пользователь приложения не заводится, берём root.
# buckets — список объектов: шаблон читает index (index $buckets 0) "name".
- path: secrets/minio/apps/django
data:
access_key: "{{ sarex_minio_k8s_user }}"
secret_key: "{{ sarex_secrets.SAREX_MINIO_ROOT_PASSWORD }}"
buckets:
- name: "{{ sarex_django_s3_bucket }}"
# JWT-ключи RS512. Те же самые, что роль генерирует для compose-стека, —
# контур переезжает, и токены должны остаться совместимыми.
- path: secrets/vault/common/rsa_keys
data:
private_key: "{{ lookup('file', jwt_private_key_path) }}"
public_key: "{{ lookup('file', jwt_public_key_path) }}"
- path: secrets/rabbitmq/auth - path: secrets/rabbitmq/auth
data: data:
username: "{{ sarex_rabbitmq_k8s_user }}" username: "{{ sarex_rabbitmq_k8s_user }}"
@ -246,6 +284,15 @@ vault_app_secrets:
sarex_rabbitmq_k8s_user: rabbit sarex_rabbitmq_k8s_user: rabbit
sarex_minio_k8s_user: minioadmin sarex_minio_k8s_user: minioadmin
# Параметры подключения django к общей СУБД контура. Должны совпадать с
# соответствующей записью в contour.databases (infrastructure/postgresql/aero).
sarex_postgres_django_db: sarex_db
sarex_postgres_django_user: django
# Бакет медиафайлов django. ВНИМАНИЕ: чарт MinIO бакеты не создаёт — его нужно
# завести отдельно, иначе загрузка файлов будет падать.
sarex_django_s3_bucket: sarex-media-storage
# Сервисы приложения, поднимаемые при sarex_compose_up (depends_on тянет # Сервисы приложения, поднимаемые при sarex_compose_up (depends_on тянет
# зависимости и порядок). k3s/gitea намеренно не поднимаем. # зависимости и порядок). k3s/gitea намеренно не поднимаем.
sarex_services: sarex_services:
@ -267,3 +314,7 @@ sarex_services:
- processing-frontend - processing-frontend
- engine - engine
- nginx - nginx
# Namespace прикладного слоя django в k3s. Используется задачей создания
# суперпользователя (tasks/main.yml) — backend переехал из compose в кластер.
django_namespace: django

View File

@ -199,19 +199,53 @@
no_log: true no_log: true
tags: [superuser] tags: [superuser]
- name: Создать/обновить Django-суперпользователя в backend # 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 when: sarex_create_superuser | default(false) | bool
ansible.builtin.command: ansible.builtin.command:
argv: argv:
- "{{ compose_cmd.split()[0] }}" - "{{ compose_cmd.split()[0] }}"
- compose
- exec - exec
- -e - -T
- k3s-server
- kubectl
- -n
- "{{ django_namespace }}"
- exec
- "{{ backend_pod.stdout | trim }}"
- -c
- backend
- --
- env
- "SU_NAME={{ superuser_name }}" - "SU_NAME={{ superuser_name }}"
- -e
- "SU_PASS={{ superuser_password }}" - "SU_PASS={{ superuser_password }}"
- -e
- "SU_EMAIL={{ superuser_name }}@{{ platform_domain }}" - "SU_EMAIL={{ superuser_name }}@{{ platform_domain }}"
- sarex-backend
- python - python
- manage.py - manage.py
- shell - shell
@ -223,6 +257,7 @@
u, c = U.objects.get_or_create(username=os.environ['SU_NAME'], defaults={'email': os.environ['SU_EMAIL']}); 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(); u.is_staff = True; u.is_superuser = True; u.set_password(os.environ['SU_PASS']); u.save();
print('created' if c else 'updated') print('created' if c else 'updated')
chdir: "{{ deploy_dir }}"
register: superuser_result register: superuser_result
changed_when: true changed_when: true
no_log: true no_log: true

View File

@ -0,0 +1,85 @@
---
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
# --- Состав контура -------------------------------------------------------
# celery выключен: очереди подключим вместе с остальными фоновыми задачами.
# s3-proxy ВКЛЮЧЁН — он отдаёт медиафайлы браузеру с CORS и Range, а бакет
# уже создан, так что проверять его есть на чём.
- target: {kind: HelmRelease, name: celery}
patch: |
$patch: delete
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: celery
namespace: django
# --- 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"

View File

@ -0,0 +1,140 @@
# Копия 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, а когда сервисы приедут — заработают сами,
# без правки конфига и перезапуска.
#
# 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;
}
# 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 "";
rewrite /workspaces-v2/(.+) /$1 break;
set $up_workspaces frontend-svc.workspaces.svc.cluster.local;
proxy_pass http://$up_workspaces:80;
}
location ~^/workspaces-v2/(.+)\.wasm$ {
proxy_http_version 1.1;
proxy_set_header Connection "";
rewrite ^/workspaces-v2/(.+) /$1 break;
set $up_workspaces_wasm frontend-svc.workspaces.svc.cluster.local;
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 "";
rewrite /workflows/(.+) /$1 break;
set $up_processing frontend-svc.processing.svc.cluster.local;
proxy_pass http://$up_processing:80;
}
location /service-worker.js {
try_files /static/$uri @index;
}
location / {
try_files $uri @index;
}
}
}

View File

@ -1,15 +1,9 @@
# Слой 3: прикладные сервисы контура. # Слой 3: прикладные сервисы контура.
# #
# Сейчас пуст. Тестовое приложение (apps/hello) удалено за ненадобностью: оно # Первым переезжает django — backend и frontend. Остальные его части
# существовало, чтобы доказать работоспособность подложки — доставку Flux'ом, # (celery, srx-admin, s3-proxy) выключены в apps/django/aero и подключатся
# внедрение sidecar istio и получение секрета из Vault. Теперь это же # следующим шагом.
# подтверждают боевые сервисы: rabbitmq и minio поднимаются с кредами,
# приезжающими из Vault через vault-agent-injector.
#
# Слой намеренно оставлен пустым, а не удалён: следующий этап — перенос
# прикладных сервисов из compose в k3s, и они приедут именно сюда. Пустой
# resources корректен для kustomize; Flux при этом удалит из кластера то, что
# раньше применял этот слой (prune: true).
apiVersion: kustomize.config.k8s.io/v1beta1 apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization kind: Kustomization
resources: [] resources:
- ../../../apps/django/aero

View File

@ -93,6 +93,36 @@ spec:
prefix: / prefix: /
service: dashboard-kong-proxy.kubernetes-dashboard.svc.cluster.local service: dashboard-kong-proxy.kubernetes-dashboard.svc.cluster.local
port: 80 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>, поэтому маршрут должен
# идти ПЕРЕД правилом для /.
- path:
prefix: /media/
service: s3-proxy-svc.django.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
# Management UI RabbitMQ. # Management UI RabbitMQ.
rabbitmq: rabbitmq:
namespace: gateway namespace: gateway