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

Я видел много проектов, где конфигурация подкладывается в файловую систему в виде properties/json/xml файлов с непостижимыми уму переоределениями в момент загрузки. И что же на самом деле использует приложение становится ясно только после просмотра лог файлов компонента либо во время отладки.

Скрипт для автоустановки своего Git репозитария можете найти в разделе «Конфигурация в собственном git репозитарии».

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

Чтобы воспользоваться всем этим мощным арсеналом нам надо лишь научиться считывать конфигурацию из git репозитария.

Для этого используем Java класс для считывания конфигурации. Он основан на обертке вокруг hawtio-git (который в свою очередь под капотом использует JGit) и доступен на github git-configuration и в maven central.

Видя конфигурационный хаос вокруг попытаюсь напомню о светлом и прекрасном.
Впервые идея хранения конфигурации в git вдохновила после знакомства с fuse fabric/fabric8.

Хранить конфигурацию в git репозитарии умеет и великолепный проект spring-cloud-config. Но если нам не нужна зависимость на spring и его PropertySource абстракцию, то решение из этой публикации лучше подойдет для приложения на jvm.


Конфигурация на github



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

Java


Configuration.java
package com.github.igorsuhorukov.configuration;

import com.github.igorsuhorukov.codehaus.plexus.util.IOUtil;
import com.github.igorsuhorukov.smreed.dropship.MavenClassLoader;
import io.hawt.git.GitConfigurationHelper;

import java.io.InputStream;

public class Configuration {
    public static void main(String[] args) throws Exception{
        GitConfigurationHelper gitConfigurationHelper = new GitConfigurationHelper("https://github.com/igor-suhorukov/aspectj-scripting.git");
        System.out.println("CONFIGURATION FROM GIT: HEAD REVISON ----------------------------------");
        System.out.println(gitConfigurationHelper.getFileContent("master","pom.xml"));
        System.out.println("CONFIGURATION FROM GIT: BY TAG ----------------------------------------");
        System.out.println(gitConfigurationHelper.getFileContent("master","aspectj-scripting-1.2", "pom.xml"));
        gitConfigurationHelper.destroy();

        System.out.println("CONFIGURATION FROM MAVEN REPO -----------------------------------------");
        InputStream configStream = MavenClassLoader.usingCentralRepo().resolveArtifact("com.github.igor-suhorukov:aspectj-scripting:pom:1.3").openStream();
        System.out.println(IOUtil.toString(configStream));
    }
}


Зависимости, необходимые для работы примера:
<dependency>
    <groupId>com.github.igor-suhorukov</groupId>
    <artifactId>git-configuration</artifactId>
    <version>1.0</version>
</dependency>
<dependency>
    <groupId>com.github.igor-suhorukov</groupId>
    <artifactId>mvn-classloader</artifactId>
    <version>1.4</version>
</dependency>


Для получения конфигурации нужно указать путь к нашему репозитарию:
new GitConfigurationHelper("https://github.com/igor-suhorukov/aspectj-scripting.git")

Возможны варианты
  • Использовать последнюю версию файла из указанного бранча:
    gitConfigurationHelper.getFileContent("master","pom.xml")
    
  • Или получить конкретную версию по тегу (в примере это тег «aspectj-scripting-1.2») или по ревизии:
    gitConfigurationHelper.getFileContent("master","aspectj-scripting-1.2", "pom.xml")
    

И в завершении работы закрыть соединение с репозитарием:
gitConfigurationHelper.destroy()


В качестве приятного дополнения к возможности хранения конфигурации в git — можно считывать конфигурацию и из maven репозитария. Для этого нужна библиотека mvn-classloader:1.4, которая уже входит в состав groovy-grape-aether:2.4.5.2.

URL artifact = MavenClassLoader.usingCentralRepo().resolveArtifact(«com.github.igor-suhorukov:aspectj-scripting:pom:1.3»)
  • usingCentralRepo() — из центрального репозитария.
  • using(repositoryPath) — для вашего корпоративного репозитария.


