Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Конфигурация приложений: слои, типы и 12-factor

Конфигурация Python-приложения: env, файлы, секреты и порядок слоёв

Как организовать настройки: env против конфигов, pydantic-settings и слои значений, секреты отдельно от конфига, профили dev/prod и валидация на старте.

Прямой ответ

Рабочая схема конфигурации: один источник правды в коде — Pydantic-settings класс (class Settings(BaseSettings): database_url: str; debug: bool = False; log_level: str = "INFO" — значения из env с типизацией и валидацией на старте: опечатка в переменной или неверный тип — падение при запуске с ясной ошибкой, а не «загадочный баг в проде через неделю»), слои с явным приоритетом — env > .env файл > дефолты кода (12-factor: конфигурация в окружении — принцип; .env — удобство разработки, в проде значения подставляет оркестратор — docker -e/compose environment/K8s ConfigMap+Secret — переменные), секреты — отдельный класс и отдельное хранилище (никогда в git — даже в .env.example только имена), никаких if prod/else dev в логике (поведение различается значениями конфига, не ветками кода по окружению). Антипаттерны, которые лечит эта схема: os.getenv("DB_URL") в двадцати модулях (нет единой точки, нет типов, нет валидации), конфиг-файлы в репозитории с боевыми значениями (секреты в истории git навсегда), «конфиг как словарь» (ключи-опечатки неуловимы до рантайма), и чтение конфигурации при импорте модуля (невозможно подменить в тестах — settings как зависимость/injectable решает — тестирование). Для сложных структур (списки, вложенность) — JSON/YAML в env-переменной или файл-конфиг, загружаемый через ту же Pydantic-модель (типизация сохраняется), для «одной переменной на всё» — не плодить: APP_CONF=/etc/app/config.yaml + env-переопределения отдельных ключей — компромисс, используемый в серьёзных системах (nginx-стиль).

Слой 1: pydantic-settings как контракт

Код: Settings с полями-типами (int/bool/Enum/HttpUrl — pydantic приведёт и проверит; bool из «1/true/yes» — разбор встроен), model_config = SettingsConfigDict(env_file=".env", env_prefix="APP_") — префикс исключает коллизии (APP_DEBUG vs чужой DEBUG), @lru_cache def get_settings() — единственный инстанс на процесс (и точка подмены в тестах — get_settings.cache_clear()/dependency override в FastAPI — зависимости); валидация бизнес-правил — валидаторами модели («debug=True недопустим при env=production» — падение на старте — защита от «забыли выключить debug»); документирование — дефолты и docstring-и полей (конфиг как документация — что можно крутить и чем это кончается). Что важно держать в Settings, а что нет: в Settings — всё, что различается между окружениями (URL БД, логи, флаги фич — feature flags как подмножие конфига) и что может понадобиться сменить без релиза; не в Settings — константы предметной области (лимиты бизнес-логики, не зависящие от окружения — они код, меняются релизом с ревью — смешение «конфига» со «всем, что может меняться» размывает границу и плодит env-зоопарк).

Слой 2: файлы, профили, секреты

Когда файлов не избежать: сложный конфиг инструментов (nginx, alembic, pytest — свои форматы), многосекционные конфиги приложения (десятки вложенных ключей — env-переменных становится 200 — YAML/TOML + Pydantic-парсинг файла честнее); порядок — файл как база, env как переопределение (операционное правило: чем ближе к деплою, тем выше приоритет — код-дефолты < файл в репо (dev-база) < файл вне репо (/etc/app) < env (оркестратор) — последнее слово за средой исполнения). Профили (dev/staging/prod) — не if ENV == "prod" в коде, а: отдельные значения переменных на среду (оркестратор подставляет), максимум — config.dev.yaml/config.prod.yaml выбор файла по env-переменной (профиль — данные, не логика); «профильное поведение» в коде (debug-only ветки) — через флаги (debug: bool), а не через имя окружения (тест «профиль staging ведёт себя как prod» должен быть тривиален). Секреты — дисциплина: отдельные класс/файл, в проде — секрет-хранилище (K8s Secret/Vault), в git — только .env.example с именами и фейк-значениями; ротация секрета — смена значения, не кода; логирование настроек на старте — с маскировкой (url с паролем — печатать хост без credentials — «дамп конфига в лог» сливал пароли не однажды). Миграция легаси (os.getenv-зоопарк → Settings) — постепенная: Settings вводится, модули переводятся по одному (get_settings() вместо getenv в точке использования — импорты/структура — settings-модуль в ядре, не в utils-свалке), getenv-остатки — линтер-правилом/ban-листом.

