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
# Расширения PostgreSQL для базы django (создаёт postgres-init суперпользователем)
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_PASSWORD=processing-secret
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
uv sync # окружение (uv сам поставит нужный Python)
uv run poe ping # проверка SSH + sudo
uv run poe check # dry-run (--check --diff), ничего не меняет
uv run poe play # боевой прогон роли
uv run poe install
```
Одной командой всё сразу: `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:`
> (через `/mnt/c`) права `0777`, и ansible игнорирует `ansible.cfg` в
@ -58,10 +79,12 @@ uv run poe play # боевой прогон роли
### Развёртывание sarex-стека (роль `sarex_stack`)
Копирует `docker-compose.yaml`, `nginx/` и `postgres/initdb` в `/root/sarex`,
рендерит `.env` из `.env.example` со **сгенерированными паролями** и доменами
`*.sarex.local.lonsdaleites.ru`, генерирует самоподписанный TLS-сертификат на
все три домена (`nginx/certs/selfsigned.{crt,key}`).
Копирует `docker-compose.yaml`, `nginx/templates` и `backend/uwsgi.ini` в
`/root/sarex`, рендерит `.env` из `.env.example` со **сгенерированными паролями**
и доменами `*.sarex.local.lonsdaleites.ru`, генерирует самоподписанный
TLS-сертификат на все три домена (`nginx/certs/selfsigned.{crt,key}`), логинится
в реестр и (при `sarex_compose_up=true`) поднимает сервисы приложения.
Роли/базы и MinIO-бакеты создаются одноразовыми `postgres-init`/`minio-init`.
```bash
uv run ansible-playbook deploy.yml --check --diff # dry-run

View File

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

View File

@ -31,6 +31,10 @@ secret_length: 24
# Хранилище секретов на control-node (persist + переиспользование). Gitignored.
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)
registry_host: cr.yandex
registry_username: json_key
@ -41,3 +45,18 @@ registry_key_src: /mnt/c/Users/user/.ssh/local/authorized_key.json
# docker login в cr.yandex — по умолчанию выключено.
sarex_compose_up: false
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/"
mode: "0644"
# backend монтирует ./backend/uwsgi.ini (http-socket + 4 воркера)
- name: Скопировать конфиг uwsgi для backend
# backend монтирует ./backend/uwsgi.ini (http-socket + 4 воркера) и
# ./backend/production.py (классическая JWT-аутентификация)
- name: Скопировать конфиги backend (uwsgi.ini, production.py)
ansible.builtin.copy:
src: "{{ deploy_src_root }}/backend/uwsgi.ini"
dest: "{{ deploy_dir }}/backend/uwsgi.ini"
src: "{{ deploy_src_root }}/backend/"
dest: "{{ deploy_dir }}/backend/"
mode: "0644"
# --- Секреты: генерация с персистом на control-node (идемпотентно) --------
@ -43,7 +44,24 @@
no_log: true
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:
env_overrides: >-
{{ sarex_secrets | combine({
@ -51,6 +69,8 @@
'ADMIN_DOMAIN': admin_domain,
'MINIO_DOMAIN': 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
tags: [env]
@ -118,13 +138,14 @@
state: absent
tags: [login]
# --- Опционально: поднять стек -------------------------------------------
- name: Поднять стек (docker compose up -d)
# --- Поднять сервисы приложения ------------------------------------------
- name: Поднять сервисы приложения (docker compose up -d)
ansible.builtin.command:
cmd: "{{ compose_cmd }} up -d"
cmd: "{{ compose_cmd }} up -d {{ sarex_services | join(' ') }}"
chdir: "{{ deploy_dir }}"
when: sarex_compose_up | bool
changed_when: true
tags: [up]
# --- Django-суперпользователь: генерация логина+пароля, создание -----------
# Только по явной команде (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}
container_name: redis
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:
- sarex-redis-data:/data
healthcheck:
@ -273,8 +275,8 @@ services:
environment: &sarex-backend-env
ALLOWED_HOSTS: "*"
DJANGO_SETTINGS_MODULE: config.settings.production
JWT_PRIVATE_KEY: "JWT"
JWT_PUBLIC_KEY: "JPK"
JWT_PRIVATE_KEY: ${SAREX_JWT_PRIVATE_KEY:-JWT}
JWT_PUBLIC_KEY: ${SAREX_JWT_PUBLIC_KEY:-JPK}
DJANGO_POSTGRES_HOST: postgres
DJANGO_POSTGRES_DATABASE: ${SAREX_POSTGRES_DB:-sarex_db}
DJANGO_POSTGRES_USER: ${SAREX_DJANGO_DB_USER:-django}
@ -300,6 +302,8 @@ services:
volumes:
# свой uwsgi.ini поверх образного: http-socket + 4 воркера (:z — SELinux/RedOS)
- ./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:
postgres:
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
environment:
<<: *sarex-backend-env
volumes:
- ./backend/production.py:/opt/sarex/config/settings/production.py:ro,z
depends_on:
postgres:
condition: service_healthy
@ -362,7 +368,7 @@ services:
POSTGRES_SSL_USE: "0"
ENABLE_SQL_QUERY: "0"
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:
- "8081:8080"
depends_on: