Самоконфигурирующиеся приложения
Всем привет.
Внедрение методики непрерывной интеграции уверено шагает по нашей многострадальной родине и всё больше людей проникаются её идеями и концепциями, что очень хорошо. В данной статье я бы хотел рассказать про прием, который использую на одной из стадий непрерывной интеграции — конфигурирования приложений.
Фотку взял с Yaplakal
Проблема
Начнем с описания проблемы. Все мы в своей работе используем несколько сред. Как правило это среда разработки (в статье мы будем называть её Dev), среда тестирования (Stage) и продуктивная среда (Prod). Их, кончено, может быть больше. Например локальные машины разработчкиов и тестировщиков тоже являются средами, если на них развернуты ваши приложения. Вопрос конфигурирования среды — это отдельная большая тема, которую мы не будем рассматривать в рамках этой статьи, а рассмотрим вопрос конфигурирования непосредственно самого приложения. В чем сложность? В том, что все ваши приложения общаются с внешними ресурсами, такими как базы данных, FTP, другие приложения и сервисы, и для каждой среды набор таких ресурсов уникальный. То есть если мы, условно, возьмем и полностью скопируем приложение из одной среды в другую, оно не будет работать корректно или вовсе не будет работать. Для того что бы его запустить, нам нужно вручную править конфигурационные файлы, а это противоречит принципам непрерывной интеграции, которая говорит нам, что не должно быть никаких ручных работ, и что все процессы должны быть автоматизированными. Автоматизировать эту работу можно различными способами. Например, хранить конфигурационные файлы от различных стендов в репозитории и подставлять их при деплое. Тут есть несколько проблем. Во-первых, нужно решить как хранить конфиги от прода, можно использовать отдельный закрытый репозиторий, но как тогда разработчики (или кто вообще) будут туда вносить изменения? Во-вторых, в конфигах, как правило, хранятся не только параметры доступа к внешним ресурсам, но и другие параметры, которые не меняются от стенда к стенду. К тому же у вас наверняка не одно приложение, а много, и что если вам понадобится на каком-то стенде поменять строку подключения к базе, то придется править множество файлов и попробуй вспомни какие именно. В итоге поддержка всех конфигурационных файлов в актуальном состоянии становится очень трудоемкой.
Конечно существуют различные приложения платные и не очень, позволяющие решить проблему конфигураций. Вы вольны выбрать любой удобный для вас способ. Но я бы хотел рассказать о наиболее изящном, на мой взгляд, и простом способе конфигурирования приложения. В плане языков программирования я использую PowerShell и Python, потому что в моем зоопарке используются .NET-приложения, но думаю создать подобную систему для вашего стека не составит труда. Тут важнее идея, а не технология.
Источник вдохновения
Конечно идея не нова и не моя, она прекрасно описана в фундаментальном труде по непрерывной интеграции. Вот несколько принципов управления конфигурациями приложений оттуда:
• Проанализируйте, в какой точке жизненного цикла приложения лучше ввести в него порцию конфигурационной информации — в момент сборки, когда упаковывается релиз-кандидат, во время развертывания или установки, в момент запуска или во время выполнения. Поговорите с администраторами и командой техподдержки, чтобы выяснить их потребности.
• Храните доступные конфигурационные параметры приложения в том же месте, в котором находится исходный код, однако значения храните в другом месте. Жизненные циклы конфигурационных параметров и кода совершенно разные, а пароли и другая секретная информация вообще не должны регистрироваться в системе управления версиями.
• Конфигурирование всегда должно быть автоматическим процессом, использующим значения, извлеченные из хранилища конфигураций, чтобы в любой момент можно было идентифицировать конфигурацию каждого приложения в любой среде.
• Система конфигурирования должна предоставлять приложению (а также сценариям упаковки, установки и развертывания) разные значения в зависимости от версии приложения и среды, в котором оно развернуто. Каждый человек должен иметь возможность легко увидеть, какие конфигурационные параметры доступны для данной версии приложения во всех средах развертывания.
• Применяйте соглашения об именовании конфигурационных параметров. Избегайте непонятных, неинформативных имен. Представьте себе человека, читающего конфигурационный файл без документации. Глядя на имя конфигурационного свойства, он должен понять, для чего оно предназначено.
• Инкапсулируйте конфигурационную информацию и создайте для нее модульную структуру, чтобы изменения в одном месте не повлияли на другие части конфигурации.
• Не повторяйтесь. Определяйте элементы конфигурации таким образом, чтобы каждая концепция была представлена в наборе конфигурационных свойств только один раз.
• Будьте минималистом. Конфигурационная информация должна быть как можно более простой и сосредоточенной на сущности решаемой задачи. Не создавайте ненужных конфигурационных свойств.
• Не усложняйте систему конфигурирования. Как и конфигурационная информация, она должна быть как можно более простой.
• Создайте тесты конфигураций, выполняемые во время развертывания или установки. Проверьте доступность служб, от которых зависит приложение. Применяйте дымовые тесты, дабы убедиться, что каждая функция приложения, зависящая от конфигурационных параметров, правильно воспринимает их.
Отмечу, что в своем решении я не учитываю версию приложения, по крайней мере в явную, на практике мне это не понадобилось, хотя добавить такую возможность не трудно. Так же я не рассматриваю вопрос тестирования конфигураций, так как это отдельная тема.
Реализация
Итак, основные шаги:
- Собираем приложение на сервере интеграции, прогоняем тесты, на выходе получаем рабочее приложение настроенное «по-умолчанию», например на среду Dev;
- Доставляем файлы этого приложения на целевой сервер (или сервера);
- Запускаем процесс конфигурации приложения;
- Запускаем приложение.
Тут важно отметить, что на все стенды надо поставлять одинаковый набор файлов, т.е. изначально все приложения одинаковые, а их настройка происходит уже непосредственно на целевом сервере.
Теперь подробнее про процесс конфигурирования:
- Сервер непрерывной интеграции после доставки приложения на целевой стенд вызывает PowerShell-скрипт Configurator.ps1, который поставляется вместе с приложением. В качестве параметров в него передается путь к файлу изменений Webconfig.json и путь к источнику параметров стенда (подробней чуть ниже).
- Файл изменений располагается в той же директории, что и файл конфигурации приложения Web.config (и в том же репозитории). Содержимое файла — это описание тех тегов и их параметров в конфигурационном файле, которые подлежат изменению.
{ "appName":"MegaApp", "fileName":"Web.config", "changes":[ { "path":"configurations/navigation/sections/add", "filter":"name=OtherApp", "target":"link", "sourceName":"LinkToOtherApp" }, { "path":"configurations/connections/add", "filter":"name=MainDB", "target":"ConnectionString", "sourceName":"MainDBConnectionString" } ] }
appName — название приложения;
fileName — название файла конфигурации приложения, который мы будем изменять;
changes — массив изменений, который необходимо вносить в файл конфигурации при перемещении приложения с одного стенда на другой;
path — путь к тэгу в конфигурационном файле. (В моем случае конфигурационный файл имеет формат xml);
filter — используется когда по указанному пути path имеется несколько одинаковых тэгов, а нам нужно взять конкретный. Мы можем отфильтровать его по значению какого-либо параметра;
target — параметр тега, который мы будем менять;
sourceName — некий псевдоним по которому мы будем определять подставляемое значение из файла настроек стенда.Пример файла Web.config - Теперь про источник параметров стенда. В своем решении я могу использовать в качестве второго параметра скрипта Configuration.ps1 три различных типа значений. Первый — это путь к json-файлу параметров стенда, второй — это URL к web-сервису хранения конфигураций (ConfigStorage), который возвращает json-объект, и третий — путь к ветке реестра на локальной машине в которой содержится URL к ConfigStorage. Фокусом с реестром я решил две задачи: первая — не хотелось хранить URL сервиса на CI-сервере, так как от параметров GET-запроса зависит настройки какого стенда вернет сервис. А вторая — хотелось единообразия команд на CI-сервере, то есть сделать так, что бы «как сконфигурироваться» определял сам целевой сервер, а не сервер непрерывной интеграции.
- Сервис хранения конфигураций (ConfigStorage) я написал на Python. Суть его очень проста: есть json-файлы параметров стендов, которые помещены в определенную директорию:
/ConfigStorage /jsons /stand_dev.json /stand_stage.json /stand_prod.json
Есть конфигурационный файл сервиса:{ "keys_storage":[ { "key":"secret_dev", "pathToFile":"/jsons/stand_dev.json " }, { "key":"secret_stage", "pathToFile":"/jsons/stand_stage.json " }, { "key":"secret_prod", "pathToFile":"/jsons/stand_prod.json " } ] }
Соответственно, что бы получить данные того или иного файла нужно отправить GET-запрос с нужным ключом:http://ConfigStorage/ConfigStorage.py?key=secret_stage
Безопасность в моем случае достигается за счет закрытого сегмента сети и доменной политики, поэтому с шифрованием я не стал заморачиваться, хотя если такая необходимость есть это тоже можно реализовать.Файл параметров стенда выглядит примерно так:
{ "stand":"Stage", "settings":[ { "appName":"default", "sources":[ { "name":"LinkToOtherApp", "value":"http://OtherApp_stage.com" }, { "name":"MainDBConnectionString", "value":"data source=maindb-stage-server;Initial Catalog=MAINDB;User ID=user;Password=pass" } ] }, { "appName":"MegaApp2", "sources":[ { "name":"LinkToOtherApp", "value":"http://OtherApp_stage2.com" } ] } ] }
stand — название стенда;
settings — массив настроек;
appName — название приложения. Здесь смысл вот в чем: по умолчанию во все конфигурационные файлы приложений подставляются значения из раздела где appName равен «default», но если нам для какого-то конкретного приложения нужно иное значение, то мы создаем дополнительный раздел с appName, где переопределяем это значение. В моем примере для приложения MegaApp в тэг с именем OtherApp подставится значениеhttp://OtherApp_stage.com
, а для MegaApp2 — значениеhttp://OtherApp_stage2.com.
sources — массив имен псевдонимов (name) и их значений (value) - И последним шагом в конфигурационный файл приложения Web.config подставляются значения согласно файлу изменений Webconfig.json из файла параметров стенда stand_stage.json.
- Profit.
Зачем все это нужно?
Вот что это дает:
- У вас появляется полное и актуальное описание того, какие параметры приложения изменяются в зависимости от стенда;
- У вас появляется полное и актуальное описание всех параметров всех стендов;
- Поскольку эти описания находятся в системе контроля версий, вы получаете полную историю изменений и причин этих изменений;
- Вы получаете единую точку всех изменений, вносить правки нужно только в одном месте;
- Вы получаете гарантию, что все конфигурационные файлы на всех стендах одинаковые. Любимая отмазка разработчиков «у меня же на компе работает, значит это вы что-то не так законфигурили» перестает работать;
- Плюшка нетехнического характера в том, что проводится четкая линия разделения ответственности сторон. Разработчики отвечают за корректность конфигурационного файла и файла изменений, инженеры за корректность файлов параметров стенда. Водораздел проходит на уровне репозиториев.
- Тестировщики для своих опытов могут создать самые извращенные файлы параметров стендов и моментально переключать все приложения на ту или иную конфигурацию;
- Конечно, наибольшее удовольствие вы получите, когда в вашем зоопарке много различных приложений, которые так или иначе связаны между собой. Я до сих пор растягиваюсь в довольной улыбке, когда вся эта ватага дружным строем принимает нужное мне положение в один клик.
ЗЫ: Исходники, к сожалению, выложить не могу, так как они являются собственностью компании, но на написание статьи я потратил намного больше времени, чем на написание самих скриптов.