Jenkins 2.0 вышел: переходим на Pipeline-as-Code и Jenkinsfile в каждом репозитории
Jenkins 2.0 официально вышел 20 апреля 2016. Переводим все проекты на Pipeline DSL с Jenkinsfile и shared libraries. Время настройки нового CI - с двух часов до пятнадцати минут.
Jenkins 2.0 официально выпущен 20 апреля 2016 года с Pipeline-as-Code как центральной функцией
20 апреля вышел Jenkins 2.0 финальным релизом. Мы ждали его с февраля, когда уже экспериментировали с Pipeline DSL на release candidate. Официальный выход - хороший повод зафиксировать, что изменилось в нашем подходе к CI за последние три месяца.
Если коротко: мы перестали настраивать CI через интерфейс Jenkins. Вообще.
Откуда взялась задача
На сопровождении у нас полтора десятка проектов разного возраста. У каждого - Jenkins-сборка, настроенная когда-то руками через UI. Часть freestyle-job-ов уходит корнями в 2013 год. Кто именно добавлял шаги, почему выбраны конкретные параметры, почему деплой идёт так, а не иначе - история не сохранилась нигде.
Запуск нового проекта означал: открыть похожий job, начать кликать, что-то скопировать, что-то забыть, получить кривую сборку, доделать. Два часа - если всё шло хорошо и ничего не забыли. Иногда больше.
В феврале мы написали о первых опытах с Jenkins Pipeline DSL. Тогда всё работало на RC с оговорками. Теперь - финальный релиз, и мы завершили миграцию основного пула проектов.
Jenkinsfile как единственный источник правды
Принцип простой: каждый репозиторий несёт в корне Jenkinsfile, который описывает весь пайплайн. Jenkins больше ничего не знает о проекте - только читает этот файл и выполняет.
Типовой Jenkinsfile для Java-проекта у нас выглядит примерно так:
@Library('adg-shared') _
node {
stage('Checkout') {
checkout scm
}
stage('Build') {
sh 'mvn clean package -DskipTests'
}
stage('Test') {
sh 'mvn test'
junit 'target/surefire-reports/*.xml'
}
stage('Docker') {
def tag = env.BRANCH_NAME == 'master' ? 'latest' : env.BRANCH_NAME
def image = docker.build("${env.PROJECT_NAME}:${tag}")
docker.withRegistry('https://registry.example.com', 'registry-creds') {
image.push()
}
}
stage('Deploy') {
if (env.BRANCH_NAME == 'master') {
adgDeploy(env: 'staging', playbook: 'deploy.yml')
}
}
}
@Library('adg-shared') _ - это подключение нашей shared library. adgDeploy - один из шагов, определённых в ней. О библиотеке отдельно.
Shared library: общие шаги один раз
Это ключевой момент, которого не было в феврале. Jenkins Pipeline позволяет вынести переиспользуемые шаги в отдельный git-репозиторий и подключать его как библиотеку. Мы сделали adg-shared с несколькими десятками функций: деплой через Ansible, публикация образов в registry, уведомления в Slack, сканирование зависимостей.
Структура библиотеки:
adg-shared/
vars/
adgDeploy.groovy # шаг деплоя через Ansible
adgNotify.groovy # уведомления Slack/почта
adgDockerPush.groovy # публикация образа с тегами
adgSonar.groovy # запуск SonarQube
src/
ru/adg/pipeline/ # вспомогательные классы
Каждый vars/*.groovy - это один вызываемый шаг. adgDeploy внутри знает, как звать Ansible, какие переменные передавать, как интерпретировать коды возврата. Jenkinsfile в конкретном проекте не знает этих деталей.
Эффект от shared library: когда мы меняем логику деплоя (например, добавили health-check после Ansible-прогона), это изменение автоматически распространяется на все проекты при следующем запуске. Раньше такое обновление означало пройти по всем job-ам руками.
Что дал Jenkinsfile в репозитории
История изменений. git log Jenkinsfile показывает, кто и когда менял пайплайн. Больше не нужно гадать, почему в сборке появился шаг с npm run legacy-build - есть коммит, есть автор, есть сообщение.
Ревью изменений CI как кода. Если разработчик меняет Jenkinsfile в ветке, это идёт в пулл-реквест. Команда видит изменение пайплайна до того, как оно попадёт в master. Это остановило уже несколько сомнительных правок.
Новый проект - не полтора часа настройки. Создаёшь Jenkins Job типа Pipeline, указываешь репозиторий. Jenkins сам читает Jenkinsfile и строит пайплайн. Всё. При типовом проекте время настройки CI - пятнадцать минут, из которых десять уходят на базовую конфигурацию Job в интерфейсе и нажатие кнопки.
Ветки работают правильно. Multibranch Pipeline автоматически создаёт job для каждой ветки, которая несёт Jenkinsfile. Feature-ветки собираются сами, по merge в master деплоится staging. Это поведение описано в Jenkinsfile - не в интерфейсе.
Где всё ещё неудобно
Groovy-sandbox - это боль, которая никуда не делась. Часть стандартных операций требует одобрения через Script Approval. Логика не всегда очевидна: два разных метода из одного класса могут быть по-разному доступны в sandbox. Мы завели отдельный документ «что одобрить в Jenkins после установки» - без него новый администратор потратит время впустую.
Отладка Groovy-кода в Jenkinsfile неудобна. Нет нормального способа запустить его локально. Пишешь, коммитишь, ждёшь запуска Job, читаешь лог. Если синтаксическая ошибка - узнаёшь об этом только при выполнении. Команда Jenkins анонсировала Declarative Pipeline как альтернативу (синтаксис pipeline { ... } вместо imperative Groovy) - у него планируется валидатор, но в реальных проектах нам пока хватает возможностей Scripted Pipeline.
Shared library версионируется, но тянуть за собой изменения - отдельная работа. Если мы поменяли интерфейс adgDeploy, нужно обновить Jenkinsfile во всех репозиториях. Пока проектов немного - терпимо, но масштаб виден.
Текущее состояние
На Pipeline перенесено около двух третей проектов. Оставшийся хвост - либо совсем простые сборки, которые трогать незачем, либо нестандартные случаи, которые требуют отдельного разбора.
Freestyle-jobs на новых проектах больше не создаём. Shared library растёт по мере переноса: каждый нестандартный случай, который встречается при миграции, либо ложится в библиотеку как новый шаг, либо фиксируется в Jenkinsfile как явное исключение.
Вернуться к настройке CI через интерфейс Jenkins никому уже не хочется. Сравниваешь: кликать по форме без истории изменений или написать двадцать строк Groovy, которые живут в git - выбор очевидный.