• Как я перестал бояться и полюбил конфиг

    Как правило, наибольшего успеха добивается тот, кто располагает лучшей информацией

    Бенджамин Дизраэли

    Лишь совсем простые приложения могут обойтись без файла конфигурации. У остальных всегда найдется, что хранить в конфиге: настройки окружения, настройки логики, часто меняющиеся фрагменты текста, etc. Словом, вопроса «что хранить в конфигурации» обычно не возникает: кандидатов достаточно. А вот вопрос «как хранить конфигурацию» возникает часто и еще чаще решается не самыми оптимальными способами. В этой статье я хотел бы рассмотреть варианты хранения конфигурации приложения, их плюсы и минусы.

    Форматы конфигов

    Ваш собственный формат

    Время от времени, у очередного разработчика загорается над головой лампочка и он понимает, что придумал новый, прекрасный и изящный формат для конфигурационного файла. Лишенный любых недостатков и интуитивно понятный всем, а особенно ему. Так вот, это совсем не так — гасите лампочку немедленно. Любые преимущества нового формата имени вас будут перечеркнуты тем немаловажным фактом, что формат новый, а значит, для него не существует: проработанного стандарта, исключающего детские болезни формата; протестированных парсеров под разные языки; и никто не умеет им пользоваться.

    Плюсы

    • Разрабатывать новые форматы — это весело!

    Минусы

    • Потребуется самостоятельное написание парсера и тестов к нему для каждой используемой платформы
    • Потребуется обучение коллег работе с новым форматом
    • Вы непременно узнаете новые интересные факты о своем детище ближе к финалу проекта
    • Нет встроенной валидации данных (ведь правда?)

    Нативный формат платформы

    Самое простое решение — использовать возможности языка для создания конфига. Например, в PHP это могло бы выглядеть примерно так:

    // config.php
    $config = [
      "foo": "bar",
      "baz": [
        "a": "b",
        "c": "d"
      ]
    ];
    
    if ($config["foo"]) {
      $confif["set"] = true;
    }
    
    $config["calculated"] = calculation();
    
    return $config;

    Невероятно гибкий подход. К сожалению, гибкость часто приводит к дурным последствиям: с одной стороны, я использовал многие возможности языка для того, чтобы облегчить себе жизнь; с другой стороны, это чистое зло в дальнейшей перспективе проекта. Условные конструкции в конфиге вообще не нужны — логика допустима в загрузчике, размазывать её и по конфигу нет никакого смысла. В приведенном выше примере, «set» является, фактически, лишь функцией от «foo», которая уже известна.

    function isFooSet($config)
    {
      return $config["foo"] ? true : null;
    }
    
    $config["set"] = isFooSet($config);

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

    А как быть с вызовом функции? Очевидно, она возвращает некое значение. Не будем заглядывать в функцию сразу, а попробуем прикинуть по описанию, что там может быть? По вызову видно, что функция не принимает никаких параметров. Значит, либо она всегда возвращает одно и то же значение, либо имеет какие-то неявные зависимости. В первом случае, функция в конфиге совершенно точно не нужна, так как может быть заменена сразу на это значение. Во втором случае, конфиг получается зависим от каких-то таинственных сил. В идеале следует стремиться к тому, чтобы конфиг не зависел ни от чего — это краеугольный камень приложения.

    function calculation() {
      return new Datetime();
    }

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

    Плюсы

    • Синтаксис понятен всем, кому известен используемый язык программирования
    • Возможно хранение структур данных любой сложности
    • Это действительно быстро, Флеш гордился бы вами

    Минусы

    • Формат позволяет внедрять в конфиг неявные зависимости
    • Прочитать конфиг с другой платформы скорее всего не получится
    • Нет встроенной валидации данных

    INI

    Представляет собой обычный текстовый файл, нехитрым образом разделенный на секции, ключи и значения. Широко используется для хранения простых конфигурационных данных в программах (например: php.ini, boot.ini). Относительно легко читаем в простых случаях, но при этом не позволяет почти ничего в более сложных. Следует десять раз подумать, прежде чем останавливать свой выбор на нём.

    [config]
    foo=bar
    baz[a]=b
    baz[c]=d

    Плюсы

    • Достаточно широко распространен
    • Легко читается человеком в случае простых конфигов

    Минусы

    • Подходит только для хранения самых простых структур
    • Нет единой спецификации, реализации для разных платформ могут отличаться
    • Нет встроенной валидации данных

    XML

    Дедушка всех конфигов, его величество XML — первый формат, который приходит в голову при размышлениях об универсальных конфигах. В самом деле, сложно представить какой-нибудь язык программирования, в котором еще не реализовано чтение XML. С другой стороны, чтение XML может быть достаточно медитативным занятием — формат совсем не прост. А количество избыточных символов в XML способно вызвать дислексию у слабых духом.

      <MyAwesomeConfig>
        <foo><![CDATA[bar]]></foo>
        <baz a="b" c="d">
      </MyAwesomeConfig>

    Плюсы

    • Широко распространен и всем известен
    • Очень подробная спецификация
    • Встроенная валидация с помощью XSD

    Минусы

    • Формат чрезвычайно многословен — забудьте слово «лаконичность»
    • Программно чтение может быть реализовано достаточно сложным образом

    JSON

    Другой широко известный формат, первоначально предназначен для передачи данных в JavaScript. Немногим уступает по распространенности XML. Любим многими за простой синтаксис и расширяемость.

    {
      "foo": "bar",
      "baz": {
        "a": "b",
        "c": "d"
      }
    }

    Плюсы

    • Широко распространен и всем известен
    • Легко читается как человеком, так и машиной
    • Довольно краток

    Минусы

    • Нет встроенной валидации данных

    YAML

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

    foo: 'bar'
    baz:
      a: 'b'
      c: 'd'

    Плюсы

    • Легко читается человеком
    • Очень краток

    Минусы

    • Пока еще ограниченная поддержка на разных платформах
    • Нет встроенной валидации данных

    Environment

    Не обязательно конфиг должен храниться в отдельном файле. Всё, что нам требуется от конфига — возможность его прочитать. Как конкретно будет реализовано хранилище данных — вопрос десятый, пока оно надежно и не требует дополнительной инициализации (как база данных, например, к которой нужно уметь подключаться, заранее зная её адрес). Одним из подобных вариантов является хранение конфигурации приложения в переменных окружения.

    // read_config.php
    $foo = get_env("FOO");
    $baz_a = get_env("BAZ_A");
    $baz_c = get_env("BAZ_C");

    На первый взгляд, идея кажется не привносящей ничего хорошего в хранение конфигурации. Хранить в ней структуру невозможно, только строковые значения. Как сохранять эти значения тоже неясно: через шелл или отдельный скрипт? Да это же еще хуже, чем INI, бывает ли такое? Пустая затея — если бы не одно но: эта схема отлично ложится на идею контейнерной виртуализации и микросервисов. Например, при запуске контейнера Docker можно указать переменные окружения для процесса:

    # docker-app/docker-compose.yml
    servicedef:
      build: ./build/my-awesome-service
      environment:
        FOO: 'bar'
        BAZ_A: 'b'
        BAZ_C: 'd'

    Контейнер, запущенный с этой конфигурацией docker-compose получил переменные окружения FOO, BAZ_A и BAZ_C. Мы можем создать любое количество контейнеров с разными значениями конфигурации, переданными приложению и для этого нам вовсе не нужны никакие манипуляции с файлами.

    Плюсы

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

    Минусы

    • Можно хранить только строковые значения
    • Нет встроенной валидации данных
    • Не самая простая идея

    В общем, есть из чего выбрать формат. В дальнейших примерах я буду использовать YAML как наиболее современный вариант.

    Организации конфигурации

    Один на всех

    Самый простой подход — один конфиг для всех экземпляров приложения. Так как, по факту, переменные конфига в разных окружениях просто обязаны быть разными, существует некий золотой «эталон» конфига, хранящийся в системе контроля версий с говорящим именем вроде «config.php.example», а также существует правило исключения реального файла конфига «config.php» из хранения в системе версий. Предлагается при установке приложения скопировать из примера конфиг и затем вручную поддерживать его актуальность по мере изменения или обновления приложения.

    ls config
    config.yml config.yml.example
    
    # config/config.yml.example
    foo: "value for foo goes here"
    baz: "value for baz goes here"
    
    # config/config.yml
    foo: "bar"
    baz: "abcd"
    
    # .gitignore
    config/config.yml

    Плюсы

    • Это самый простой способ из всех
    • В системе хранения версий не хранятся важные данные, только примеры
    • Относительно просто установить новый экземпляр приложения

    Минусы

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

    По конфигу на окружение

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

    ls config
    development.yml production.yml playground-for-that-weird-new-guy.yml
    cat config_pointer.yml
    config: development.yml

    Плюсы

    • Не требует ручного обновления конфигурационных файлов при изменении или обновлении
    • Способ относительно прост

    Минусы

    • В хранилище версий могут попасть важные данные! НЕ ДЕЛАЙТЕ ТАК И БУДЬТЕ ВНИМАТЕЛЬНЫ
    • Большая избыточность данных в конфигах, значения хранятся во всех конфигах, даже если они одинаковы
    • Необходимо дополнительно к чтению основного конфига реализовать чтение конфига с указателем на текущее окружение

    Наследуемые конфиги

    Один из минусов прошлого подхода (избыточность) можно убрать, если реализовать механизм наследования для конфигов. Например, шаблоны писем для всех экземпляров приложения лежат в одной и той же директории, но она все равно вынесена в конфиг для удобства будущих изменений. Нет никакого смысла хранить свой указатель на эту директорию для каждого окружения, достаточно одного общего. А вот база данных каждому экземпляру нужна своя собственная.

    ls config
    development.yml production.yml playground-for-that-weird-new-guy.yml base.yml
    
    cat config_pointer.yml
    config: development.yml
    
    # development.yml
    parent: base.yml
    foo: bar
    
    # base.yml
    foo: not_bar
    baz: abcd

    Тогда для окружения «development» будут приняты следующие значения:

    foo: bar
    baz: abcd

    Плюсы

    • Нет избыточности данных
    • Не требует ручного обновления конфигурационных файлов при обновлении или изменении

    Минусы

    • В хранилище версий могут попасть важные данные! НЕ ДЕЛАЙТЕ ТАК И БУДЬТЕ ВНИМАТЕЛЬНЫ
    • Необходимо дополнительно к чтению основного конфига реализовать чтение конфига с указателем на текущее окружение
    • Необходимо дополнительно реализовать механизм наследования (возможно, рекурсивный)
    • Чтение нескольких конфигов медленнее чтения одного конфига (можно решить кешированием)

    Конфиг с заглушками

    На самом деле, не требуется создавать миллион разных файлов с конфигурацией для миллиона установленных экземляров приложения. Как правило, все конфиги сводятся к небольшому набору типовых, отличающихся между собой только несколькими значениями. Нет ведь никакой разницы, какой именно адрес базы данных указан в конфиге приложения — с точки зрения конфига и то, и другое — просто строки. Важен набор настроек, которые вместе меняют логику программы. Например, показывать ли расширенные сообщения об ошибках или использовать ли агрессивное кеширование. Именно вместе, в совокупности, этот набор имеет ценность.

    ls config
    development.yml production.yml base.yml
    
    # development.yml
    parent: base.yml
    foo: %FOO%

    Обратите внимание: больше нет необходимости ни в дополнительном конфиге с указателем текущего окружения, ни в дополнительном конфиге для того-странного-нового-парня. Вместо них можно использовать переменные окружения. Например, переменная окружения MY_AWESOME_APP_ENVIRONMENT будет равна «development.yml», а «FOO» будет равна «baz».

    Плюсы

    • Минимальная избыточность данных
    • Не требует ручного обновления конфигурационных файлов при обновлении или изменении
    • Важные данные можно хранить в переменных состояния, а в системе хранения версий заглушки и менее важные данные

    Минусы

    • Необходимо дополнительно реализовать замену заглушек на соответствуюшие значения из переменных окружения
    • Необходимо дополнительно реализовать механизм наследования (возможно, рекурсивный)
    • Чтение нескольких конфигов медленнее чтения одного конфига (можно решить кешированием)

    Вместо заключения

    Способы организации хранения и работы с конфигурационными файлами не ограничиваются описанными выше. Любой из них можно улучшить. В определенных ситуациях, у того или иного способа могут появиться неожиданные плюсы и минусы — выше разобраны только общие случаи. Не бойтесь экспериментировать и размышлять.

One is the pinnacle of evolution; another one is me

One on the picture is the pinnacle of evolution; another one is me: inspired developer, geek culture lover, sport and coffee addict