From ee1881e5160550c15930061ee5242ace14462a69 Mon Sep 17 00:00:00 2001 From: emelinda Date: Sat, 8 Aug 2026 16:49:16 +0300 Subject: [PATCH] =?UTF-8?q?feat(aero):=20django=20=D0=B2=20k3s=20=E2=80=94?= =?UTF-8?q?=20backend,=20frontend,=20s3-proxy=20=D0=B8=20=D0=B2=D1=8B?= =?UTF-8?q?=D1=85=D0=BE=D0=B4=20=D0=BD=D0=B0=D1=80=D1=83=D0=B6=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Первый прикладной сервис переехал из 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. --- aero/roles/sarex_stack/defaults/main.yml | 51 +++++++ aero/roles/sarex_stack/tasks/main.yml | 45 +++++- apps/django/aero/kustomization.yaml | 85 +++++++++++ apps/django/aero/nginx-configmap.yaml | 140 ++++++++++++++++++ clusters/aero/apps/kustomization.yaml | 16 +- .../istio-config/aero/istio-config.yaml | 30 ++++ 6 files changed, 351 insertions(+), 16 deletions(-) create mode 100644 apps/django/aero/kustomization.yaml create mode 100644 apps/django/aero/nginx-configmap.yaml diff --git a/aero/roles/sarex_stack/defaults/main.yml b/aero/roles/sarex_stack/defaults/main.yml index 9c5f6bf..e86c6c4 100644 --- a/aero/roles/sarex_stack/defaults/main.yml +++ b/aero/roles/sarex_stack/defaults/main.yml @@ -83,6 +83,8 @@ registry_secrets: - {name: regcred, namespace: postgresql} - {name: regcred, namespace: rabbitmq} - {name: regcred, namespace: minio} + # Образы backend и frontend лежат в приватном cr.yandex. + - {name: regcred, namespace: django} # Поднимать стек (docker compose up -d). Требует запущенной службы docker и # docker login в cr.yandex — по умолчанию выключено. @@ -125,6 +127,8 @@ gitea_sync_paths: - infrastructure/postgresql - infrastructure/rabbitmq - infrastructure/minio + # Прикладной слой: django (backend + frontend). + - apps/django # Рабочая копия репозитория gitea на хосте (клон, живёт между прогонами). gitea_sync_workdir: "{{ deploy_dir }}/gitea-sync" @@ -231,6 +235,40 @@ vault_app_secrets: processing: "{{ sarex_secrets.SAREX_PROCESSING_DB_PASSWORD }}" bim: "{{ sarex_secrets.SAREX_BIM_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 data: username: "{{ sarex_rabbitmq_k8s_user }}" @@ -246,6 +284,15 @@ vault_app_secrets: sarex_rabbitmq_k8s_user: rabbit 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 тянет # зависимости и порядок). k3s/gitea намеренно не поднимаем. sarex_services: @@ -267,3 +314,7 @@ sarex_services: - processing-frontend - engine - nginx + +# Namespace прикладного слоя django в k3s. Используется задачей создания +# суперпользователя (tasks/main.yml) — backend переехал из compose в кластер. +django_namespace: django diff --git a/aero/roles/sarex_stack/tasks/main.yml b/aero/roles/sarex_stack/tasks/main.yml index 6fc9c8c..8d39f89 100644 --- a/aero/roles/sarex_stack/tasks/main.yml +++ b/aero/roles/sarex_stack/tasks/main.yml @@ -199,19 +199,53 @@ no_log: true 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 ansible.builtin.command: argv: - "{{ compose_cmd.split()[0] }}" + - compose - exec - - -e + - -T + - k3s-server + - kubectl + - -n + - "{{ django_namespace }}" + - exec + - "{{ backend_pod.stdout | trim }}" + - -c + - backend + - -- + - env - "SU_NAME={{ superuser_name }}" - - -e - "SU_PASS={{ superuser_password }}" - - -e - "SU_EMAIL={{ superuser_name }}@{{ platform_domain }}" - - sarex-backend - python - manage.py - shell @@ -223,6 +257,7 @@ 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 diff --git a/apps/django/aero/kustomization.yaml b/apps/django/aero/kustomization.yaml new file mode 100644 index 0000000..e10750d --- /dev/null +++ b/apps/django/aero/kustomization.yaml @@ -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" diff --git a/apps/django/aero/nginx-configmap.yaml b/apps/django/aero/nginx-configmap.yaml new file mode 100644 index 0000000..9a679bc --- /dev/null +++ b/apps/django/aero/nginx-configmap.yaml @@ -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; + } + } + } diff --git a/clusters/aero/apps/kustomization.yaml b/clusters/aero/apps/kustomization.yaml index 05f7d35..7b24061 100644 --- a/clusters/aero/apps/kustomization.yaml +++ b/clusters/aero/apps/kustomization.yaml @@ -1,15 +1,9 @@ # Слой 3: прикладные сервисы контура. # -# Сейчас пуст. Тестовое приложение (apps/hello) удалено за ненадобностью: оно -# существовало, чтобы доказать работоспособность подложки — доставку Flux'ом, -# внедрение sidecar istio и получение секрета из Vault. Теперь это же -# подтверждают боевые сервисы: rabbitmq и minio поднимаются с кредами, -# приезжающими из Vault через vault-agent-injector. -# -# Слой намеренно оставлен пустым, а не удалён: следующий этап — перенос -# прикладных сервисов из compose в k3s, и они приедут именно сюда. Пустой -# resources корректен для kustomize; Flux при этом удалит из кластера то, что -# раньше применял этот слой (prune: true). +# Первым переезжает django — backend и frontend. Остальные его части +# (celery, srx-admin, s3-proxy) выключены в apps/django/aero и подключатся +# следующим шагом. apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -resources: [] +resources: + - ../../../apps/django/aero diff --git a/infrastructure/istio-config/aero/istio-config.yaml b/infrastructure/istio-config/aero/istio-config.yaml index b94e536..f435aa0 100644 --- a/infrastructure/istio-config/aero/istio-config.yaml +++ b/infrastructure/istio-config/aero/istio-config.yaml @@ -93,6 +93,36 @@ spec: prefix: / service: dashboard-kong-proxy.kubernetes-dashboard.svc.cluster.local 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/, поэтому маршрут должен + # идти ПЕРЕД правилом для /. + - 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. rabbitmq: namespace: gateway