- пятая, необязательная часть правила: код парой «плохо → хорошо» после обоснования; метка добавлена в словарь (EXAMPLES) и в строку о версии языка во всех тринадцати файлах - сказано, чем примеры не являются: требований в блоке нет, дословным сниппетом он не служит, при расхождении с нормой правят пример - READING.md обновлён по META-30, в машинные проверки добавлен порядок блоков, в читательские — что примеры норму не расширяют
23 KiB
topic, prefix
| topic | prefix |
|---|---|
| config | CONF |
Конфигурация приложения
Как устроена конфигурация: где лежит, как попадает в процесс, что с секретами и когда падает.
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
Область действия
Правила написаны для приложений, которые мы пишем сами: только там мы управляем тем, как конфигурация читается. Сторонний образ, живущий на переменных окружения, вне области действия — это не повод отказываться от конвенции для своих приложений.
Правила
CONF-1. Конфигурация — файл, а не окружение
ДОЛЖЕН. Приложение читает параметры из файла конфигурации; переменные окружения источником конфигурации не служат.
ПОЧЕМУ. Три довода, по убыванию веса:
- Один типизированный источник. Файл несёт секции, комментарии, единицы измерения и валидируется целиком. Окружение — плоский набор нетипизированных строк, который приходится документировать отдельно; появление второго канала конфигурации гарантирует расхождение между ними.
- Окружение наследуется дочерними процессами. Всё, что приложение
запускает — конвертер,
git, шелл-хук, — по умолчанию получает копию секретов, хотя они ему не нужны. - В контейнере окружение расползается по лишним поверхностям.
docker inspectпоказывает его любому, у кого есть доступ к сокету докера; переменные оседают в compose-файле и.envна диске — то есть файл всё равно появляется, только без структуры и валидации.
Обратите внимание, чего в этих доводах нет: /proc/<pid>/environ не
является аргументом — он имеет права 0400 и защищён проверкой
PTRACE_MODE_READ, то есть доступен ровно тому же кругу, что и файл под
0600.
CONF-2. Формат конфигурации — текстовый, с секциями и комментариями
СЛЕДУЕТ. Конкретный формат (TOML, YAML) выбирается по стеку.
ПОЧЕМУ. Комментарий у каждого поля (CONF-9) — часть того, ради чего конфиг вообще читают; формат, в котором комментарий негде разместить, делает CONF-9 невыполнимым. Секции дают структуру, которую валидатор проверяет целиком, — плоский список пар такой возможности не даёт и возвращает нас к тем же свойствам, из-за которых отвергнуто окружение (CONF-1).
CONF-3. Имя файла фиксировано, путь переопределяется опцией
СЛЕДУЕТ. Имя по умолчанию ищется в рабочей директории процесса, а путь задаётся опцией командной строки.
ПОЧЕМУ. Запуск без аргументов работает одинаково в разработке, в контейнере и на сервере, и способ запуска не приходится помнить отдельно для каждой среды. Опция нужна ровно для случаев, когда конфигов несколько (тесты, второй инстанс): без неё их разводят переменной окружения — тем самым каналом, который закрывает CONF-1.
CONF-20. Отсутствие файла конфигурации — ошибка старта
ДОЛЖЕН. Если файла нет ни по пути из опции, ни по имени по умолчанию в рабочей директории (CONF-3), приложение не стартует: сообщение называет искомый путь, код возврата ненулевой.
ПОЧЕМУ. Конфиг — артефакт деплоя (CONF-12), и его отсутствие означает, что развёртывание не довело работу до конца, а не что приложение попросили работать на умолчаниях. Умолчания (CONF-7) существуют, чтобы работал неполный файл, а не отсутствующий, — именно здесь читатель спотыкается чаще всего.
Старт без файла ничего не спасает: у приложения с обязательными полями или
секретами всё равно упадёт валидация (CONF-18), только вместо одного сообщения
«нет config.toml» получится каскад «поле пусто», за которым настоящая
причина — деплой не отрендерил файл — не видна.
Приложение, которое запускается вообще без конфигурации, этой конвенцией не описывается: это отдельный случай и отдельная конвенция.
CONF-4. В репозитории лежит образец, а не рабочий конфиг
НЕ ДОЛЖЕН. Реальный конфиг не коммитится; в репозитории — образец.
ПОЧЕМУ. Рабочий конфиг содержит отрендеренные секреты (CONF-12), а секрет, попавший в историю, чинится ротацией, а не удалением файла. Кроме того, закоммиченный конфиг конкретной среды становится вторым источником истины: он расходится с тем, что реально развёрнуто, и расходится молча.
CONF-5. Конфиг разбирается один раз при старте
ДОЛЖЕН. Разбор — при старте, в одну типизированную структуру; чтения файла конфигурации в бизнес-коде нет.
ПОЧЕМУ. Второе место чтения — это второй момент времени: две части кода начинают видеть разные значения одного параметра, и расхождение не воспроизводится, потому что зависит от того, когда файл потрогали. Типизированная структура вдобавок переносит ошибку формата в старт (CONF-17), где она видна сразу, а не в первый вызов ветки, которая это поле читает.
CONF-6. Конфиг неизменяем после старта
ДОЛЖЕН. Смена параметров — рестарт процесса.
ПОЧЕМУ. Изменяемый конфиг делает поведение функцией момента: один запрос обслуживается наполовину старыми, наполовину новыми значениями, а разбор инцидента требует знать хронологию правок файла, а не его текущее содержимое.
Горячая перезагрузка — отдельное решение с отдельным обоснованием, а не умолчание.
CONF-7. Умолчания живут в коде
ДОЛЖЕН. Значение по умолчанию задаётся в коде, файл его перекрывает.
ПОЧЕМУ. Умолчание, живущее в образце, действует только для тех, кто образец скопировал: конфиг без этого поля даёт другое поведение, и ни одно из двух значений не выглядит ошибкой. Умолчание в коде — одно определённое поведение для неполного конфига и одно место, где это значение меняется.
CONF-8. Образец перечисляет все поля
ДОЛЖЕН. В образце присутствуют все секции и все поля, включая те, у которых есть умолчание (CONF-7).
ПОЧЕМУ. Поле, живущее только в коде, для читателя конфига не существует: он не знает, что параметр вообще можно менять, и добивается нужного поведения обходным путём. Полнота образца — цена, которой CONF-7 покупает себе видимость.
CONF-9. У каждого поля образца есть комментарий
ДОЛЖЕН. Каждое поле сопровождается комментарием, из которого ясно:
- зачем поле — что оно меняет в поведении;
- диапазон или допустимые значения — перечисление либо границы;
- единицы измерения, если применимо: секунды/миллисекунды, байты, доля
0–1.
ПОЧЕМУ. Так конфиг читается без открывания кода — этим он и полезен; без комментария читатель всё равно идёт в код, и образец перестаёт быть справочником. Единицы стоят отдельно: перепутанные секунды и миллисекунды дают валидное значение и работающий процесс, а ошибка обнаруживается по последствиям — таймаут в тысячу раз не тот.
CONF-10. Обязательность полей определяется дискриминатором type
ДОЛЖЕН. Когда набор полей секции зависит от поля-дискриминатора (выбор бекенда или внешнего сервиса), валидация идёт по его значению:
| № | Значение type |
Валидация |
|---|---|---|
| CONF-10.1 | поддерживаемое | обязательны поля этого варианта; поля прочих вариантов не требуются |
| CONF-10.2 | неизвестное | ошибка на старте с перечислением поддерживаемых значений |
ПОЧЕМУ. Фиксированный на секцию набор обязательных полей оставляет
выбор из двух плохих: заполнять поля бекенда, который не используется, или
не проверять обязательность вовсе — то есть выключить валидацию ровно там,
где вариантов много и ошибиться легче всего. Перечисление поддерживаемых
значений (CONF-10.2) нужно потому, что опечатка в type иначе неотличима от
неподдерживаемого варианта, и за списком приходится идти в код.
CONF-11. Образец показывает все варианты type
СЛЕДУЕТ. Основной вариант предзаполнен рабочими значениями, альтернативные — блоками-комментариями ниже, каждый со своим описанием полей.
ПОЧЕМУ. Иначе набор вариантов виден только из кода валидации, и образец теряет свойство справочника (CONF-8, CONF-9) ровно на той секции, где выбор действительно есть. Закомментированный блок вдобавок переключается правкой на месте, а не сборкой секции с нуля по документации.
CONF-12. Секреты в конфиг приносит деплой
ДОЛЖЕН. Деплой рендерит значения секретов прямо в файл конфигурации; отдельного слоя секретов в приложении нет.
ПОЧЕМУ. Источник истины секрета — внешнее хранилище деплоя, не репозиторий и не окружение. Любой второй канал — переменная окружения рядом с файлом, собственный клиент к хранилищу внутри приложения — возвращает вопрос «откуда взялось значение, которое сейчас в процессе». Приложение при этом остаётся тривиальным: оно читает файл и про секреты не знает ничего особенного.
CONF-13. Рендеренный конфиг — 0600 и владелец-рантайм
ДОЛЖЕН. Права 0600, владелец — пользователь, от имени которого
работает процесс.
ПОЧЕМУ. После CONF-1 и CONF-12 файл конфигурации — единственная поверхность, на которой секреты лежат, и весь довод «файл вместо окружения» держится на его правах: конфиг, читаемый всеми на машине, раздаёт секреты шире, чем раздало бы окружение, — и тогда CONF-1 меняет одну утечку на другую.
CONF-14. В образце секретные поля — пустые строки
ДОЛЖЕН. Значение секретного поля в образце — пустая строка, а не пример.
ПОЧЕМУ. Правдоподобная заглушка доезжает до продакшена как настоящее значение: шаблон отрендерился криво, поле осталось от образца, и проверка непустоты (CONF-15) его пропускает. Пустая строка делает недорендеренный конфиг механически отличимым от заполненного.
CONF-15. Загрузчик проверяет, что обязательные секреты не пусты
ДОЛЖЕН. Непустота обязательных секретов проверяется на старте.
ПОЧЕМУ. Это ловит криво отрендеренный шаблон до того, как он превратится в 401 от внешнего API через час работы, — то есть в момент, когда причина ещё очевидна и связана с деплоем.
CONF-16. Секреты не попадают в логи
НЕ ДОЛЖЕН. Значение секретного поля не появляется в записи лога ни на одном уровне.
ПОЧЕМУ. У логов круг доступа шире, чем у файла под 0600 (CONF-13): они
собираются, пересылаются и попадают в бэкапы, где права исходного файла уже
ничего не значат. Попавший в лог секрет чинится ротацией, а не удалением
записи. Типичный источник утечки — отладочный дамп разобранного конфига при
старте.
CONF-17. Конфиг валидируется на старте, до приёма трафика
ДОЛЖЕН. Невалидный конфиг — запись уровня ERROR и выход с ненулевым
кодом; процесс не стартует «наполовину».
ПОЧЕМУ. Наполовину стартовавший процесс проходит проверку живости и падает позже — на первом запросе, который трогает испорченный параметр, — и падение выглядит дефектом кода, а не ошибкой деплоя. Ненулевой код нужен, чтобы неудачный старт увидел супервизор: без него он неотличим от штатного завершения, и приложение считается развёрнутым.
CONF-18. Минимальный набор проверок
ДОЛЖЕН. Валидация покрывает как минимум:
| № | Что проверяется | Когда всплывёт без проверки |
|---|---|---|
| CONF-18.1 | обязательные поля заданы (непустота секретов — CONF-15) | в ветке, которая это поле читает |
| CONF-18.2 | пути существуют и доступны на запись/чтение по назначению | при первой записи, уже после отчёта об успешном старте |
| CONF-18.3 | числовые диапазоны и единицы: доли, таймауты, счётчики попыток | искажённым поведением без единого сообщения об ошибке |
| CONF-18.4 | строки, которые парсятся во что-то (длительности, зоны, URL, идентификаторы сущностей), реально парсятся | при первом обращении к тому, что за ними стоит |
| CONF-18.5 | включённые секции консистентны: у включённой интеграции заданы все обязательные поля | при первом вызове интеграции, часто по расписанию |
ПОЧЕМУ. Список минимальный и собран по одному признаку — правый столбец: каждая из этих ошибок иначе всплывает там, где связь с деплоем уже потеряна, и диагностируется как дефект приложения. Проверка на старте сводит их все к одному моменту и одному сообщению.
CONF-19. Проблемы конфига показываются разом
ДОЛЖЕН. Валидация собирает все найденные проблемы и выводит их одним списком, а не падает на первой.
ПОЧЕМУ. Иначе цикл «запуск — одна ошибка — правка» повторяется столько раз, сколько в конфиге ошибок, а на сервере каждая итерация — это ещё и деплой. Криво отрендеренный шаблон обычно ломает не одно поле, а все поля одного источника: разом они читаются как одна причина, по одной — как череда несвязанных мелочей.
CONF-21. Значение поля в сообщении валидатора — по признаку секретности
ДОЛЖЕН. Состав сообщения определяется тем же признаком секретности поля, которым уже пользуются CONF-15 и CONF-16:
| № | Поле | В сообщении |
|---|---|---|
| CONF-21.1 | несекретное | имя поля, ожидание и полученное значение: «ожидалось 0–1, получено 1.5» |
| CONF-21.2 | секретное | имя поля и суть нарушения, без значения |
ПОЧЕМУ. Сообщение без значения отправляет читателя в файл — сличать
глазами каждую строку списка CONF-19; ошибки вида «секунды вместо миллисекунд»
или пробел в конце значения из такого сообщения не читаются вовсе. Значение
секретного поля при этом печатать некуда: вывод старта уходит в лог
супервизора и вывод CI, где круг доступа шире прав файла 0600, — тот же
канал утечки, который закрывает CONF-16. Отдельный список «что не печатать»
не заводится: признак один на CONF-15, CONF-16 и CONF-21, а второй список
разошёлся бы с первым — и поле оказалось бы секретным для логов, но
печатаемым валидатором.
Связано
- конвенция
time— формат времени; зона отображения — единственный конфигурируемый параметр времени, семантика описана там. - конвенция
app-directories— конфиг лежит в категории «конфигурация» и доступен приложению только на чтение.