- 1) Что даёт шаблон
- 2) Архитектура и модель транзакций
- 3) RBAC-контракт: версионируемый каталог ролей/прав
- 4) Схема БД и связи таблиц
- 5) Механизм RBAC-миграций (как менять роли/права)
- 6) Конфигурация окружения
- 7) Запуск runtime
- 8) Актуальный API surface
- 9) Smoke-проверка
- 10) Quality и тесты
- Чёткие границы слоёв:
domain: чистые бизнес-правила и политикиapplication: orchestration use-case через портыinfrastructure: адаптеры БД/кеша/безопасностиpresentation: HTTP-роуты/схемы/DI
- Runtime-стек:
- FastAPI + Dishka DI
- Postgres (
asyncpg+ SQLAlchemy) - Redis
- Alembic-миграции
- JWT access/refresh
- Контроль качества:
ruff+ty+pytestчерезmake check
- На один HTTP-запрос:
- одна request-scoped
AsyncSession - один request-scoped
TransactionManager
- одна request-scoped
- Write use-case выполняются в короткой транзакции (
run_result_in_tx+manager.transaction()). - Обертка write-транзакции повторяет весь use-case на transient DB-ошибках (
db.transient.*) с ограниченным exponential backoff. - Поведение вложенности:
nested=True-> savepoint (begin_nested)- если scope уже открыт -> без доп. scope (
nullcontext) - если внешняя транзакция уже есть, но scope нет -> контролируемый commit/rollback scope
current_userпри cache miss использует отдельную короткоживущую DB-session, чтобы auth-read не менял границу write-транзакции текущего запроса.- Поведение старта:
- фабрика приложения собирает
Settingsиз env и отключает docs/openapi вprod|production - DI подключается через
setup_di(app, settings)и интеграцию Dishka routes - app-level
AppErrorhandler санитизирует payload/meta и маппит status codes assert_closed_by_default(app)проверяет auth dependency для непубличных роутов
- фабрика приложения собирает
Каталог ролей/прав сидируется и версионируется миграциями.
Публичный API не создаёт роли и permission’ы; API только назначает/снимает роли у пользователей.
Текущие системные роли (SystemRole):
super_adminadminuser
Текущие permission-коды (PermissionCode):
users:readusers:createusers:updateusers:deleterbac:read_rolesrbac:assign_rolerbac:revoke_role
Текущий реестр (ROLE_PERMISSIONS):
user: пустой набор правadmin: полный admin-набор вышеsuper_admin: сейчас равенadmin(подготовлена точка расширения)
Дополнительные роли/права можно добавлять миграционным шаблоном (20260209_0003), а затем назначать через API.
Основные таблицы создаются миграцией 20260203_0001:
usersidUUID PKemailunique (stored lowercase)loginuniqueusernameuniquepassword_hashis_active
rolesidUUID PKcodeuniquedescription
permissionscodePKdescription
role_permissions(many-to-many роль <-> permission)- PK: (
role_id,permission_code) - FK
role_id -> roles.id(CASCADE) - FK
permission_code -> permissions.code(CASCADE)
- PK: (
user_roles(many-to-many пользователь <-> роль)- PK: (
user_id,role_id) - FK
user_id -> users.id(CASCADE) - FK
role_id -> roles.id(CASCADE)
- PK: (
Email canonicalization:
- Приложение нормализует
emailк lowercase при записи (register/update/login по email). - БД фиксирует инвариант
email = lower(email)через CHECK constraint (миграция20260309_0007). - Миграция упадёт, если есть коллизии (разные email, которые совпадают после lowercasing).
Цепочка для авторизации:
users -> user_roles -> roles -> role_permissions -> permissions
migrations/versions/20260203_0001_bootstrap_rbac.py:
- создаёт
users,roles,permissions,role_permissions,user_roles - создаёт индексы на связующих таблицах
- сидирует:
- все системные роли
- все permission-коды
- связи роль-права из
ROLE_PERMISSIONS
- опционально создаёт bootstrap-пользователей из
RBAC_BOOTSTRAP_USERS
Формат RBAC_BOOTSTRAP_USERS (JSON-массив, опционально):
[
{
"email": "owner@example.com",
"login": "owner",
"username": "owner",
"password": "StrongPass123!",
"roles": ["super_admin"]
}
]Если roles не указан, используется ["super_admin"].
Используй шаблон migrations/templates/roles_permissions_template.py:
- создай ревизию
uv run alembic revision -m "rbac update"
- скопируй шаблон в новую ревизию
- заполни обязательные данные:
revision,down_revisionNEW_ROLES: dict[str, frozenset[str]]
- опционально заполни:
SUPER_ADMIN_EXTRA_PERMISSIONSROLE_DESCRIPTIONSROLE_USER_BINDINGS(роль -> tuple логинов пользователей)
- применяй:
uv run alembic upgrade head
Что делает шаблон:
- валидирует формат role/permission кодов
- требует непустой набор прав для каждой объявленной роли
- делает upsert ролей и permission’ов
- синхронизирует связи role-permission (добавляет недостающие, удаляет лишние)
- может привязать существующих пользователей к ролям по login
- идемпотентен: повторный
upgradeсохраняет целевое состояние downgradeудаляет только роли, объявленные в payload этой миграции
Создай свой .env из .env.example и заполни реальными значениями:
cp .env.example .envКлючевые переменные:
DATABASE_URLREDIS_URLJWT_ISSUERJWT_AUDIENCEJWT_ALGJWT_SECRETJWT_ACCESS_TTL_SJWT_REFRESH_TTL_SDEFAULT_REGISTRATION_ROLE_CODEAUTH_USER_CACHE_TTL_S- настройки refresh-lock:
REFRESH_LOCK_TTL_SREFRESH_LOCK_WAIT_TIMEOUT_S
- настройки пула/таймаутов:
DB_POOL_SIZEDB_MAX_OVERFLOWDB_POOL_TIMEOUT_SDB_POOL_RECYCLE_SDB_CONNECT_TIMEOUT_S
- настройки Argon2:
ARGON2_TIME_COSTARGON2_MEMORY_COST_KIBARGON2_PARALLELISMARGON2_HASH_LENARGON2_SALT_LEN
- настройки observability (
OBS_OTEL_*):OBS_OTEL_ENABLEDOBS_OTEL_SERVICE_NAMEOBS_OTEL_SERVICE_VERSIONOBS_OTEL_ENVIRONMENTOBS_OTEL_EXPORTER_OTLP_ENDPOINTOBS_OTEL_METRICS_EXPORT_INTERVAL_MSOBS_OTEL_HTTP_INSTRUMENTATION_ENABLEDOBS_OTEL_CAPTURE_REQUEST_HEADERSOBS_OTEL_CAPTURE_RESPONSE_HEADERSOBS_OTEL_SANITIZE_FIELDS_CSV
- runtime-настройки observability:
OBS_OTEL_GRPC_PORTOBS_PROMETHEUS_PORTOBS_GRAFANA_PORTOBS_ALERTMANAGER_PORTOBS_LOKI_PORTOBS_ALLOY_PORTOBS_TEMPO_HTTP_PORTOBS_LOKI_RETENTION_PERIOD
Postgres safety timeouts (DB-level, migration 20260309_0006):
statement_timeout = 30slock_timeout = 1sidle_in_transaction_session_timeout = 30s
Для прода:
- используй сильные секреты и реальные креды
- не коммить
.env - в
APP_ENV=prod|productionотключаются docs/openapi
Compose scope-файлы (модульный runtime):
compose/data.yaml— Postgres + Rediscompose/migrate.yaml— one-shot шаг миграций/инициализацииcompose/app.yaml— app + nginxcompose/release.yaml— release overlay (только pull image, без build)compose/obs.yaml— OTel/Prometheus/Grafana/Loki/Tempo/Alertmanager/VictoriaMetricscompose/core.hostnet.yaml— Linux host-network override для Postgres/Rediscompose/migrate.hostnet.yaml— Linux host-network override для migratecompose/app.hostnet.yaml— Linux host-network override для app/nginxcompose/obs.hostnet.yaml— Linux host-network override для observability
Базовый runtime (data + migrate + app):
docker compose -f compose/data.yaml down -v --remove-orphans
docker compose -f compose/data.yaml up -d
DOCKER_BUILD_NETWORK=host docker compose -f compose/data.yaml -f compose/migrate.yaml run --rm migrate
docker compose -f compose/data.yaml -f compose/app.yaml up -d --buildRelease runtime (prebuilt image, без сборки на сервере):
export APP_IMAGE=ghcr.io/<owner>/fastapi-template-app:vX.Y.Z
export MIGRATE_IMAGE=ghcr.io/<owner>/fastapi-template-migrate:vX.Y.Z
export NGINX_IMAGE=ghcr.io/<owner>/fastapi-template-nginx:vX.Y.Z
# Опционально для локального smoke с локальными тегами:
# export RELEASE_PULL_POLICY=missing
docker compose -f compose/data.yaml up -d
docker compose -f compose/data.yaml -f compose/migrate.yaml -f compose/release.yaml run --rm migrate
docker compose -f compose/data.yaml -f compose/release.yaml up -dCI release flow по тегу:
.github/workflows/release-images.ymlсобирает и публикует образыapp/migrate/nginxв GHCR на тегахv*..github/workflows/release.ymlавтоматически создаёт GitHub Release object для того же тега.
Runtime с observability:
OBS_OTEL_ENABLED=true docker compose -f compose/data.yaml -f compose/app.yaml -f compose/obs.yaml up -d --buildLinux host-network fallback (используй, если с хоста недоступны Docker-публикации портов):
docker compose -f compose/data.yaml -f compose/core.hostnet.yaml down -v --remove-orphans
docker compose -f compose/data.yaml up -d
DOCKER_BUILD_NETWORK=host docker compose -f compose/data.yaml -f compose/migrate.yaml -f compose/core.hostnet.yaml -f compose/migrate.hostnet.yaml run --rm migrate
DOCKER_BUILD_NETWORK=host docker compose -f compose/data.yaml -f compose/app.yaml -f compose/core.hostnet.yaml -f compose/app.hostnet.yaml up -d --build
OBS_OTEL_ENABLED=true docker compose -f compose/data.yaml -f compose/app.yaml -f compose/obs.yaml -f compose/core.hostnet.yaml -f compose/app.hostnet.yaml -f compose/obs.hostnet.yaml up -dЕсли curl на хосте возвращает 000 (Empty reply from server):
- Обычно это сетевой/runtime нюанс Docker на конкретной машине, а не падение приложения.
- Сначала проверь сервис из Docker-сети:
docker run --rm --network <project>_default curlimages/curl:8.12.1 -s -o /dev/null -w '%{http_code}\n' http://nginx:8080/system- Для этой машины используй host-network fallback:
docker compose -f compose/data.yaml -f compose/app.yaml -f compose/core.hostnet.yaml -f compose/app.hostnet.yaml up -d --build
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/systemСтандартные порты сервисов:
- app:
8080 - postgres:
5432 - redis:
6379 - OTel gRPC:
4317(сcompose/obs.yaml) - Prometheus-1:
9090(сcompose/obs.yaml) - Prometheus-2:
9091(сcompose/obs.yaml) - Grafana:
3000(сcompose/obs.yaml) - Alertmanager-1:
9093(сcompose/obs.yaml) - Alertmanager-2:
9193(сcompose/obs.yaml) - Loki:
3100(сcompose/obs.yaml) - Alloy:
12345(сcompose/obs.yaml) - Tempo:
3200(сcompose/obs.yaml) - VictoriaMetrics:
8428(сcompose/obs.yaml)
Если любой стандартный порт занят, освободи его вручную до запуска (или останови конфликтующий процесс/контейнер).
Лимиты auth в nginx настраиваются через compose env:
AUTH_LOGIN_RATEAUTH_REGISTER_RATEAUTH_REFRESH_RATE
Entrypoint’ы runtime:
compose/migrate.yaml(migrate.command) ->uv run --no-dev --no-sync --frozen alembic upgrade headcompose/app.yaml(app.command) ->uv run --no-dev --no-sync --frozen uvicorn ...compose/release.yaml-> image-only сервисы app/migrate/nginx сpull_policy: ${RELEASE_PULL_POLICY:-always}(release-деплой без локальной сборки)
Горизонтальное масштабирование только app-сервиса (без затрагивания data/obs scope):
docker compose -f compose/data.yaml -f compose/app.yaml up -d --scale app=3Observability-стек (включается добавлением -f compose/obs.yaml и OBS_OTEL_ENABLED=true для app):
- конфиг OTel Collector:
ops/otel-collector/config.yaml - конфиг scrape для Prometheus:
ops/prometheus/prometheus.yml,ops/prometheus/prometheus-2.yml - правила Prometheus:
ops/prometheus/rules/recording.yml,ops/prometheus/rules/alerts.yml - шаблон конфига Alertmanager:
ops/alertmanager/alertmanager.yml - скрипт рендера runtime-конфига Alertmanager:
ops/alertmanager/render-config.sh - конфиг Loki:
ops/loki/loki.yml - конфиг Alloy:
ops/alloy/config.alloy - конфиг Tempo:
ops/tempo/tempo.yml - provisioning datasource для Grafana:
ops/grafana/provisioning/datasources/datasources.yml - provisioning dashboard для Grafana:
ops/grafana/provisioning/dashboards/dashboards.yml - в compose OTEL по умолчанию выключен (
OBS_OTEL_ENABLED=false) - observability-порты в default compose-режиме биндуются только на localhost (
127.0.0.1) - gRPC Collector публикуется на
${OBS_OTEL_GRPC_PORT}(по умолчанию4317) для локальной отладки - UI Prometheus-1:
http://127.0.0.1:${OBS_PROMETHEUS_PORT}(по умолчанию9090) - UI Prometheus-2:
http://127.0.0.1:${OBS_PROMETHEUS_REPLICA_PORT}(по умолчанию9091) - UI Grafana:
http://127.0.0.1:${OBS_GRAFANA_PORT}(по умолчанию3000, кредыadmin/admin; переопределяется черезGRAFANA_ADMIN_USER,GRAFANA_ADMIN_PASSWORD) - UI Alertmanager-1:
http://127.0.0.1:${OBS_ALERTMANAGER_PORT}(по умолчанию9093) - UI Alertmanager-2:
http://127.0.0.1:${OBS_ALERTMANAGER_REPLICA_PORT}(по умолчанию9193) - API Loki:
http://127.0.0.1:${OBS_LOKI_PORT}(по умолчанию3100) - UI Alloy:
http://127.0.0.1:${OBS_ALLOY_PORT}(по умолчанию12345) - UI/API Tempo:
http://127.0.0.1:${OBS_TEMPO_HTTP_PORT}(по умолчанию3200) - UI/API VictoriaMetrics:
http://127.0.0.1:${OBS_VICTORIAMETRICS_PORT}(по умолчанию8428) - retention Prometheus задается через
OBS_PROMETHEUS_RETENTION(по умолчанию24h) и хранится в Docker volume - retention Alertmanager задается через
ALERTMANAGER_RETENTION(по умолчанию120h) и хранится в Docker volume - заголовок
X-Trace-Idдобавляется только при наличии валидного активного trace - OTLP receiver в Collector защищен bearer auth (
OBS_OTEL_AUTH_TOKENв collector,OBS_OTEL_EXPORTER_OTLP_HEADERSв app) - OTLP exporter в app поддерживает опциональные TLS/mTLS-файлы:
OBS_OTEL_EXPORTER_OTLP_CA_CERT_FILEOBS_OTEL_EXPORTER_OTLP_CLIENT_CERT_FILEOBS_OTEL_EXPORTER_OTLP_CLIENT_KEY_FILE
- Опциональные флаги runtime-инструментации:
OBS_OTEL_SQLALCHEMY_INSTRUMENTATION_ENABLED=trueвключает инструментирование SQLAlchemy engine при созданииOBS_OTEL_REDIS_INSTRUMENTATION_ENABLED=trueвключает инструментирование Redis-клиента
- Collector строит span-to-metrics для зависимостей (DB/Redis) через
spanmetricsconnector:- Spanmetrics считаются в отдельном traces-pipeline без tail sampling; tail sampling применяется только в pipeline экспорта в Tempo.
traces_spanmetrics_calls_totaltraces_spanmetrics_duration_seconds_bucket
- datasource Grafana конфигурируются через env:
GRAFANA_PROMETHEUS_URLGRAFANA_PROMETHEUS_LTS_URLGRAFANA_LOKI_URLGRAFANA_TEMPO_URL
- Реплики Prometheus пишут remote_write в VictoriaMetrics для long-term retention (
/api/v1/write) - webhook endpoints Alertmanager конфигурируются через env:
ALERTMANAGER_WEBHOOK_TICKET_URLALERTMANAGER_WEBHOOK_PAGE_URLALERTMANAGER_WEBHOOK_HEARTBEAT_URL
- Опциональные прямые каналы (дополнительно к webhook):
ALERTMANAGER_TICKET_SLACK_WEBHOOK_URLALERTMANAGER_PAGE_SLACK_WEBHOOK_URLALERTMANAGER_PAGE_PAGERDUTY_ROUTING_KEY
- при
APP_ENV=prodдо запуска задай не-дефолтныйGRAFANA_ADMIN_PASSWORDи не используй локальные URL для Alertmanager receiver-ов.
Для ежедневной работы в Prometheus добавлены shortcut recording series:
service:rps:5mservice:error_rate_5xx:5mservice:latency_p95_ms:5mdependency:db_redis_error_rate_pct:5mdependency:db_redis_latency_p95_seconds:5m- Также добавлены monitoring-of-monitoring alert-правила:
NoHTTPMetricsData,NoCollectorMetricsData,WatchdogCollectorExporterQueueSaturationCollectorReceiverRefusedDataCollectorExporterSendFailuresAlertmanagerNotificationFailures
- Alert-правила зависимостей:
HighDependencyErrorRateHighDependencyP95Latency
- Уведомление по алерту отправляется только когда правило находится в firing не меньше времени
for.
Преднастроенные Grafana dashboard:
Service REDPlatform ObservabilityService REDвключает фильтрыService/Dependency, drill-down ссылки на dependency-alerts и ссылки в Tempo Explore.Start Here (Overview)преднастроен как домашний dashboard Grafana и показывает только базовые сигналы здоровья.
Текущие пути OpenAPI:
POST /auth/registerPOST /auth/loginPOST /auth/logoutPOST /auth/refreshGET /systemPOST /usersGET /users/mePATCH /users/{user_id}DELETE /users/{user_id}GET /rbac/users/{user_id}/rolesPOST /rbac/users/{user_id}/rolesDELETE /rbac/users/{user_id}/roles/{role_code}
curl -i http://127.0.0.1:8080/system
curl -i http://127.0.0.1:8080/openapi.json
curl -i http://127.0.0.1:8080/docsПолный gate:
make checkLive endpoint matrix (опционально):
RUN_LIVE_E2E=1 E2E_BASE_URL=http://127.0.0.1:8080 uv run pytest tests/test_e2e_endpoint_matrix_live.py -qОпциональный admin-сегмент:
E2E_ADMIN_BEARER="<admin access token>"