Дальше работать с артефактом довольно просто. Вызов метода artifact.getFile() позволяет получить ссылку на артефакт в локальной файловой системе, а artifact.openStream() открывает InputStream к этому файлу.

В каком формате хранить конфигурацию и чем разбирать эти файлы дальше — зависит от задачи и предпочтений и решать вам. Конфигурация может представлять из себя как простой java.util.Properties, так и Json(Gson/jackson), JAXB/XStream, либо конфигурацию в виде скриптов на groovy, scala, javascript…

Groovy


То же самое из прошлого заголовка про java гораздо лаконичнее записывается в groovy. Запустить пример можно командой:
java -jar groovy-grape-aether-2.4.5.2.jar readGitConfig.groovy

readGitConfig.groovy
import com.github.igorsuhorukov.smreed.dropship.MavenClassLoader
@Grab(group = 'com.github.igor-suhorukov', module = 'git-configuration', version = '1.0')
import io.hawt.git.GitConfigurationHelper

def gitConfigurationHelper = new GitConfigurationHelper('https://github.com/igor-suhorukov/aspectj-scripting.git')
println 'CONFIGURATION FROM GIT: HEAD REVISON ----------------------------------'
println gitConfigurationHelper.getFileContent('master','pom.xml')
println 'CONFIGURATION FROM GIT: BY TAG ----------------------------------------'
println gitConfigurationHelper.getFileContent('master','aspectj-scripting-1.2', 'pom.xml')
gitConfigurationHelper.destroy()

println 'CONFIGURATION FROM MAVEN REPO -----------------------------------------'
def configStream = MavenClassLoader.usingCentralRepo().resolveArtifact('com.github.igor-suhorukov:aspectj-scripting:pom:1.3').openStream()
println configStream.text


Конфигурация в собственном git репозитарии



В качестве in-house репозитария выбрал Gitblit — для знающих java его проще установить и поддерживать, а механизм push hooks на Groovy позволяет творить чудеса. В качестве альтернативы можно выбрать Gogs — Go Git Service или любой другой сервер Git репозитариев.

Запуск и конфигурацию Gitblit сервера выполним с помощью groovy скрипта автоустановки.

java -jar groovy-grape-aether-2.4.5.2.jar gitblit.groovy

gitblit.groovy
import com.github.igorsuhorukov.smreed.dropship.MavenClassLoader
@Grab(group='org.codehaus.plexus', module='plexus-archiver', version='2.10.2')
import org.codehaus.plexus.archiver.zip.ZipUnArchiver
@Grab(group='org.codehaus.plexus', module='plexus-container-default', version='1.6')
import org.codehaus.plexus.logging.console.ConsoleLogger
@Grab(group = 'org.eclipse.jetty', module = 'jetty-runner', version = '9.3.7.RC1' )
import org.eclipse.jetty.runner.Runner

def gitblit = new File(MavenClassLoader.using('http://gitblit.github.io/gitblit-maven').resolveArtifact('com.gitblit:gitblit:war:1.7.1').getFile())

File gitblitDirectory = new File(System.getProperty('user.home'), gitblit.getName().replace('.war',''))

if(!gitblitDirectory.exists()){
    gitblitDirectory.mkdir()
    ZipUnArchiver unArchiver = new ZipUnArchiver()
    unArchiver.setSourceFile(gitblit)
    unArchiver.enableLogging(new ConsoleLogger(ConsoleLogger.LEVEL_DEBUG,"Logger"))
    unArchiver.setDestDirectory(gitblitDirectory)
    unArchiver.extract()

    def dataPath = new File(System.getProperty('user.home'), '.gitblit_data')
    if(!dataPath.exists()){ dataPath.mkdir() }
    def webXml = new File(gitblitDirectory.getAbsoluteFile(), 'WEB-INF/web.xml')
    webXmlText = webXml.text
    webXml.withWriter { w -> w << webXmlText.replace('${contextFolder}/WEB-INF/data', dataPath.getAbsolutePath()) }
}

Runner.main([gitblitDirectory] as String[])


Скрипт скачивает gitblit.war из репозитария проекта, распаковывает его в домашнюю директорию пользователя и заменяет в конфигурации gitblit путь к хранилищу репозитариев. После этого запускает jetty сервер и gitblit внутри него.



Приведу последовательность команд, которые использовал для создания конфигурации в новом репозитарии ssh://admin@localhost:29418/configuration.git

vim configuration.properties
git add configuration.properties 
git commit -m "import config"
git tag -a ver1.0 -m "configuration version 1.0"
vim configuration.properties
git add configuration.properties 
git commit -m "update config"
git push -u origin master --tags


Код чтения конфигурации ничем не отличается от предыдущих примеров. Остается только создать в коде объект:
new GitConfigurationHelper("http://admin@localhost:8080/r/configuration.git")

И использовать тот же API, что и в github примере.



Итоги



В публикации мы рассмотрели как считывать конфигурацию в java/groovy из git и maven репозитариев. С помощью groovy скрипта смогли установить и сконфигурировать gitblit репозитарий в автоматическом режиме.

Код из статьи применим в jvm проектах и было бы интересно услышать от вас как эта же задача решается в других языках программирования и на других платформах.

Комментарии (11)


  1. Fen1kz
    27.01.2016 16:20
    +1

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


    1. igor_suhorukov
      27.01.2016 17:07
      +1

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

      В случае отдельных конфигураций для разных типов приложения (dev, test, uat, prod) или переопределение одного «суперконфига» в каждом процессе каждого узла сети.

      Если я слишком абстрактно описал подход, могу на примерах показать как это можно решить.


      1. Fen1kz
        27.01.2016 20:11

        У нас используется такой подход:
        в репозитории есть application.properties.example со строками вида:
        database.url=postresql/блабла
        database.username=я

        сам application.properties, естественно, заигнорен

        Когда приходит новый разработчик, он делает cp application.properties.example application.properties и вбивает в полученный файл свои данные.

        Как такое сделать при вашем методе? Да, без примеров, если честно, тяжеловато.


        1. igor_suhorukov
          27.01.2016 23:22

          Как пример для случая отдельных конфигураций для разных типов приложения (dev, test, uat, prod)

          def envName = System.getenv("environment_name")
          println gitConfigurationHelper.getFileContent('master',"settings-$envName.json")
          


          1. intenter
            28.01.2016 12:09

            Обычно, все гораздо сильнее усложняется в больших организациях на больших распределенных проектах. Конфигурация получается «многомерной». Например:
            Деление по environment: dev, qa, uat, prod
            Деление по регионам: eu, us, asia, etc…
            Деление по разработчикам внутри dev: mike, john, etc
            Деление по версиям системы (если несколько версий могут быть активны одновременно)
            Деление по клиентам, и т.д.

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


            1. igor_suhorukov
              28.01.2016 12:21

              Согласен, что конфигурация может быть достаточно сложной и динамической. В один прекрасный момент сложность конфигурирования может стать такой, что template engine не спасет и нужны динамически вычисляемые значения. Так не проще ли использовать динамическую конфигурацию на основе скриптового языка: groovy, javascript, lua… !?

              Git — лишь как слой для хранения конфигурации, предлагающий много возможностей, готовых инструментов и знакомый разработчикам!
              Можно хранить конфигурацию в etcd, zookeeper… fabric8.io в итоге ушли с zookeeper на git


        1. igor_suhorukov
          28.01.2016 08:59

          Когда приходит новый разработчик, он делает cp application.properties.example application.properties и вбивает в полученный файл свои данные.


          Если конфигурация специфична для каждого пользователя, то fork, правка, commit и push


  1. reality
    27.01.2016 19:01
    +2

    Ужас какой


    1. igor_suhorukov
      27.01.2016 23:23

      Где ужас?) Можно подробности?


  1. M_Muzafarov
    28.01.2016 17:36

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


    1. igor_suhorukov
      28.01.2016 17:47

      Это лишь пример. Можно настроить и аутентификацию.
      Хранить пароли в конфигурации не зашифрованном виде — моветон. Любой аудит порвет за такое как тузик грелку