--- status: рекомендуемая --- # Конфигурация приложения Как устроена конфигурация: где лежит, как попадает в процесс, что с секретами и когда падает. ## Файл, а не окружение **Конфигурация — файл.** Причины, по убыванию веса: - **Один типизированный источник.** Файл несёт секции, комментарии, единицы измерения и валидируется целиком. Окружение — плоский набор нетипизированных строк, который приходится документировать отдельно; появление второго канала конфигурации гарантирует расхождение между ними. - **Окружение наследуется дочерними процессами.** Всё, что приложение запускает — конвертер, `git`, шелл-хук, — по умолчанию получает копию секретов, хотя они ему не нужны. - **В контейнере окружение расползается по лишним поверхностям.** `docker inspect` показывает его любому, у кого есть доступ к сокету докера; переменные оседают в compose-файле и `.env` на диске — то есть файл всё равно появляется, только без структуры и валидации. Обратите внимание, чего в списке **нет**: `/proc//environ` не является аргументом — он имеет права `0400` и защищён проверкой `PTRACE_MODE_READ`, то есть доступен ровно тому же кругу, что и файл под `0600`. Запрет держится на «один источник» и на том, что все приложения свои. Для стороннего образа, живущего на env, конвенция неприменима — это не повод отказываться от неё для своих. Практика: - Формат — текстовый, с комментариями и секциями (TOML, YAML — по стеку). - Имя по умолчанию фиксировано и ищется в рабочей директории процесса; путь переопределяется опцией командной строки. - Реальный конфиг не коммитится. В репозитории лежит **образец**. ## Грузим один раз, дальше не перечитываем - Разбор — **один раз при старте**, в одну типизированную структуру. Дальше по коду читаем только её: чтения файла в бизнес-коде нет. - Конфиг **неизменяем** после старта; смена параметров — рестарт процесса. Горячая перезагрузка — отдельное решение с отдельным обоснованием, а не умолчание. - Умолчания задаются в коде, файл их перекрывает. Образец при этом перечисляет **все** поля, включая те, у которых есть умолчание: поле, живущее только в коде, для читателя конфига не существует. ## Образец самодокументируем Образец коммитим как единый справочник по конфигу: все секции и все поля. **Каждое поле снабжаем комментарием**, из которого ясно: - **зачем** поле — что оно меняет в поведении; - **диапазон или допустимые значения** — перечисление либо границы; - **единицы измерения**, если применимо: секунды/миллисекунды, байты, доля `0–1`. Так конфиг читается без открывания кода — этим он и полезен. ## Поля по дискриминатору `type` Когда набор полей секции зависит от поля-дискриминатора (выбор одного из бекендов или внешних сервисов), обязательность полей определяется его значением, а не фиксирована для секции. - **Валидация — по значению `type`**: для каждого поддерживаемого варианта свой набор обязательных полей; поля других вариантов не требуются. Неизвестное значение → ошибка на старте с перечислением поддерживаемых. - **Образец — по значению `type`**: основной вариант предзаполнен рабочими значениями, альтернативные — блоками-комментариями ниже, каждый со своим описанием полей. Из примера видны все варианты, не открывая код. ## Секреты приносит деплой Секреты доставляет **деплой**, рендеря их прямо в конфиг. Отдельного слоя секретов в приложении нет — оно просто читает файл. Источник истины секрета — внешнее хранилище деплоя, не репозиторий и не окружение. - Рендеренный конфиг не коммитится; права `0600`, владелец — runtime- пользователь. - В образце секретные поля — пустые строки. - Загрузчик на старте проверяет, что обязательные секреты не пусты: это ловит криво отрендеренный шаблон до того, как он превратится в 401 от внешнего API через час работы. - В логи секреты не попадают. ## Валидация и fail-fast Конфиг валидируем **на старте, до приёма трафика**. Невалидный конфиг — запись уровня `ERROR` и выход с ненулевым кодом: не стартуем «наполовину». Проверяем как минимум: - обязательные поля заданы, обязательные секреты не пусты; - пути существуют и доступны на запись/чтение по назначению; - числовые диапазоны и единицы (доли, таймауты, счётчики попыток); - строки, которые парсятся во что-то (длительности, зоны, URL), реально парсятся; - включённые секции консистентны: если интеграция включена — заданы все её обязательные поля. Проблемы собираем и показываем **разом**, а не по одной за запуск. ## Связано - `arch/time.md` — формат времени; зона отображения — единственный конфигурируемый параметр времени, семантика описана там. - `arch/app-directories.md` — конфиг лежит в категории «конфигурация» и доступен приложению только на чтение.