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:
parent
8e14cd1e0b
commit
ee1881e516
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
85
apps/django/aero/kustomization.yaml
Normal file
85
apps/django/aero/kustomization.yaml
Normal 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"
|
||||
140
apps/django/aero/nginx-configmap.yaml
Normal file
140
apps/django/aero/nginx-configmap.yaml
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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/<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.
|
||||
rabbitmq:
|
||||
namespace: gateway
|
||||
|
||||
Loading…
Reference in New Issue
Block a user