Статьи

Docker для разработчиков Java: непрерывная интеграция в Docker

Эта статья является частью нашего курса Академии под названием Docker Tutorial для разработчиков Java .

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

1. Введение

В этом руководстве мы увидели, как Docker проникает во все аспекты типичного жизненного цикла Java- приложений: сборку, разработку, тестирование и развертывание. В этой части мы сосредоточимся на одной из все более важных тем непрерывной интеграции.

Хотя рынок решений для непрерывной интеграции (и непрерывной доставки) переполнен, к лучшему или худшему, существует один продукт, который стоит годами и очень близок сердцу каждого разработчика Java . Да, ваше предположение верно, это Jenkins , ведущий сервер автоматизации с открытым исходным кодом.

2. Почему Дженкинс?

Для многих отношения с Дженкинсом подпадают под категорию ненависти или любви. Оставив историю позади, второе поколение платформы Jenkins , а именно ветка релиза 2.x , поразило экосистему, как торнадо, и буквально изменило правила игры.

Чтобы подчеркнуть, вот ключевые особенности Jenkins, которые отличают его от многих других конкурирующих решений:

  • Встроенная поддержка конвейеров доставки (также известный как «конвейер как код»)
  • Улучшенный интерфейс и удобство использования
  • Очень прост в установке и настройке
  • Исключительная расширяемость и очень богатая экосистема плагинов
  • Распределенное развертывание
  • автоматизация

Чтобы проиллюстрировать большинство из них в действии, мы собираемся применить на практике то, что мы узнали о Docker, и создадим собственный имидж Jenkins . Мало того, мы интегрировали бы его с Git (используя чрезвычайно популярный хостинг Github ), настроили инструментальные средства, создали многоотраслевой конвейер для нашего приложения Spring Boot, которое мы разработали ранее . И, наконец, что не менее важно, мы собираемся сделать все это полностью автоматизированным способом. Если это звучит захватывающе, давайте начнем.

3. Трубопровод как код

Одна из последних тенденций в отрасли — рассматривать ваши конвейеры непрерывной доставки как код, хранить их в системе контроля версий и, при необходимости, создавать версии. Если вы хотите внедрить что-то подобное в свои проекты, Дженкинс — идеальная пара, без сомнения.

Существует множество преимуществ конфигураций конвейера, которые Jenkins поддерживает «из коробки». Единственное, что мы будем использовать в этой части руководства, — это сохранить определение конвейера Jenkinsfile вместе с вашим проектом в корне дерева репозитория.

Как вы, наверное, помните , наше приложение Spring Boot требует Java и использует Gradle для управления зависимостями. Имея это в виду, вот пример, который мы могли бы придумать как простой конвейер для создания такого проекта в Дженкинсе .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
pipeline {
  agent any
 
  options {
    disableConcurrentBuilds()
    buildDiscarder(logRotator(numToKeepStr:'5'))
  }
 
  triggers {
    pollSCM('H/15 * * * *')
  }
 
  tools {
    jdk "jdk-8u162"
  }
 
  environment {
    JAVA_HOME="${jdk}"
  }
 
  stages {
    stage('Cleanup before build') {
      steps {
        cleanWs()
      }
    }
 
    stage('Checkout from Github') {
      steps {
        checkout scm
      }
    }
 
    stage('Build') {
      steps {
        script {
          def rtGradle = Artifactory.newGradleBuild()
          rtGradle.tool = "Gradle 4.3.0"
          rtGradle.run buildFile: 'build.gradle', tasks: 'clean build'
        }
      }
    }
  }
 
  post {
    always {
      archiveArtifacts artifacts: 'build/libs/*.jar', fingerprint: true
    }
  }
}

Надеюсь, вы согласитесь, это удивительно чисто и выразительно. Единственное, что нам нужно сделать, — это совместить это определение с данным проектом.

Если вы хотите увидеть наше развертывание Jenkins в действии, сейчас было бы неплохо создать новый репозиторий Github (или Bitbucket , Gitlab ,…) с приложением Spring Boot внутри.

4. Дженкинс и Докер

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

