заведён канон общих конвенций для личных проектов
- 13 конвенций по осям arch / lang / stack / common; репозитории берут оттуда копии в свой docs/conventions/ и коммитят их у себя - conv — синхронизация копий: add / status / diff / pull / push, локальные регионы исключены из сравнения, поэтому расхождение не даёт шума
This commit is contained in:
+122
@@ -0,0 +1,122 @@
|
||||
---
|
||||
status: рекомендуемая
|
||||
---
|
||||
|
||||
# Конфигурация приложения
|
||||
|
||||
Как устроена конфигурация: где лежит, как попадает в процесс, что с
|
||||
секретами и когда падает.
|
||||
|
||||
## Файл, а не окружение
|
||||
|
||||
**Конфигурация — файл.** Причины, по убыванию веса:
|
||||
|
||||
- **Один типизированный источник.** Файл несёт секции, комментарии,
|
||||
единицы измерения и валидируется целиком. Окружение — плоский набор
|
||||
нетипизированных строк, который приходится документировать отдельно;
|
||||
появление второго канала конфигурации гарантирует расхождение между ними.
|
||||
- **Окружение наследуется дочерними процессами.** Всё, что приложение
|
||||
запускает — конвертер, `git`, шелл-хук, — по умолчанию получает копию
|
||||
секретов, хотя они ему не нужны.
|
||||
- **В контейнере окружение расползается по лишним поверхностям.**
|
||||
`docker inspect` показывает его любому, у кого есть доступ к сокету
|
||||
докера; переменные оседают в compose-файле и `.env` на диске — то есть
|
||||
файл всё равно появляется, только без структуры и валидации.
|
||||
|
||||
Обратите внимание, чего в списке **нет**: `/proc/<pid>/environ` не является
|
||||
аргументом — он имеет права `0400` и защищён проверкой `PTRACE_MODE_READ`,
|
||||
то есть доступен ровно тому же кругу, что и файл под `0600`.
|
||||
|
||||
Запрет держится на «один источник» и на том, что все приложения свои. Для
|
||||
стороннего образа, живущего на env, конвенция неприменима — это не повод
|
||||
отказываться от неё для своих.
|
||||
|
||||
Практика:
|
||||
|
||||
- Формат — текстовый, с комментариями и секциями (TOML, YAML — по стеку).
|
||||
- Имя по умолчанию фиксировано и ищется в рабочей директории процесса;
|
||||
путь переопределяется опцией командной строки.
|
||||
- Реальный конфиг не коммитится. В репозитории лежит **образец**.
|
||||
|
||||
## Грузим один раз, дальше не перечитываем
|
||||
|
||||
- Разбор — **один раз при старте**, в одну типизированную структуру.
|
||||
Дальше по коду читаем только её: чтения файла в бизнес-коде нет.
|
||||
- Конфиг **неизменяем** после старта; смена параметров — рестарт процесса.
|
||||
Горячая перезагрузка — отдельное решение с отдельным обоснованием, а не
|
||||
умолчание.
|
||||
- Умолчания задаются в коде, файл их перекрывает. Образец при этом
|
||||
перечисляет **все** поля, включая те, у которых есть умолчание: поле,
|
||||
живущее только в коде, для читателя конфига не существует.
|
||||
|
||||
## Образец самодокументируем
|
||||
|
||||
Образец коммитим как единый справочник по конфигу: все секции и все поля.
|
||||
**Каждое поле снабжаем комментарием**, из которого ясно:
|
||||
|
||||
- **зачем** поле — что оно меняет в поведении;
|
||||
- **диапазон или допустимые значения** — перечисление либо границы;
|
||||
- **единицы измерения**, если применимо: секунды/миллисекунды, байты, доля
|
||||
`0–1`.
|
||||
|
||||
Так конфиг читается без открывания кода — этим он и полезен.
|
||||
|
||||
## Поля по дискриминатору `type`
|
||||
|
||||
Когда набор полей секции зависит от поля-дискриминатора (выбор одного из
|
||||
бекендов или внешних сервисов), обязательность полей определяется его
|
||||
значением, а не фиксирована для секции.
|
||||
|
||||
- **Валидация — по значению `type`**: для каждого поддерживаемого варианта
|
||||
свой набор обязательных полей; поля других вариантов не требуются.
|
||||
Неизвестное значение → ошибка на старте с перечислением поддерживаемых.
|
||||
- **Образец — по значению `type`**: основной вариант предзаполнен рабочими
|
||||
значениями, альтернативные — блоками-комментариями ниже, каждый со своим
|
||||
описанием полей. Из примера видны все варианты, не открывая код.
|
||||
|
||||
## Секреты приносит деплой
|
||||
|
||||
Секреты доставляет **деплой**, рендеря их прямо в конфиг. Отдельного слоя
|
||||
секретов в приложении нет — оно просто читает файл. Источник истины
|
||||
секрета — внешнее хранилище деплоя, не репозиторий и не окружение.
|
||||
|
||||
- Рендеренный конфиг не коммитится; права `0600`, владелец — runtime-
|
||||
пользователь.
|
||||
- В образце секретные поля — пустые строки.
|
||||
- Загрузчик на старте проверяет, что обязательные секреты не пусты: это
|
||||
ловит криво отрендеренный шаблон до того, как он превратится в 401 от
|
||||
внешнего API через час работы.
|
||||
- В логи секреты не попадают.
|
||||
|
||||
<!-- local:секретные-поля -->
|
||||
<!-- /local -->
|
||||
|
||||
## Валидация и fail-fast
|
||||
|
||||
Конфиг валидируем **на старте, до приёма трафика**. Невалидный конфиг —
|
||||
запись уровня `ERROR` и выход с ненулевым кодом: не стартуем «наполовину».
|
||||
|
||||
Проверяем как минимум:
|
||||
|
||||
- обязательные поля заданы, обязательные секреты не пусты;
|
||||
- пути существуют и доступны на запись/чтение по назначению;
|
||||
- числовые диапазоны и единицы (доли, таймауты, счётчики попыток);
|
||||
- строки, которые парсятся во что-то (длительности, зоны, URL), реально
|
||||
парсятся;
|
||||
- включённые секции консистентны: если интеграция включена — заданы все её
|
||||
обязательные поля.
|
||||
|
||||
Проблемы собираем и показываем **разом**, а не по одной за запуск.
|
||||
|
||||
<!-- local:проверки -->
|
||||
<!-- /local -->
|
||||
|
||||
## Связано
|
||||
|
||||
- `arch/time.md` — формат времени; зона отображения — единственный
|
||||
конфигурируемый параметр времени, семантика описана там.
|
||||
- `arch/app-directories.md` — конфиг лежит в категории «конфигурация» и
|
||||
доступен приложению только на чтение.
|
||||
|
||||
<!-- local:связано -->
|
||||
<!-- /local -->
|
||||
Reference in New Issue
Block a user