GrabDuck

git и конфигурационные файлы

:

На эту тему высказались авторы манифеста 12 factors.

Вот что они пишут о конфигурации:

Лакмусовой бумажкой того, правильно ли разделены конфигурация и код приложения, является факт того, что кодовая база приложения может быть в любой момент открыта в свободный доступ без компрометации каких-либо приватных данных.

Далее:

Другим подходом к конфигурации является использование конфигурационных файлов, которые не сохраняются в систему контроля версий, например, config/database.yml в Rails. Это огромное улучшение перед использованием констант, которые сохраняются в коде, но по-прежнему и у этого метода есть недостатки: легко по ошибке сохранить конфигурационный файл в репозиторий; существует тенденция, когда конфигурационные файлы разбросаны в разных местах и в разных форматах, из за этого становится трудно просматривать и управлять всеми настройками в одном месте.

Это как раз способ, который описан в вопросе. Авторы манифеста предлагают такое решение:

Приложение двенадцати факторов хранит конфигурацию в переменных окружения (часто сокращается до env vars или env). Переменные окружения легко изменить между развёртываниями, не изменяя код; в отличие от файлов конфигурации, менее вероятно случайно сохранить их в репозиторий кода; и в отличие от пользовательских конфигурационных файлов или других механизмов конфигурации, таких как Java System Properties, они являются независимым от языка и операционной системы стандартом.

Естественно, всё это применимо в основном к веб-приложениям. Для десктопных и мобильных приложений эти правила уже не подходят.

От себя добавлю, что Azure-приложения ASP.NET и ASP.NET WebAPI сейчас настраиваются именно так: в панели приложения на вкладке Application settings можно указать переменные окружения. Тоже самое в Heroku.

Пример

В нашем проекте требуется рассылать электронные письма и СМС. Естественно, на локальных контурах разработчиков и на общем контуре разработки ничего рассылать не надо, но при этом нужен доступ к содержимому уведомлений. То есть разработчики должны видеть, что служба уведомлений сработала, и видеть, что именно будет отправлено клиенту на боевом контуре.

Поскольку мы используем внедрение зависимостей, мы сделали несколько реализаций классов, рассылающих уведомления. Продуктовая реализация осуществляет отправку писем, а реализация разработчиков пишет уведомления в файл. В качестве библиотеки IoC мы используем Autofac, который позволяет регистрировать зависимости в конфигурационном файле, так что в нашем Web.config была зарегистрирована служба уведомлений для разработчиков.

MSBuild умеет трансформировать конфигурационные файлы ASP.NET проектов во время развёртывания. Если в вашей папке находятся файлы Web.config и Web.Release.config, при развёртывании проекта в конфигурации Release MSBuild применит трансформации из Web.Release.config к Web.config. Можно менять атрибуты разделов, добавлять и удалять подразделы. Мы у себя меняли регистрируемый класс, так что на контуре Release запускалась реальная служба уведомлений вместо отладочной.

Тогда нас такое решение устроило.

Некоторое время всё работало хорошо, но потом вышла 4-я версия Autofac, которая стала совместима с новой системой конфигурирования .NET. При этом разработчики Autofac выпилили поддержку старого способа, то есть старых добрых Web.config и App.config. При этом MSBuild не умеет автоматически трансформировать новые файлы конфигурации.

Пришлось переделать схему. Теперь для каждого контура мы стали хранить свою версию конфигурации IoC в файлах IoC.Dev.json, IoC.Stage.json, IoC.Release.json. Загрузка нужного файла конфигурации стала осуществляться так:

Startup.cs

var configName = Environment.GetEnvironmentVariable("APPSETTING_CONFIG_NAME") ?? "Dev";
var config = new ConfigurationBuilder().AddJsonFile($"IoC.{configName}.json", optional: true, reloadOnChange: true);
var module = new ConfigurationModule(config.Build());
builder.RegisterModule(module);

Это было стихийное решение, которое мы обнаружили в Google и смогли применить к нашему Azure-проекту. Однако это ещё не идеал с точки зрения 12-тифактороного приложения. Часть настройки действительно вынесена в переменную окружения, но часть находится в файлах IoC.*.json.

Что не так? Администратор системы может решить, что файлы IoC.Dev.json, IoC.Stage.json, IoC.Release.json можно спокойно менять, хотя на самом деле мы довольно суровы относительно них. Нам не нужна здесь универсальность и гибкость, мы бы хотели ограничить настройку двумя вариантами: а) шлём уведомления; б) складываем уведомления в секретное место.

Так что мы можем захардкодить эти две стратегии и при старте приложения выбирать ту из них, которая указана в переменной окружения:

var notifyStrategy = Environment.GetEnvironmentVariable("APPSETTING_NOTIFY_STRATEGY");
switch (notifyStrategy)
{
case "send":
    builder.RegisterType<ReadNotifier>().As<INotifier>();
    break;

case "save":
    builder.RegisterType<FakeNotifier>().As<INotifier>();
    break;

default:
    throw new ArgumentException("Ну всё теперь.", nameof(notifyStrategy));
}

Теперь администратор приложения может конфигурировать его на своём уровне погружения. Он не сломает ничего важного в дебрях XML/JSON IoC. В результате нам удалось полностью избавиться от файлов конфигурации на этом уровне и стать ближе к идеалам 12-тифакторных приложений.

Исходя из этого, я бы советовал:

  1. Все секретные настройки, включая строки подключения к базам данных, логины и пароли для отправки писем и прочее, брать из переменных окружения. Развёртывание приложения свести к запуску одной команды (для .NET это MSBuild). Переменные окружения и процесс развёртывания описать в README.md.

  2. Внедрение зависимостей реализовывать непосредственно в коде, предоставляя несколько стратегий, которыми будет управлять администратор. Он скажет вам спасибо, если ему не придётся изучать детали приложения, и он сможет одной настройкой внедрить совершенно другой набор из трёх-пяти-десяти типов, не разбираясь в их взаимосвязях.

  3. Некоторые конфигурационные файлы на самом деле представляют из себя декларативную часть кода и не требуют изменения после развёртывания. Детали зависит от языка, думаю, что чаще это встречается в интерпретируемых языках. Такие файлы мы не должны считать истинно конфигурационными и можем оставить их в проекте как есть.

  4. Конфигурацию, которую может менять администратор и которая может сохраняться между развёртываниями, вынести в БД. Можно и в файл, но в этом случае он может быть уничтожен при неаккуратном развёртывании. Значения в базе или в файле, если их нет при первом запуске системы, прописываются стандартные из кода.