Что интересно, у Дженкинса также есть всесторонняя поддержка Docker (как часть объявлений конвейера и определений заданий), и мы также собираемся рассмотреть это.

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

1
FROM jenkins/jenkins:lts

Хорошее начало, но было бы здорово предварительно установить некоторые плагины, которые, по нашему мнению, будут полезны для нас. Это легко сделать с помощью уже установленного скрипта install-plugins.sh .

1
RUN /usr/local/bin/install-plugins.sh docker-plugin docker-slaves workflow-scm-step workflow-cps  pipeline-model-definition docker-workflow cloudbees-folder timestamper workflow-aggregator git gradle pipeline-maven artifactory maven-plugin ssh-slaves build-timeout pipeline-stage-view  antisamy-markup-formatter mailer matrix-auth junit findbugs maven-invoker-plugin pipeline-build-step credentials ws-cleanup email-ext ldap pam-auth subversion blueocean credentials-binding

Честно говоря, в этом списке достаточно много плагинов, но это лишь малая часть того, что доступно. Экосистема плагинов вокруг Jenkins просто поражает. На этом этапе нам нужно сгенерировать пару ключей для использования с репозиторием Github (или Bitbucket , Gitlab ,…).

1
ssh-keygen -t rsa -C jenkins@javacodegeeks.com

И скопируйте эту пару ключей на наше изображение:

1
2
COPY github/id_rsa /var/jenkins_home/.ssh/
COPY github/id_rsa.pub /var/jenkins_home/.ssh/

Двигаясь дальше, мы, безусловно, стремимся запустить Jenkins, используя защищенный протокол HTTPS , поэтому нам нужно будет создать самозаверяющий сертификат вместе с закрытым ключом.

1
2
openssl req  -x509  -sha256  -newkey rsa:2048  -keyout jenkins.key  -out jenkins.crt  -days 1024  -nodes
openssl rsa -in jenkins.key -out jenkins.rsa

Добавим также сгенерированные файлы к изображению:

1
2
COPY jenkins/jenkins.crt /var/lib/jenkins/cert
COPY jenkins/jenkins.rsa /var/lib/jenkins/pk

Круто, мы почти закончили с частью окружающей среды. Давайте завершим это с парой переменных среды.

1
2
3
ENV JENKINS_SLAVE_AGENT_PORT 50001
ENV JAVA_OPTS -Djenkins.install.runSetupWizard=false
ENV JENKINS_OPTS --httpPort=-1 --httpsPort=8083 --httpsCertificate=/var/lib/jenkins/cert --httpsPrivateKey=/var/lib/jenkins/pk

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

1
2
ENV JENKINS_USER admin
ENV JENKINS_PASS ef8a7543087c4b999cbecdd57696f557

Кроме того, было бы хорошо узнать URL-адрес для подключения к Git-совместимому репозиторию и получить проект, который мы собираемся построить.

1
ENV GIT_URL "<Git URL to repo>"

Далее идут важные, но не очень очевидные. Нам потребуется имя пользователя и пароль, чтобы иметь возможность загрузить официальный дистрибутив Oracle JDK (как мы все помним, лицензионное соглашение должно быть принято до того, как это сделать).

1
2
ENV ORACLE_JDK_USER "<username>"
ENV ORACLE_JDK_PASSWORD "<password>"

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

Несложный пример сценария будет хорошей отправной точкой для понимания основной идеи автоматизации Jenkins , а вот init.groovy .

1
2
3
def instance = Jenkins.getInstance()
instance.setNumExecutors(5)
instance.setSlaveAgentPort([55001])

Это не выглядит страшно на всех. Постепенно добавляя больше мяса, давайте посмотрим на следующий скрипт, который настраивает параметры безопасности для доступа к экземпляру Jenkins , security.groovy .

01
02
03
04
05
06
07
08
09
10
11
// Get system environment
def env = System.getenv()
def instance = Jenkins.getInstance()
instance.setSecurityRealm(new HudsonPrivateSecurityRealm(false))
instance.setAuthorizationStrategy(new GlobalMatrixAuthorizationStrategy())
 
