12 июня 2018 г.

Configuration as code

По ходу подготовки своего доклада к Гейзенбагу мне несколько раз приходила в голову мысль: насколько было бы проще проверять конфигурацию, если бы она была не в текстовых файликах, а в java-коде! Насколько меньше всего нужно было бы валидировать — очень многое просто нельзя было бы сделать неправильно, потому что компилятор не пропустил бы.

Сейчас мне кажется, что выносить конфигурацию приложения в текстовые файлы (в том числе xml/json/yaml…) — это сильно переоцененный прием. Настолько переоцененный, что он уже почти антипаттерн. Ну как синглетон — сам по себе вроде неплох, но был период, когда его пользовали где ни попадя, в хвост и гриву. Так же и с конфигурацией в текстовых файлах: якобы это дает гибкость, но эта гибкость на практике оказывается чаще вредной, чем полезной.

И как раз когда я писал тесты для конфигурации — было очень заметно, насколько много в конфигурации лишней гибкости. Выясняется, что:
  1. Чтобы большая по объему конфигурация была управляемой — необходимы тесты
  2. При написании тестов большую часть времени я занимаюсь тем, что “отменяю” всю гибкость вольного формата текстового файла. Изобретаю заново — в тестах — те вещи, которые уже есть в строго-типизированном языке программирования.
Спрашивается: чего бы сразу не описывать конфигурацию на строго-типизированном языке?

Для меня текстовые конфигурации это наследство tutorial-ов, на которых программисты учатся. Когда я по ним учился, то там в обязательном порядке все, что можно, выносилось в .properties-файл. Но в tutorial-е в этот файл попадает 3-5 свойств, а к концу туториала — файлов становится аж 2, для UAT и для PROD. В реальном приложении к третьему году разработки таких файлов 150, свойств в них 10000, и всё горит, и все в аду, но это так привычно, что кажется just a normal amount of hell.

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

Это как с inversion of control/dependency-injection (очень близкая идея, кстати): ведь IoC/DI не равен тождественно spring-app-context.xml. Есть разные контейнеры, и разные форматы описания того, как контейнер должен собрать из компонентов приложение.

Можно и вообще без контейнера: когда я работал в 1С над Матконструктором, я, как продвинутый хипстер, конечно же сплетал компоненты в приложение через spring. Но у матконструктора была версия в виде апплета, и тащить мегабайт .jar-ников спринга в апплет только ради фазы инициализации было за гранью добра. Поэтому (я уже не помню, кому пришла в голову эта гениальная идея) мы написали xslt, которая из spring-контекста генерировала .java-файл, делающий ровно то, что было задекларировано в контексте. Это оказалось довольно просто сделать, так как мы пользовались только базовыми возможностями spring. И хотя в релизной версии приложения не оставалось никаких следов спринга, паттерн dependency injection сохранялся: сгенерированный класс был тем самым компонентом, ответственностью которого является собрать остальные компоненты в целое приложение.

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

Как я себе вижу конфигурацию в коде? Пока не очень четко :) Скорее всего, как DSL-like API для конфигурирования приложения. Java не самый лучший выбор для создания DSL-ей, но даже ее скупыми средствами вполне можно создать такой набор builder-ов, что многие ошибки конфигурации просто не пройдут компиляцию (а еще ведь остаются рантайм-проверки). А если взять какой-нибудь Котлин, то можно написать и вовсе гарные DSL-и. При этом:
  1. Есть проверка типов (у нас же строго типизированный язык!)
  2. Есть готовая вся система типов из доменной модели приложения, и ее можно расширить типами, специфичными для конфигурации.
  3. Есть поддержка IDE: подсвечивание ошибочных элементов, подсказки допустимых, всплывающие подсказки с описанием смысла параметров и их допустимых значений (если вы позаботились об этом при написании АПИ), переименование/рефакторинг
Поддержка IDE — это очень важный элемент, потому что он сильно сокращает время исправления ошибки. Например, есть немало решений для валидации конфигов, но валидация обнаруживает ошибки либо уже в рантайме, либо в тестировании, а ошибки компиляции современные IDE показывают сразу по ходу написания. А чтобы валидацию подхватила IDE — нужно постараться, и написать к ней специальный плагин, причем такой плагин нужен под каждый механизм валидации свой.

Вообще, мне кажется, сейчас эта идея витает в воздухе — возможно, потому, что появился Kotlin, который очень близко интегрируется с java, но при этом имеет отличные возможности для DSL. Вот пара идейно-близких примеров, которые я нашел сходу: Александр Тарасов рассказывает, как он использует kotlin-DSL чтобы конфигурировать эксперименты, а Иван Осипов — как они DSL-ем готовят параметры для параметризованных тестов.

11 комментариев:

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

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

      Удалить
  2. 1. Для изменения пароля к базе или (условно) изменения какого-нибудь времени жизни кэша или протоколов SSL/TLS пересобирать проект?
    2. Как менять конфигурацию коробочного неизменяемого/непересобираемого продукта
    3. Зачем 2 и более конфигов?

    Такое ощущение, что совершенно отсутствует понимание, для чего нужны конфиг файлы

    ОтветитьУдалить
    Ответы
    1. Зачем 2 конфига, я ещё могу понять. Но если конфигов больше десятка, это явный повод задуматься, а всё ли в порядке в проекте-то. Теперь пофантазируем, но из жизни. Положим в один конфиг xml (килобайт на 40+), в другой -- YAML, чтобы с пробелами всё было неочевидно, в третий -- ini.
      Voila! Жизнь эксплуатации стала куда интересней, и это ещё даже без программирования на конфигах.

      А есть ведь ещё lua, его тоже любят пихать везде.

      Удалить
    2. 10 тестовых окружений — это 10 разных __наборов__ конфигов.
      Каждый набор может содержать 10+ разных файлов, которые конфигурируют разные части/модули вашей системы.

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

      Никто не говорит, что код конфигов и код системы это одна единица деплоя.

      Удалить
    3. Да, а в жизни всё это сваливается в одну директорию, которая раскатывается везде. Код интеллектуально поймёт в какой именно конфиг надо смотреть, а человеку в конфиг смотреть не надо.

      Вопрос: чем такой конфиг отличается от кода, если человек его ни найти, ни прочитать не в состоянии, а изменения приезжают только вместе с изменениями в коде?

      Удалить
  3. Звучит хорошо.

    Однако:
    Изменение конфигурации можно сделать прямо налету, даже в проде, если сильно приспичит. На это есть определённые уже отлаженные процессы.
    Изменение кода - только через ченж реквест, со всеми вытекающими - апрувы, Change Advisory Board, и прочие мутные ребята.
    Если это надо быстро - это emergency change, на который надо живой инцидент, а каждый инцидент это изменение KPI. К чему приводит рост количества инцидентов, объяснять думаю не надо..
    Да и обычно для кода нет прямого пути в прод, поэтому - INT->UAT->PROD, а при правильно построенной иерархии ролей это требует участия двух разных команд - девелоперов и саппортеров.

    ОтветитьУдалить
    Ответы
    1. А ничего, что конфигами прод можно положить еще лучше чем кодом?
      Все эти шаги для конфигов тоже нужны их изменение тоже несет риски.

      Наладьте процессы, чтоб и код можно было так же менять. Никто не говорит, что код конфигов и код системы это одна единица деплоя.

      Удалить
  4. A spring's JavaConfig разве не об этом?

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

    ОтветитьУдалить
  6. Проблема текстовых конфигов-лапши в том, что в них выносят даже те параметры, которые никогда не будут меняться.
    С другой стороны, нужен где-то список всех возможных для изменения параметров.

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

    ОтветитьУдалить