На Хабре много статей о Jenkins, но мало где описывается пример работы Jenkins и докер агентов. Все популярные инструменты сборки проектов типа Drone.io, Bitbucket Pipeline, GitLab, GitHub actions и другие, могут собирать все в контейнерах. Но как же Jenkins?
На сегодняшний день есть решение проблемы: Jenkins 2 замечательно умеет работать с Docker агентами. В статье я хочу поделиться опытом и показать, как вы можете это сделать сами.
Почему я занялся решением этой проблемы?
Так как мы в компании Citronium используем множество различных технологий, то на сборочной машине приходится держать разные версии Node.JS, Gradle, Ruby, JDK и прочих. Но зачастую конфликтов версий не избежать. Да, вы будете правы если скажете, что есть различные менеджеры версий типа nvm, rvm, но не всё так гладко с ними и у этих решений есть проблемы:
- большой объем рантаймов, который разработчики забывают чистить;
- есть конфликты между разными версиями одних рантаймов;
- каждому разработчику нужен разный набор компонентов.
Есть и другие проблемы, но давайте я лучше расскажу про решение.
Jenkins в Docker
Так как сейчас Docker уже хорошо укоренился в сфере разработки, то почти все можно запустить при помощи Docker. Мое же решение в том, чтобы Jenkins был в Docker и мог запускать другие Docker контейнеры. Этим вопросом стали задаваться еще в 2013 году в статье "Docker can now run within Docker".
Если вкратце просто необходимо в рабочий контейнер установить сам Docker и примонтировать файл /var/run/docker.sock
.
Вот пример Dockerfile, который получился для Jenkins.
FROM jenkins/jenkins:lts
ARG DOCKER_COMPOSE_VERSION=1.25.0
USER root
RUN apt-get update && apt-get upgrade -y && apt-get -y install apt-transport-https ca-certificates curl gnupg2 git software-properties-common && curl -fsSL https://download.docker.com/linux/$(. /etc/os-release; echo "$ID")/gpg > /tmp/dkey; apt-key add /tmp/dkey && add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/$(. /etc/os-release; echo "$ID") $(lsb_release -cs) stable" && apt-get update && apt-get -y install docker-ce && apt-get clean autoclean && apt-get autoremove && rm -rf /var/lib/{apt,dpkg,cache,log}/
RUN curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose && chmod +x /usr/local/bin/docker-compose
RUN usermod -aG docker jenkins && gpasswd -a jenkins docker
USER jenkins
Таким образом мы получили Docker контейнер который может выполнять Docker команды на хостовой машине.
Настройка сборки
Не так давно Jenkins получил возможность описывать свои правила при помощи Pipeline синтаксиса, что позволяет достаточно просто менять скрипт сборки и хранить его в репозитории.
Так давайте же мы поместим в сам репозиторий специальный Dockerfile, который будет содержать в себе все необходимые для сборки библиотеки. Таким образом сам разработчик может подготовить повторяемую среду и не нужно будет OPS просить поставить на хост определенную версию Node.JS.
FROM node:12.10.0-alpine
RUN npm install yarn -g
Такой сборочный образ подходит для большинства Node.JS приложений. А если вам, например, нужен образ для JVM проекта со включенным внутрь Sonar сканером? Вы сами вольны выбирать нужные для сборки компоненты.
FROM adoptopenjdk/openjdk12:latest
RUN apt update && apt install -y bash unzip wget
RUN mkdir -p /usr/local/sonarscanner && cd /usr/local/sonarscanner && wget https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-3.3.0.1492-linux.zip && unzip sonar-scanner-cli-3.3.0.1492-linux.zip && mv sonar-scanner-3.3.0.1492-linux/* ./ && rm sonar-scanner-cli-3.3.0.1492-linux.zip && rm -rf sonar-scanner-3.3.0.1492-linux && ln -s /usr/local/sonarscanner/bin/sonar-scanner /usr/local/bin/sonar-scanner
ENV PATH $PATH:/usr/local/sonarscanner/bin/
ENV SONAR_RUNNER_HOME /usr/local/sonarscanner/bin/
Мы описали сборочное окружение, но при чем тут Jenkins? А Jenkins агенты умеют работать с такими Docker образами и проводить сборку внутри.
stage("Build project") {
agent {
docker {
image "project-build:${DOCKER_IMAGE_BRANCH}"
args "-v ${PWD}:/usr/src/app -w /usr/src/app"
reuseNode true
label "build-image"
}
}
steps {
sh "yarn"
sh "yarn build"
}
}
Директива agent
использует свойство docker
, где вы можете указать:
- имя сборочного контейнера согласно вашей политике нейминга;
- аргументы необходимые для запуска сборочного контейнера, где в нашем случае мы монтируем текущую директорию как директорию внутри контейнера.
А уже в шагах сборки мы указываем, какие команды выполнить внутри сборочного Docker агента. Это может все что угодно, таким образом я так же запускаю деплой приложений при помощи ansible.
Ниже я хочу показать общий Jenkinsfile, который может собрать простое Node.JS приложение.
def DOCKER_IMAGE_BRANCH = ""
def GIT_COMMIT_HASH = ""
pipeline {
options {
buildDiscarder(
logRotator(
artifactDaysToKeepStr: "",
artifactNumToKeepStr: "",
daysToKeepStr: "",
numToKeepStr: "10"
)
)
disableConcurrentBuilds()
}
agent any
stages {
stage("Prepare build image") {
steps {
sh "docker build -f Dockerfile.build . -t project-build:${DOCKER_IMAGE_BRANCH}"
}
}
stage("Build project") {
agent {
docker {
image "project-build:${DOCKER_IMAGE_BRANCH}"
args "-v ${PWD}:/usr/src/app -w /usr/src/app"
reuseNode true
label "build-image"
}
}
steps {
sh "yarn"
sh "yarn build"
}
}
post {
always {
step([$class: "WsCleanup"])
cleanWs()
}
}
}
Что же вышло?
Благодаря такому способу мы решили следующие проблемы:
- время конфигурации сборки окружения сводится к 10 — 15 минутам на проект;
- полностью повторяемое окружение сборки приложения, так как можно так собирать и на локальном компьютере;
- нет проблем с конфликтами разных версий сборочных инструментов;
- всегда чистый воркспейс, который не забивается.
Само по себе решение простое и очевидное и позволяет получить одни плюсы. Да, порог входа чуточку поднялся по сравнению с простыми командами для сборок, но зато теперь есть гарантия, что будет собираться всегда и разработчик сам может выбрать все, что необходимо для его процесса сборки.
Так же вы можете воспользоваться собранным мною образом Jenkins + Docker. Все исходники открыты и лежат на rmuhamedgaliev/jenkins_docker.
В ходе написания статьи появилась дискуссия о использовании агентов на удаленных серверах, чтобы не грузить мастер ноду при помощи плагина docker-plugin. Но об этом я расскажу в будущем.
Комментарии (15)
dnbstd
21.12.2019 22:39В stable helm чарте Jenkins для к8s есть динамически создаваемые агенты. Взгляните, очень полезная вещь. По моему даже на хабре описывали.
rmuhamedgaliev Автор
21.12.2019 22:40Хорошо, спасибо. Знаю что еще есть Jenkins X для куберенетиса.
dnbstd
21.12.2019 23:27Это немного разные вещи. Jenkins X — для деплоя в кубернетес онли. И на сколько я помню представляет из себя консольную утилиту.
kruftik
22.12.2019 07:45apt-get update && \
apt-get -y install docker-ce && \
usermod -aG docker jenkins
RUN curl -L github.com/docker/compose/releases/download/1.25.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose && chmod +x /usr/local/bin/docker-compose
RUN apt-get clean autoclean && apt-get autoremove —yes && rm -rf /var/lib/{apt,dpkg,cache,log}/
анти-бест-практиз-докерфайл. обновляем список пакетов в одном слое, чистим за собой в слое через 1…pankraty
22.12.2019 10:29А вы не могли бы чуть подробнее описать, почему это плохо, и как было бы правильно?
kSx
22.12.2019 11:15+2Каждая инструкция создаёт новый слой на основе предыдущего. И если почистить /var/lib/{apt,dpkg,cache,log} не при установке пакетов (с помощью && после apt-get install), а в последующей инструкции, то образ от этого не станет меньше.
unix196
25.12.2019 01:58а это точно актуально?
github.com/rocker-org/rocker/issues/35unix196
25.12.2019 08:13docker run --rm debian:stretch cat /etc/apt/apt.conf.d/docker-clean
# Since for most Docker users, package installs happen in "docker build" steps,
# they essentially become individual layers due to the way Docker handles
# layering, especially using CoW filesystems. What this means for us is that
# the caches that APT keeps end up just wasting space in those layers, making
# our layers unnecessarily large (especially since we'll normally never use
# these caches again and will instead just "docker build" again and make a brand
# new image).
# Ideally, these would just be invoking "apt-get clean", but in our testing,
# that ended up being cyclic and we got stuck on APT's lock, so we get this fun
# creation that's essentially just "apt-get clean".
DPkg::Post-Invoke { "rm -f /var/cache/apt/archives/*.deb /var/cache/apt/archives/partial/*.deb /var/cache/apt/*.bin || true"; };
APT::Update::Post-Invoke { "rm -f /var/cache/apt/archives/*.deb /var/cache/apt/archives/partial/*.deb /var/cache/apt/*.bin || true"; };
Dir::Cache::pkgcache "";
Dir::Cache::srcpkgcache "";
Disminder
25.12.2019 02:11Зачем собирать изображение при сборке таски, если его можно залить в хаб?
И зачем DinD, если с мастера Дженкинса можно подключиться по SSH к сборочной ноде (ну или к своей же, если совсем туго с железом), и там уже запускать сборочные контейнеры shell-скриптом из пайплайна (лично мне кажется что так больше прозрачности в работе тасок)?rmuhamedgaliev Автор
25.12.2019 02:19Зачем собирать изображение при сборке таски, если его можно залить в хаб?
Никто вам не мешает поступить так. Просто укажите в Jenkinsfile имя нужно вам образа. Я же просто показал такой пример. Вы сами вольны выбирать свой подход.
pipeline { agent none stages { stage('Back-end') { agent { docker { image 'maven:3-alpine' } } steps { sh 'mvn --version' } } stage('Front-end') { agent { docker { image 'node:7-alpine' } } steps { sh 'node --version' } } } }
И зачем DinD, если с мастера Дженкинса можно подключиться по SSH к сборочной ноде (ну или к своей же, если совсем туго с железом), и там уже запускать сборочные контейнеры shell-скриптом из пайплайна (лично мне кажется что так больше прозрачности в работе тасок)?
Использование shell скриптов заставляет вас самих контролировать жизненный цикл сборки. А агент, после собрки позволяет потушить сборочный контейнер. Да вы можете все писать руками и при помощи баш скриптов, но тогда размер кода который вам необходимо поддерживать вырастет вразы.
Про SSH вам придется подключаться самим к себе, только смысла в этом нет. Лучше подключение использовать когда у вас > 1 машины, котоыре могут работать с Docker.Disminder
25.12.2019 10:30Я же просто показал такой пример
А, тогда ок, просто непонятно чем была обоснована сборка нового изображения каждый билд.
после собрки позволяет потушить сборочный контейнер
Весьма ценное уточнение, спасибо.
Shadow0fClown
1) А зачем кастомный образ для Jenkins master, если можно просто пробросить докер сокет в контейнере, как показывают в документации?
2) А последний приведённый Jenkinsfile точно рабочий? Мне казалось, что если мы хотим указать agent в каком-то из stage, то мы должны в самом начале сделать agent none, или уже что-то изменилось?
3) Возможно сразу использованать docker-plugin более правильно, ведь также можно указать как удалённый сервер, так и локальный сокет.
rmuhamedgaliev Автор
MasMaX
2) Первое упоминание agent вне stage это агент по-умолчанию. Для тех stage, где он не укащан явно. Вместо any здесь так же глобально можно указать например docker-образ.