Практический сценарий: наведение порядка за неделю

Исходник: 60+ os.getenv по коду, два .env в git-истории (с боевым паролём БД — ротация обязательна и сделана), «конфиг» частично в constants.py, dev/prod-различия — if settings.ENV == "prod" в четырёх модулях. Шаг 1 — Settings-класс со всеми переменными (инвентаризация: grep os.getenv — полный список — 63 ключа, из них 12 мертвы — не читаются нигде после прошлых рефакторингов — удалены); Шаг 2 — .env.example + реальный .env вне git (в .gitignore был, но история — ротация пароля БД и всех API-ключей); Шаг 3 — перевод модулей на get_settings() (30 файлов — механическая замена — отдельный PR с тестами — CI зелёный); Шаг 4 — убраны ENV-ветки: debug-логика под флаг debug (на staging теперь можно включить прод-подобный debug — раньше было нельзя — «staging не воспроизводит прод» частично закрыто); Шаг 5 — валидация на старте: на первом же деплое — падение «REDIS_URL: field required» — переменная в новом манифесте не перенесена — найдено за минуту до трафика (раньше — молчаливый fallback на localhost и час расследования); Шаг 6 — документация: таблица переменных (имя/тип/дефолт/смысл) генерируется из Settings-модели (pydantic-schema → markdown в CI — конфиг-документация не устаревает). Итог недели: 51 живая переменная в одном типизированном контракте, старт-валидация, ноль секретов в репо, профили — данными.

FAQ

env против TOML/YAML — что выбрать для нового проекта? Старт — env/pydantic-settings (12-factor-совместимость с любым оркестратором из коробки, ноль парсинга); TOML/YAML — когда конфигурация объективно сложна (вложенность/списки >20 ключей) или это инструмент с файловым конфигом по природе; гибрид (файл-база + env-переопределения) — стандарт для «серьёзных» сервисов; главное — типизированная модель поверх любого формата.

Почему не хранить конфиг в БД? Конфиг старта (как подключить БД) в БД не храним по определению; рантайм-настройки (флаги, лимиты) в БД/конфиг-сервисе — законно (смена без деплоя — feature flags), но с кешем/фолбэком (БД с настройками легла — приложение должно стартовать на дефолтах) и версионированием изменений (кто и когда крутил — аудит); «весь конфиг в БД» — антипаттерн bootstrapping-а.

Как тестировать код, зависящий от Settings? Три уровня: дефолты Settings покрывают тест-среду (Settings() без env — валидные dev-значения); подмена — dependency override/monkeypatch get_settings (фикстура — pytest-паттерны); отдельные тесты самой модели — валидаторы («debug+production → ошибка» — тест на правило); антипаттерн — чтение реального .env в тестах (результаты зависят от машины).

Нужен ли конфиг-сервис (Consul/etcd) обычному продукту? До десятков сервисов — нет: env+секрет-хранилище оркестратора покрывают; конфиг-сервис оправдан при динамической переконфигурации без рестарта на масштабе (флаги/лимиты на лету — специализированные flag-сервисы проще Consul-самодела); преждевременный конфиг-сервис — ещё один stateful-компонент в эксплуатации без выигрыша.

Источники: 12factor.net, документация pydantic-settings — ссылки в шапке.