def user = instance.getSecurityRealm().createAccount(env.JENKINS_USER, env.JENKINS_PASS)
user.save()
 
instance.getAuthorizationStrategy().add(Jenkins.ADMINISTER, env.JENKINS_USER)
instance.save()

Вы также можете заметить, как мы используем переменные среды JENKINS_USER и JENKINS_PASS из определения Dockerfile выше. В дальнейшем мы собираемся сохранить закрытый ключ (который мы ранее скопировали в образ), чтобы получить доступ к SCM по нашему выбору, записанному в скрипте credentials.groovy .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
def instance = Jenkins.getInstance()
String keyfile = "/var/jenkins_home/.ssh/id_rsa"
def domain = Domain.global()
def store = instance.getExtensionList('com.cloudbees.plugins.credentials.SystemCredentialsProvider')[0].getStore()
 
def credentials = new BasicSSHUserPrivateKey(
     CredentialsScope.GLOBAL,
     "22d9d94b-2794-4d0c-8576-accf87764d0f",
     "jenkins",
     new BasicSSHUserPrivateKey.FileOnMasterPrivateKeySource(keyfile),
     "",
     ""
)
 
store.addCredentials(domain, credentials)

Поскольку мы решили сосредоточиться на приложении Spring Boot и для этого уже создали Jenkinsfile , было бы неплохо предварительно установить Oracle JDK 8 (последняя версия 8u162 ) и инструменты Gradle (мы используем версию 4.3.0 , хотя не последний, но давайте придерживаться этого). Фрагмент jdk.groovy заботится о конфигурации Oracle JDK .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
def env = System.getenv()
def instance = Jenkins.getInstance()
def descriptor = instance.getDescriptor("hudson.model.JDK")
def installations = []
def installer = new JDKInstaller("jdk-8u162-oth-JPR", true)
def installerProps = new InstallSourceProperty([installer])
def installation = new JDK("jdk-8u162", "", [installerProps])
installations.push(installation)
 
descriptor.setInstallations(installations.toArray(new JDK[0]))
descriptor.save()
 
def jdkInstaller = instance.getDescriptor("hudson.tools.JDKInstaller")
jdkInstaller.doPostCredential(env.ORACLE_JDK_USER, env.ORACLE_JDK_PASSWORD)

Обратите внимание на обязательный шаг для настройки имени пользователя и пароля для загрузки JDK (с использованием переменных среды ORACLE_JDK_USER и ORACLE_JDK_PASSWORD ). С другой стороны, gradle.groovy выглядит намного проще.

1
2
3
4
5
def instance = Jenkins.getInstance()
def gradle = new GradleInstallation("Gradle 4.3.0", "", [new InstallSourceProperty([new GradleInstaller("4.3.0")])])
def descriptor = instance.getDescriptorByType(GradleInstallation.DescriptorImpl)
descriptor.setInstallations(gradle)
descriptor.save()

Потрясающе пока, но самый интересный сценарий остался в конце. Это конфигурация конвейера, хранящаяся в pipeline.groovy .

1
2
3
4
5
6
def env = System.getenv()
def instance = Jenkins.getInstance()
 
def project = instance.createProject(WorkflowMultiBranchProject, 'spring-boot-webapp-pipeline')
GitSCMSource gitSCMSource = new GitSCMSource(null, env.GIT_URL, "22d9d94b-2794-4d0c-8576-accf87764d0f", "*", "", false)
project.getSourcesList().add(new BranchSource(gitSCMSource))

Обратите внимание, как мы 22d9d94b-2794-4d0c-8576-accf87764d0f на учетные данные, предварительно настроенные в credentials.groovy , используя уникальный идентификатор 22d9d94b-2794-4d0c-8576-accf87764d0f . URL-адрес Git- репозитория GIT_URL из переменной среды GIT_URL .

И в качестве последнего шага, давайте скопируем все сценарии в наш образ, чтобы дать Дженкинсу возможность выполнить их на этапе инициализации.

1
2
3
4
5
6
COPY scripts/init.groovy /usr/share/jenkins/ref/init.groovy.d/init.groovy
COPY scripts/credentials.groovy /usr/share/jenkins/ref/init.groovy.d/credentials.groovy
COPY scripts/pipeline.groovy /usr/share/jenkins/ref/init.groovy.d/pipeline.groovy
COPY scripts/security.groovy /usr/share/jenkins/ref/init.groovy.d/security.groovy
COPY scripts/jdk.groovy /usr/share/jenkins/ref/init.groovy.d/jdk.groovy
COPY scripts/gradle.groovy /usr/share/jenkins/ref/init.groovy.d/gradle.groovy

Удивительно, единственное, что мы должны сделать вручную (но только один раз), это разрешить github/id_rsa.pub доступ только для чтения к вашему Github- хранилищу (или Bitbucket , Gitlab ,…). Когда это будет сделано, мы сможем создать изображение:

1
docker build . --tag jenkins:jcg

И запустить наш контейнер Дженкинс прямо сейчас.

1
docker run --rm -p 8083:8083 -p 50001:50001 -v /var/run/docker.sock:/var/run/docker.sock --privileged jenkins:jcg

После запуска контейнера мы можем перейти к https: // localhost: 8083 и использовать имя пользователя admin и пароль ef8a7543087c4b999cbecdd57696f557 для входа в систему, сканирования конвейера, ожидания сборки и проверки состояния задания spring-boot-webapp-pipeline .

Трубопроводная работа

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

JDK оснастка

Gradle инструменты

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

Источник ветки Git

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

Этапы сборки

Если вы ищете современный и интересный веб-интерфейс, Jenkins предлагает такой пользовательский интерфейс с Blue Ocean , созданный с нуля для Jenkins Pipeline .

Синий океан

Мы могли бы заявить об успехе на этом этапе, но, если честно, необходимость установки инструментов и тому подобное усложняет процесс автоматизации. Есть ли шанс, что мы могли бы сделать это проще? Да, конечно, и ключевым игроком здесь снова является Docker .

5. Докер в Дженкинсе

Как мы уже упоминали, у Jenkins есть выдающаяся поддержка Docker, которую выполняет большое количество плагинов, ключевыми из которых являются плагин Docker и плагин Docker Pipeline . Практически это означает, что ваш конвейер (или другой вид работы) может раскручивать контейнеры для запуска сборки (и любых других задач или этапов) на них.

Лучший способ проиллюстрировать разницу — это изменить наш Jenkinsfile из предыдущего раздела, чтобы использовать агент Docker вместо предопределенного инструментария.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
pipeline {
  agent {
    docker {
      image 'gradle:4.3.0-jdk8-alpine'
    }
  }
 
  options {
    disableConcurrentBuilds()
    buildDiscarder(logRotator(numToKeepStr:'5'))
  }
 
  triggers {
    pollSCM('H/15 * * * *')
  }
 
  stages {
    stage('Cleanup before build') {
      steps {
        cleanWs()
      }
    }
 
    stage('Checkout from Github') {
      steps {
        checkout scm
      }
    }
 
    stage('Build') {
      steps {
        sh 'gradle build'
      }
    }
  }
 
  post {
    always {
      archiveArtifacts artifacts: 'build/libs/*.jar', fingerprint: true
    }
  }
}

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

Этапы сборки с Docker

Это только один пример интеграции Docker с Jenkins. Существует множество различных способов использования Docker для доведения до совершенства пределов процессов непрерывной интеграции (и доставки).

Есть еще одна вещь, которую необходимо упомянуть здесь, прежде чем мы закончим обсуждение. Поскольку мы запускаем Jenkins в Docker , а Jenkins сам выполняет этапы задания / этапа в Docker , такая модель использования контейнеров известна как Docker-in-Docker (или DinD ). Есть довольно много вещей, о которых нужно знать, когда вы работаете в среде DinD , блестяще подытожил Жером Петаццони в своем блоге . Пожалуйста, проверьте это, если вы серьезно рассматриваете эту модель.

6. Выводы

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

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

Полные сценарии и исходные коды проекта доступны для скачивания: