Compare commits

...

2 Commits

7 changed files with 231 additions and 29 deletions

View File

@ -15,6 +15,10 @@ SAREX_DJANGO_DB_USER=django
SAREX_DJANGO_DB_PASSWORD=django-secret SAREX_DJANGO_DB_PASSWORD=django-secret
# Расширения PostgreSQL для базы django (создаёт postgres-init суперпользователем) # Расширения PostgreSQL для базы django (создаёт postgres-init суперпользователем)
SAREX_DJANGO_DB_EXTENSIONS=ltree SAREX_DJANGO_DB_EXTENSIONS=ltree
# JWT-ключи backend (RS512): PEM в одну строку с литеральными \n (backend разворачивает).
# Генерируются автоматически в .secrets ролью sarex_stack; здесь — плейсхолдеры.
SAREX_JWT_PRIVATE_KEY=changeme
SAREX_JWT_PUBLIC_KEY=changeme
SAREX_PROCESSING_DB_USER=processing SAREX_PROCESSING_DB_USER=processing
SAREX_PROCESSING_DB_PASSWORD=processing-secret SAREX_PROCESSING_DB_PASSWORD=processing-secret
SAREX_BIM_DB_USER=bim SAREX_BIM_DB_USER=bim

View File

@ -41,16 +41,37 @@ chmod 600 ~/.ssh/id_ed25519
## Запуск ## Запуск
Из WSL, в каталоге проекта (`/mnt/c/.../iac/aero`): Из WSL, в каталоге проекта (`/mnt/c/.../iac/aero`).
### Установка с чистого листа — одной командой
```bash ```bash
uv sync # окружение (uv сам поставит нужный Python) uv run poe install
uv run poe ping # проверка SSH + sudo
uv run poe check # dry-run (--check --diff), ничего не меняет
uv run poe play # боевой прогон роли
``` ```
Одной командой всё сразу: `uv run poe up` (`sync` → `galaxy``play`). Последовательность: `sync``galaxy`**`provision`** (базовые пакеты + docker
и запуск службы) → **`stack`** (деплой конфигурации + `docker compose up -d`
сервисов приложения) → **`superuser`** (генерация и создание Django-админа).
Перед запуском впишите хосты в [inventory.ini](inventory.ini) и убедитесь, что
на control-node есть SSH-ключ и `~/.ssh/local/authorized_key.json` для входа в
реестр.
### Пошагово / отдельные команды
| Команда | Действие |
| --- | --- |
| `uv run poe ping` | проверка SSH + sudo |
| `uv run poe provision` | базовые пакеты + docker/containerd + запуск службы |
| `uv run poe deploy` | деплой конфигурации: файлы, `.env`, сертификаты, `docker login` |
| `uv run poe stack` | деплой + поднять стек (`docker compose up -d`) |
| `uv run poe gen-env` | только сгенерировать пароли и `.env` |
| `uv run poe login` | только `docker login` в реестр по ключу |
| `uv run poe superuser` | сгенерировать логин+пароль и создать Django-суперпользователя |
| `uv run poe check` / `deploy-check` | dry-run (`--check --diff`) |
Поднимаемые сервисы задаются в `sarex_services` (роль `sarex_stack`) —
`postgres(+init)`, `redis`, `rabbitmq`, `minio(+init)`, `measurements`,
`backend`, `celery`, `frontend`, `nginx` (k3s/gitea/processing не поднимаются).
> `ANSIBLE_CONFIG` и `UV_LINK_MODE` заданы в `[tool.poe.env]` — на диске `C:` > `ANSIBLE_CONFIG` и `UV_LINK_MODE` заданы в `[tool.poe.env]` — на диске `C:`
> (через `/mnt/c`) права `0777`, и ansible игнорирует `ansible.cfg` в > (через `/mnt/c`) права `0777`, и ansible игнорирует `ansible.cfg` в
@ -58,10 +79,12 @@ uv run poe play # боевой прогон роли
### Развёртывание sarex-стека (роль `sarex_stack`) ### Развёртывание sarex-стека (роль `sarex_stack`)
Копирует `docker-compose.yaml`, `nginx/` и `postgres/initdb` в `/root/sarex`, Копирует `docker-compose.yaml`, `nginx/templates` и `backend/uwsgi.ini` в
рендерит `.env` из `.env.example` со **сгенерированными паролями** и доменами `/root/sarex`, рендерит `.env` из `.env.example` со **сгенерированными паролями**
`*.sarex.local.lonsdaleites.ru`, генерирует самоподписанный TLS-сертификат на и доменами `*.sarex.local.lonsdaleites.ru`, генерирует самоподписанный
все три домена (`nginx/certs/selfsigned.{crt,key}`). TLS-сертификат на все три домена (`nginx/certs/selfsigned.{crt,key}`), логинится
в реестр и (при `sarex_compose_up=true`) поднимает сервисы приложения.
Роли/базы и MinIO-бакеты создаются одноразовыми `postgres-init`/`minio-init`.
```bash ```bash
uv run ansible-playbook deploy.yml --check --diff # dry-run uv run ansible-playbook deploy.yml --check --diff # dry-run

View File

@ -28,22 +28,26 @@ UV_LINK_MODE = "copy"
sync = "uv sync" sync = "uv sync"
galaxy = "ansible-galaxy collection install -r requirements.yml" galaxy = "ansible-galaxy collection install -r requirements.yml"
ping = "ansible docker_hosts -m ping" ping = "ansible docker_hosts -m ping"
play = "ansible-playbook site.yml" # Провижининг хоста: базовые пакеты + docker/containerd + запуск службы
provision = "ansible-playbook site.yml"
check = "ansible-playbook site.yml --check --diff" check = "ansible-playbook site.yml --check --diff"
# Деплой конфигурации: файлы, .env с паролями, сертификаты, docker login
deploy = "ansible-playbook deploy.yml" deploy = "ansible-playbook deploy.yml"
deploy-check = "ansible-playbook deploy.yml --check --diff" deploy-check = "ansible-playbook deploy.yml --check --diff"
# Деплой + поднять стек приложения (docker compose up -d)
stack = "ansible-playbook deploy.yml -e sarex_compose_up=true"
# Только генерация паролей и рендер .env на хосте (без копирования/сертификатов) # Только генерация паролей и рендер .env на хосте (без копирования/сертификатов)
gen-env = "ansible-playbook deploy.yml --tags env" gen-env = "ansible-playbook deploy.yml --tags env"
# docker login в приватный реестр по ключу json_key # docker login в приватный реестр по ключу json_key
login = "ansible-playbook deploy.yml --tags login" login = "ansible-playbook deploy.yml --tags login"
# сгенерировать логин+пароль и создать Django-суперпользователя в backend # Сгенерировать логин+пароль и создать Django-суперпользователя в backend
superuser = "ansible-playbook deploy.yml --tags superuser" superuser = "ansible-playbook deploy.yml -e sarex_create_superuser=true --tags superuser"
lint = "ansible-lint" lint = "ansible-lint"
# Полная подготовка окружения: uv sync + установка коллекций # Подготовка окружения: uv sync + установка коллекций
[tool.poe.tasks.setup] [tool.poe.tasks.setup]
sequence = ["sync", "galaxy"] sequence = ["sync", "galaxy"]
# Всё сразу: подготовка окружения и прогон плейбука # Установка с чистого листа: окружение → docker → деплой+стек → суперпользователь
[tool.poe.tasks.up] [tool.poe.tasks.install]
sequence = ["sync", "galaxy", "play"] sequence = ["sync", "galaxy", "provision", "stack", "superuser"]

View File

@ -31,6 +31,10 @@ secret_length: 24
# Хранилище секретов на control-node (persist + переиспользование). Gitignored. # Хранилище секретов на control-node (persist + переиспользование). Gitignored.
secrets_store: "{{ playbook_dir }}/.secrets" secrets_store: "{{ playbook_dir }}/.secrets"
# RSA-ключи JWT backend (RS512). Генерируются на control-node, persist в secrets_store.
jwt_private_key_path: "{{ secrets_store }}/{{ inventory_hostname }}/jwt_private.pem"
jwt_public_key_path: "{{ secrets_store }}/{{ inventory_hostname }}/jwt_public.pem"
# Приватный реестр образов и ключ для входа (json_key) # Приватный реестр образов и ключ для входа (json_key)
registry_host: cr.yandex registry_host: cr.yandex
registry_username: json_key registry_username: json_key
@ -41,3 +45,18 @@ registry_key_src: /mnt/c/Users/user/.ssh/local/authorized_key.json
# docker login в cr.yandex — по умолчанию выключено. # docker login в cr.yandex — по умолчанию выключено.
sarex_compose_up: false sarex_compose_up: false
compose_cmd: "docker compose" compose_cmd: "docker compose"
# Сервисы приложения, поднимаемые при sarex_compose_up (depends_on тянет
# зависимости и порядок). k3s/gitea/processing намеренно не поднимаем.
sarex_services:
- postgres
- postgres-init
- redis
- rabbitmq
- minio
- minio-init
- measurements
- backend
- celery
- frontend
- nginx

View File

