# Конфигурация проекта mapper (flows mapper) Документ описывает все переменные окружения и способы конфигурирования сервиса `mapper` — mini-HTTP-сервиса, который объединяет (мапит) данные сервисов документации (`documentations`) и процессов (`flows`), а также заметок (`notes`) и Django-бэкенда Sarex. ## Способы конфигурирования Сервис настраивается **только через переменные окружения**. Разбор выполняется в `src/app/config.py` через библиотеку [`pydantic-settings`](https://docs.pydantic.dev/latest/concepts/pydantic_settings/). В отличие от многих сервисов, здесь **нет единого корневого префикса и нет вложенного делимитера** (`env_nested_delimiter`). Вместо этого каждая секция описана отдельным классом `BaseSettings` со своим `env_prefix` (задаётся во вложенном классе `Config`): | Класс | `env_prefix` | Секция | | --- | --- | --- | | `Settings` | — (без префикса) | Корневые настройки (`API_PREFIX`) | | `LoggerSettings` | `LOG_` | Логирование | | `RedisSettings` | `REDIS_` | Кеш Redis | | `DocumentationSettings` | `DOCUMENTATION_` | HTTP-клиент сервиса документации | | `FlowSettings` | `FLOW_` | HTTP-клиент сервиса процессов | | `DjangoSettings` | `DJANGO_` | HTTP-клиент Django-бэкенда | | `NoteSettings` | `NOTE_` | HTTP-клиент сервиса заметок | `DocumentationSettings`, `FlowSettings`, `DjangoSettings` и `NoteSettings` наследуются от общего класса `AsyncSessionManager` (поля `host`, `timeout`, `retries`), поэтому у каждого из них одинаковый набор из трёх переменных: `_HOST`, `_TIMEOUT`, `_RETRIES`. Отдельного конфиг-файла (yaml/toml) у приложения нет. Приложение **не загружает `.env` автоматически** (в `config.py` не задан `env_file`, зависимости `python-dotenv` нет) — переменные нужно экспортировать в окружение самому, напр. `set -a && . ./.env && set +a`, либо пробрасывать через контейнер/оркестратор. Источники переменных по способам запуска: | Способ запуска | Откуда берутся переменные | | --- | --- | | Локально (uvicorn/gunicorn) | Переменные окружения процесса. Redis поднимается через `docker-compose.yaml` (только сервис `redis`) | | Контейнер | `Dockerfile` → `entrypoint.sh`: `gunicorn -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000 app.main:app` | | Kubernetes (Helm, из репозитория сервиса) | `.helm/values.yaml`, блок `universal-chart.services.backend.envs`; деплой из `.gitlab-ci.yml` | | Kubernetes (GitOps, инфра-репозиторий) | `iac/apps/mapper/*`: Kustomize-база `base/deployment.yaml` (env + секреты Vault) и Flux `HelmRelease` в overlay'ах `brusnika-*` | Точки входа: | Команда | Назначение | | --- | --- | | `uvicorn main:app` / `python app/main.py` | Локальный запуск (в `main.py` порт `8002`, host `0.0.0.0`) | | `gunicorn -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000 app.main:app` | Прод-запуск (`entrypoint.sh`, порт `8000`) | Стек: Python 3.10 (`python:3.10-slim-buster`), FastAPI, httpx (асинхронные клиенты), Redis (кеш), PyJWT (разбор токенов). ## Переменные приложения Дефолт `—` означает, что значение обязательно (иначе ошибка старта). Все дефолты ниже соответствуют коду `config.py`. ### App (класс `Settings`) | Переменная | Тип | Значение по умолчанию | Назначение | | --- | --- | --- | --- | | `API_PREFIX` | string | `/api/v1` | Префикс маршрутов API | ### Logger (`LOG_*`) | Переменная | Тип | Значение по умолчанию | Назначение | | --- | --- | --- | --- | | `LOG_LEVEL` | string | `INFO` | Уровень логирования (имя уровня `logging`; при неизвестном значении используется `INFO`) | | `LOG_FORMAT` | string | `[%(asctime)s] [%(levelname)s] [%(filename)s]: %(message)s` | Формат строки лога | ### Redis (`REDIS_*`) Кеширует JSON-ответы внешних сервисов (по ключу `"{user_id}_{url}"`). | Переменная | Тип | Значение по умолчанию | Назначение | | --- | --- | --- | --- | | `REDIS_USE` | bool | `True` | Включить кеш. При `True` на старте выполняется `PING` (падение при недоступном Redis) | | `REDIS_HOST` | string | `localhost` | Хост Redis | | `REDIS_PORT` | int | `6379` | Порт Redis | | `REDIS_DB` | int | `0` | Номер базы Redis | | `REDIS_EXPIRE_DAYS` | int | `1` | TTL записей кеша в днях (в секундах — `expire_days * 24 * 3600`) | ### HTTP-клиенты внешних сервисов Все четыре клиента наследуют `AsyncSessionManager` (`host`, `timeout`, `retries`). Клиент httpx создаётся с `verify=False` (проверка TLS-сертификата отключена) и транспортом с числом ретраев `retries`. Токены пробрасываются заголовками `Authorization` (всегда) и `Identity` (в режиме Zitadel). | Секция / префикс | Назначение | Дефолт `HOST` | | --- | --- | --- | | `DOCUMENTATION_*` | Сервис документации (диски, документы, бандлы) | `https://stage-api.sarex.io/documentations/api/v1` | | `FLOW_*` | Сервис процессов (flows, review-данные) | `https://stage-api.sarex.io/flows/api/v1` | | `DJANGO_*` | Django-бэкенд Sarex (target-links) | `https://stage.sarex.io/api` | | `NOTE_*` | Сервис заметок (notes) | `https://stage-api.sarex.io/notes/api/v1` | Для каждого — три переменные (пример для `FLOW`): | Переменная | Тип | Значение по умолчанию | Назначение | | --- | --- | --- | --- | | `FLOW_HOST` | string | см. выше | Базовый URL сервиса | | `FLOW_TIMEOUT` | int | `30` | Таймаут запроса (сек) | | `FLOW_RETRIES` | int | `3` | Число повторов транспорта httpx | ## Аутентификация Аутентификация выполняется в `src/app/dependensies.py` (`get_user_data`) на основе заголовков запроса и **без проверки подписи токена** (`jwt.decode(..., options={"verify_signature": False})`). Публичный ключ не используется, отдельных переменных для ключа нет. | Условие | Режим | Как извлекается `user_id` | | --- | --- | --- | | Есть заголовки `Authorization` и `Identity` | `zitadel` | Из payload `Identity`-токена, поле `urn:zitadel:iam:user:metadata.user_id` (base64) | | Есть только `Authorization` | `sarex` | Из payload основного токена, поле `user_id` | | Заголовков нет | — | `401 Unauthorized` | ## Переменные инфраструктуры и сборки Не читаются кодом приложения, но участвуют в сборке/запуске. | Переменная | Где используется | Назначение | | --- | --- | --- | | `SERVICE_NAME` | `.gitlab-ci.yml` | Имя сервиса (`mapper`), используется как `CHART_NAME` | | `DOCKERFILE_PATH` | `.gitlab-ci.yml` | Путь к Dockerfile | | `BUILD_ARGS`, `CI_TRIGGER_SOURCE` | `.gitlab-ci.yml` | Аргументы сборки / источник триггера (`app`) | | `IMAGE_NAME`, `CI_COMMIT_SHA`, `CI_PROJECT_URL`, `CI_JOB_URL`, `CI_PROJECT_NAMESPACE` | `.gitlab-ci.yml` (`HELM_SET_ARGS`) | Прокидываются в universal-chart (образ, commit, ссылки, owner) | ## Переменные из Helm-чарта репозитория сервиса (`.helm/values.yaml`) Чарт зависит от `universal-chart` (`oci://cr.yandex/.../charts`, версия `0.1.7`). Переменные приложения задаются в блоке `services.backend.envs` с разбивкой по окружениям (`_default`/`stage`/`preprod`/`production`). | Переменная | `stage` | `preprod` | `production` | | --- | --- | --- | --- | | `DOCUMENTATION_HOST` | `https://stage-api.sarex.io/documentations/api/v1` | `https://api.preprod.sarex.io/documentations/api/v1` | `https://api.sarex.io/documentations/api/v1` | | `FLOW_HOST` | `https://stage-api.sarex.io/flows/api/v1` | `https://api.preprod.sarex.io/flows/api/v1` | `https://api.sarex.io/flows/api/v1` | | `DJANGO_HOST` | `https://stage.sarex.io/api` | `https://preprod.sarex.io/api` | `https://lk.sarex.io/api` | | `NOTE_HOST` | `https://stage-api.sarex.io/notes/api/v1` | `https://api.preprod.sarex.io/notes/api/v1` | `https://api.sarex.io/notes/api/v1` | | `REDIS_USE` | `0` | `0` | `0` | | `TIMEOUT` | `120` | `120` | `120` | Прочие параметры чарта (не переменные приложения): `deployment.*` (имя, порт `8000`, `replicaCount` 1/1/3/3, ресурсы, `probes.liveness/readiness` — **отключены**), `image.name` (`cr.yandex/.../mapper`), `service.*` (ClusterIP, порт `8000`), `imagePullSecrets` (`dockerhub`), `labels.monitoring=prometheus`. ## Переменные из инфра-репозитория (`iac/apps/mapper`) GitOps-деплой через Kustomize + Flux, namespace `mapper`. Здесь же лежит настоящий документ. Структура: | Путь | Назначение | | --- | --- | | `base/` | Базовый Kustomize (`namespace`, `serviceaccount` `mapper-vault`, `deployment`, `service`) | | `yc-k8s-test/` | Overlay поверх `base` (патч `replicas: 1`) | | `brusnika-stage/` | Flux `HelmRelease` (universal-chart), хосты `test.sarex.brusnika.tech`, `imagePullSecrets: dockerhub` | | `brusnika-prod/` | Flux `HelmRelease` (universal-chart), хосты `cde.brusnika.ru`, `imagePullSecrets: regcred` | Обычные env в `base/deployment.yaml` (production-хосты Sarex): | Переменная | Значение | | --- | --- | | `DOCUMENTATION_HOST` | `https://api.sarex.io/documentations/api/v1` | | `FLOW_HOST` | `https://api.sarex.io/flows/api/v1` | | `DJANGO_HOST` | `https://lk.sarex.io/api` | | `NOTE_HOST` | `https://api.sarex.io/notes/api/v1` | | `REDIS_USE` | `0` | | `TIMEOUT` | `120` | Секреты монтируются через **HashiCorp Vault Agent** (аннотации `vault.hashicorp.com/*`, роль `mapper`) как файлы в `/vault/secrets/*`, которые перед стартом экспортируются в окружение (`set -a && . /vault/secrets/... && set +a`): | Файл секрета | Источник (Vault path) | Экспортируемые переменные | | --- | --- | --- | | `mapper-django-auth` | `secrets/data/vault/common/django_auth` | `MAPPER_DJANGO_TOKEN` | | `mapper-db` | `secrets/data/postgresql/apps/mapper` | `MAPPER_DB_USER`, `MAPPER_DB_PASSWORD`, `MAPPER_DB_HOST`, `MAPPER_DB_PORT`, `MAPPER_DB_NAME` | | `mapper-rabbitmq` | `secrets/data/rabbitmq/apps/mapper` | `MAPPER_RABBITMQ_VHOST`, `MAPPER_RABBITMQ_USERNAME`, `MAPPER_RABBITMQ_PASSWORD`, `MAPPER_RABBITMQ_HOST`, `MAPPER_RABBITMQ_PORT` | | `mapper-s3` | `secrets/data/minio/apps/mapper` | `MAPPER_S3_ENDPOINT`, `MAPPER_S3_REGION`, `MAPPER_S3_BUCKET`, `MAPPER_S3_ACCESS_KEY_ID`, `MAPPER_S3_SECRET_ACCESS_KEY` | | `mapper-kafka` | `secrets/data/kafka/apps/mapper` | `MAPPER_KAFKA_BOOTSTRAP_SERVERS`, `MAPPER_KAFKA_SECURITY_PROTOCOL`, `MAPPER_KAFKA_SASL_MECHANISM`, `MAPPER_KAFKA_USERNAME`, `MAPPER_KAFKA_PASSWORD` | ## Переменные в CI (`.gitlab-ci.yml`) Пайплайн подключает шаблоны `generic/common-ci` (`common-security-scan.yaml`, `universal-pipeline.yaml@apps-business`) и переключает окружение по ветке/тегу: | Условие | STAND | Namespace | CHART_VERSION | | --- | --- | --- | --- | | ветка `stage` | `stage` | `platform` | `0.0.1-stage` | | ветка `master` | `preprod` | `mapper-preprod` | `0.0.1-preprod` | | тег (`CI_COMMIT_TAG`) | `production` | `mapper-prod` | `0.0.1-prod` | | merge request | — (сборка образа отключена) | — | — | Стадия `test`: job `linter` (`flake8 src/app`, `max-line-length=120`) и `typechecker` (`mypy src/app` с `types-redis`; `disallow_untyped_defs=True`). ## Замечания и потенциальные проблемы - **`TIMEOUT` не читается приложением.** В Helm/Kustomize задаётся `TIMEOUT=120`, но клиенты читают `DOCUMENTATION_TIMEOUT`/`FLOW_TIMEOUT`/`DJANGO_TIMEOUT`/`NOTE_TIMEOUT` (каждый со своим префиксом). Без префикса переменная игнорируется — реальный таймаут остаётся `30`. Чтобы поднять таймаут, задавайте `_TIMEOUT`. - **Секреты Vault не используются кодом.** `MAPPER_DB_*`, `MAPPER_RABBITMQ_*`, `MAPPER_S3_*`, `MAPPER_KAFKA_*`, `MAPPER_DJANGO_TOKEN` монтируются и экспортируются в окружение (`base/deployment.yaml`), но текущая версия приложения ни PostgreSQL, ни RabbitMQ, ни S3, ни Kafka, ни `MAPPER_DJANGO_TOKEN` **не читает** (в `config.py` таких настроек нет). Похоже, инфраструктура заготовлена наперёд либо унаследована из шаблона. - **Кеш отключён во всех окружениях (`REDIS_USE=0`).** При этом в `get_response` (`utils.py`) при не-200 ответе апстрима и выключенном кеше возвращается `None`, а роутер отдаёт `400 Bad Request`. То есть при выключенном Redis запасного кеша нет. - **Подпись JWT не проверяется** (`verify_signature=False`) ни в режиме `zitadel`, ни в `sarex`. Доверие к токену — на сетевом слое (Istio/ingress). Публичный ключ не настраивается. - **TLS-проверка апстримов отключена** (`httpx.AsyncClient(verify=False)`) для всех четырёх клиентов. - **Нет healthcheck-эндпоинта.** Пробы `liveness`/`readiness` в чарте выключены — это согласовано. - **Порты различаются:** локально `main.py` слушает `8002`, в контейнере gunicorn — `8000` (проброшен в k8s Service). - **`docker-compose.yaml`** поднимает только Redis (redis-stack-server); само приложение в compose не описано. ## Минимальный набор для локального запуска Поднять Redis (`docker compose up redis`) либо задать `REDIS_USE=False`, затем `uvicorn main:app` из `src`. Минимально стоит задать (у остальных есть рабочие дефолты для stage): - `REDIS_USE` (`False`, если Redis не поднят) и при необходимости `REDIS_HOST`/`REDIS_PORT` - при работе против нестандартных стендов — `DOCUMENTATION_HOST`, `FLOW_HOST`, `DJANGO_HOST`, `NOTE_HOST` - `LOG_LEVEL` (по желанию) Готовые значения-примеры приведены в `.env.example`.