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

Фотку взял с Yaplakal
Начнем с описания проблемы. Все мы в своей работе используем несколько сред. Как правило это среда разработки (в статье мы будем называть её Dev), среда тестирования (Stage) и продуктивная среда (Prod). Их, кончено, может быть больше. Например локальные машины разработчкиов и тестировщиков тоже являются средами, если на них развернуты ваши приложения. Вопрос конфигурирования среды – это отдельная большая тема, которую мы не будем рассматривать в рамках этой статьи, а рассмотрим вопрос конфигурирования непосредственно самого приложения. В чем сложность? В том, что все ваши приложения общаются с внешними ресурсами, такими как базы данных, FTP, другие приложения и сервисы, и для каждой среды набор таких ресурсов уникальный. То есть если мы, условно, возьмем и полностью скопируем приложение из одной среды в другую, оно не будет работать корректно или вовсе не будет работать. Для того что бы его запустить, нам нужно вручную править конфигурационные файлы, а это противоречит принципам непрерывной интеграции, которая говорит нам, что не должно быть никаких ручных работ, и что все процессы должны быть автоматизированными. Автоматизировать эту работу можно различными способами. Например, хранить конфигурационные файлы от различных стендов в репозитории и подставлять их при деплое. Тут есть несколько проблем. Во-первых, нужно решить как хранить конфиги от прода, можно использовать отдельный закрытый репозиторий, но как тогда разработчики(или кто вообще) будут туда вносить изменения? Во-вторых, в конфигах, как правило, хранятся не только параметры доступа к внешним ресурсам, но и другие параметры, которые не меняются от стенда к стенду. К тому же у вас наверняка не одно приложение, а много, и что если вам понадобится на каком-то стенде поменять строку подключения к базе, то придется править множество файлов и попробуй вспомни какие именно. В итоге поддержка всех конфигурационных файлов в актуальном состоянии становится очень трудоемкой.
Конечно, существуют различные приложения платные и не очень, позволяющие решить проблему конфигураций. Вы вольны выбрать любой удобный для вас способ. Но я бы хотел рассказать о наиболее изящном, на мой взгляд, и простом способе конфигурирования приложения. В плане языков программирования я использую PowerShell и Python, потому что в моем зоопарке используются .NET-приложения, но думаю создать подобную систему для вашего стека не составит труда. Тут важнее идея, а не технология.
Конечно идея не нова и не моя, она прекрасно описана в фундаментальном труде по непрерывной интеграции. Вот несколько принципов управления конфигурациями приложений оттуда:
Отмечу, что в своем решении я не учитываю версию приложения, по крайней мере в явную, на практике мне это не понадобилось, хотя добавить такую возможность не трудно. Так же я не рассматриваю вопрос тестирования конфигураций, так как это отдельная тема.
Итак, основные шаги:
Тут важно отметить, что на все стенды надо поставлять одинаковый набор файлов, т.е. изначально все приложения одинаковые, а их настройка происходит уже непосредственно на целевом сервере.
Теперь подробнее про процесс конфигурирования:

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

Фотку взял с 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<?xml version="1.0" encoding="utf-8"?> <configuration> <navigation> <sections> <add name="OtherApp" text="Приложение1" link="http://OtherApp_dev.com" /> <add name="NotChangeApp" text="Хабр" link="http://habrahabr.ru" /> </sections> </navigation> <connectionStrings> <add name="MainDb" connectionString="data source=maindb-server;Initial Catalog=MAINDB;User ID=user;Password=pass" providerName="System.Data.SqlClient" /> <add name="NotChangeDb" connectionString="data source=localhost;Initial Catalog=DB;User ID=user;Password=pass;" providerName="System.Data.SqlClient" /> </connectionStrings> </configuration>
- Теперь про источник параметров стенда. В своем решении я могу использовать в качестве второго параметра скрипта 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.
Зачем все это нужно?
Вот что это дает:
- У вас появляется полное и актуальное описание того, какие параметры приложения изменяются в зависимости от стенда;
- У вас появляется полное и актуальное описание всех параметров всех стендов;
- Поскольку эти описания находятся в системе контроля версий, вы получаете полную историю изменений и причин этих изменений;
- Вы получаете единую точку всех изменений, вносить правки нужно только в одном месте;
- Вы получаете гарантию, что все конфигурационные файлы на всех стендах одинаковые. Любимая отмазка разработчиков «у меня же на компе работает, значит это вы что-то не так законфигурили» перестает работать;
- Плюшка нетехнического характера в том, что проводится четкая линия разделения ответственности сторон. Разработчики отвечают за корректность конфигурационного файла и файла изменений, инженеры за корректность файлов параметров стенда. Водораздел проходит на уровне репозиториев.
- Тестировщики для своих опытов могут создать самые извращенные файлы параметров стендов и моментально переключать все приложения на ту или иную конфигурацию;
- Конечно, наибольшее удовольствие вы получите, когда в вашем зоопарке много различных приложений, которые так или иначе связаны между собой. Я до сих пор растягиваюсь в довольной улыбке, когда вся эта ватага дружным строем принимает нужное мне положение в один клик.
ЗЫ: Исходники, к сожалению, выложить не могу, так как они являются собственностью компании, но на написание статьи я потратил намного больше времени, чем на написание самих скриптов.