@ -23,11 +23,12 @@
dest: "{{ deploy_dir }}/nginx/templates/" dest: "{{ deploy_dir }}/nginx/templates/"
mode: "0644" mode: "0644"
# backend монтирует ./backend/uwsgi.ini (http-socket + 4 воркера) # backend монтирует ./backend/uwsgi.ini (http-socket + 4 воркера) и
- name: Скопировать конфиг uwsgi для backend # ./backend/production.py (классическая JWT-аутентификация)
- name: Скопировать конфиги backend (uwsgi.ini, production.py)
ansible.builtin.copy: ansible.builtin.copy:
src: "{{ deploy_src_root }}/backend/uwsgi.ini" src: "{{ deploy_src_root }}/backend/"
dest: "{{ deploy_dir }}/backend/uwsgi.ini" dest: "{{ deploy_dir }}/backend/"
mode: "0644" mode: "0644"
# --- Секреты: генерация с персистом на control-node (идемпотентно) -------- # --- Секреты: генерация с персистом на control-node (идемпотентно) --------
@ -43,7 +44,24 @@
no_log: true no_log: true
tags: [env] tags: [env]
- name: Собрать словарь переопределений для .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: ansible.builtin.set_fact:
env_overrides: >- env_overrides: >-
{{ sarex_secrets | combine({ {{ sarex_secrets | combine({
@ -51,6 +69,8 @@
'ADMIN_DOMAIN': admin_domain, 'ADMIN_DOMAIN': admin_domain,
'MINIO_DOMAIN': minio_domain, 'MINIO_DOMAIN': minio_domain,
'SAREX_MINIO_CONSOLE_URL': 'https://' ~ minio_domain, 'SAREX_MINIO_CONSOLE_URL': 'https://' ~ minio_domain,
'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 no_log: true
tags: [env] tags: [env]
@ -118,13 +138,14 @@
state: absent state: absent
tags: [login] tags: [login]
# --- Опционально: поднять стек ------------------------------------------- # --- Поднять сервисы приложения ------------------------------------------
- name: Поднять стек (docker compose up -d) - name: Поднять сервисы приложения (docker compose up -d)
ansible.builtin.command: ansible.builtin.command:
cmd: "{{ compose_cmd }} up -d" cmd: "{{ compose_cmd }} up -d {{ sarex_services | join(' ') }}"
chdir: "{{ deploy_dir }}" chdir: "{{ deploy_dir }}"
when: sarex_compose_up | bool when: sarex_compose_up | bool
changed_when: true changed_when: true
tags: [up]
# --- Django-суперпользователь: генерация логина+пароля, создание ----------- # --- Django-суперпользователь: генерация логина+пароля, создание -----------
# Только по явной команде (uv run poe superuser передаёт sarex_create_superuser=true), # Только по явной команде (uv run poe superuser передаёт sarex_create_superuser=true),

125
backend/production.py Normal file
View File

@ -0,0 +1,125 @@
from .base import *
from logging.handlers import SysLogHandler
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS')
FILE_UPLOAD_PERMISSIONS = 0o644
DEBUG = False
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
SECURE_SSL_REDIRECT = False
# STATIC_ROOT = env('STATIC_ROOT')
# MEDIA_ROOT = env('MEDIA_ROOT')
# SECRET_KEY = env('SECRET_KEY')
SECRET_KEY = 't2=9+($2f%7ptsdy4!rby$)mcfl1l%o2e@vs^d(g&(wwi&%k1v'
# DATABASE CONFIGURATION START
# ------------------------------------------------------------------------------
POSTGRES_DATABASE = env('DJANGO_POSTGRES_DATABASE')
POSTGRES_USER = env('DJANGO_POSTGRES_USER')
POSTGRES_PASSWORD = env('DJANGO_POSTGRES_PASSWORD')
POSTGRES_HOST = env('DJANGO_POSTGRES_HOST')
POSTGRES_PORTS = env('DJANGO_POSTGRES_PORTS', default="5432")
DATABASES = {
'default': {
'ENGINE': 'django_prometheus.db.backends.postgresql',
'NAME': POSTGRES_DATABASE,
'USER': POSTGRES_USER,
'PASSWORD': POSTGRES_PASSWORD,
'HOST': POSTGRES_HOST,
'PORT': POSTGRES_PORTS,
}
}
# DATABASE CONFIGURATION END
# ------------------------------------------------------------------------------
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'filters': {
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse'
}
},
'formatters': {
'verbose': {
'format': '[contactor] %(levelname)s %(asctime)s %(message)s'
},
},
'handlers': {
# Send all messages to console
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
},
# critical errors are logged to sentry
'sentry': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'logging.StreamHandler',
},
},
'loggers': {
# This is the "catch all" logger
'': {
'handlers': ['console', 'sentry'],
'level': 'DEBUG',
'propagate': False,
},
}
}
COMPARATOR_SECTION = env("COMPARATOR_SECTION", default="sarex-production-storage")
SIMPLE_JWT = {
# 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
'ACCESS_TOKEN_LIFETIME': timedelta(hours=1),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': False,
'UPDATE_LAST_LOGIN': False,
'ALGORITHM': 'RS512',
'SIGNING_KEY': env.str("JWT_PRIVATE_KEY").replace("\\n", "\n"),
'VERIFYING_KEY': env.str("JWT_PUBLIC_KEY").replace("\\n", "\n"),
'AUDIENCE': None,
'ISSUER': SIMPLE_JWT_ISSUER,
'AUTH_HEADER_TYPES': ('Bearer',),
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
'TOKEN_TYPE_CLAIM': 'token_type',
'JTI_CLAIM': 'jti',
'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
# 'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
'SLIDING_TOKEN_LIFETIME': timedelta(hours=1),
'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
}
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
COMPARISON_API_URL = f"{WORKFLOWSSETTINGS.host}/comparisons"
DOCUMENTATION_API_URL = f"{WORKFLOWSSETTINGS.host}/documentations"
PDM_FILES_API_URL = f"{WORKFLOWSSETTINGS.host}/files"
# --- Override для self-hosted (auth_type=original) --------------------------
# В base.py классическая SimpleJWT-аутентификация закомментирована (образ собран
# под Zitadel), поэтому Bearer-токен от /api/token/ отвергается защищёнными
# эндпоинтами. Возвращаем её первым классом; Session — для админки.
REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] = [
"rest_framework_simplejwt.authentication.JWTAuthentication",
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.BasicAuthentication",
]

View File

@ -181,7 +181,9 @@ services:
image: ${SAREX_REDIS_IMAGE:-cr.yandex/crp3ccidau046kdj8g9q/redis:7.0.10} image: ${SAREX_REDIS_IMAGE:-cr.yandex/crp3ccidau046kdj8g9q/redis:7.0.10}
container_name: redis container_name: redis
restart: unless-stopped restart: unless-stopped
command: ["redis-server", "--save", "60", "1", "--loglevel", "warning"] # protected-mode no: redis только во внутренней docker-сети (порт наружу не
# публикуется), иначе бэкенд/celery из других контейнеров отбиваются "DENIED"
command: ["redis-server", "--save", "60", "1", "--loglevel", "warning", "--protected-mode", "no", "--stop-writes-on-bgsave-error", "no"]
volumes: volumes:
- sarex-redis-data:/data - sarex-redis-data:/data
healthcheck: healthcheck:
@ -273,8 +275,8 @@ services:
environment: &sarex-backend-env environment: &sarex-backend-env
ALLOWED_HOSTS: "*" ALLOWED_HOSTS: "*"
DJANGO_SETTINGS_MODULE: config.settings.production DJANGO_SETTINGS_MODULE: config.settings.production
JWT_PRIVATE_KEY: "JWT" JWT_PRIVATE_KEY: ${SAREX_JWT_PRIVATE_KEY:-JWT}
JWT_PUBLIC_KEY: "JPK" JWT_PUBLIC_KEY: ${SAREX_JWT_PUBLIC_KEY:-JPK}
DJANGO_POSTGRES_HOST: postgres DJANGO_POSTGRES_HOST: postgres
DJANGO_POSTGRES_DATABASE: ${SAREX_POSTGRES_DB:-sarex_db} DJANGO_POSTGRES_DATABASE: ${SAREX_POSTGRES_DB:-sarex_db}
DJANGO_POSTGRES_USER: ${SAREX_DJANGO_DB_USER:-django} DJANGO_POSTGRES_USER: ${SAREX_DJANGO_DB_USER:-django}
@ -300,6 +302,8 @@ services:
volumes: volumes:
# свой uwsgi.ini поверх образного: http-socket + 4 воркера (:z — SELinux/RedOS) # свой uwsgi.ini поверх образного: http-socket + 4 воркера (:z — SELinux/RedOS)
- ./backend/uwsgi.ini:/opt/sarex/uwsgi.ini:ro,z - ./backend/uwsgi.ini:/opt/sarex/uwsgi.ini:ro,z
# production.py с включённой классической JWT-аутентификацией
- ./backend/production.py:/opt/sarex/config/settings/production.py:ro,z
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
@ -319,6 +323,8 @@ services:
entrypoint: celery -A config worker -l info -E -Q default -n default_worker.%h --concurrency=1 -B entrypoint: celery -A config worker -l info -E -Q default -n default_worker.%h --concurrency=1 -B
environment: environment:
<<: *sarex-backend-env <<: *sarex-backend-env
volumes:
- ./backend/production.py:/opt/sarex/config/settings/production.py:ro,z
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
@ -362,7 +368,7 @@ services:
POSTGRES_SSL_USE: "0" POSTGRES_SSL_USE: "0"
ENABLE_SQL_QUERY: "0" ENABLE_SQL_QUERY: "0"
DJANGO_HOST: http://backend:8000 # django-backend в этом же compose DJANGO_HOST: http://backend:8000 # django-backend в этом же compose
PUBLIC_KEY: "JPK" # JWT public key (заглушка, как у backend) PUBLIC_KEY: ${SAREX_JWT_PUBLIC_KEY:-JPK} # JWT public key (общий с backend)
ports: ports:
- "8081:8080" - "8081:8080"
depends_on: depends_on: