Gradle + Jenkins Pipeline: переносим freestyle-джобы в Jenkinsfile и перестаём бояться CI-конфига
Jenkins 2.0 принёс Jenkinsfile и Pipeline DSL - CI-конфиг теперь живёт рядом с кодом в Git и проходит ревью вместе с ним. Переносим Gradle-сборки с freestyle.
Jenkins 2.0 вышел в апреле 2016 года с Jenkinsfile и Pipeline DSL, превратив CI-конфигурацию в код, хранимый в репозитории
Jenkins 2.0 вышел в апреле, и мы несколько месяцев смотрели на него с интересом, но не торопились. Причина проста: обновить мажорную версию Jenkins в продакшне - это не «пятиминутное дело». Обновление на одном из клиентских проектов всё же случилось в начале августа, и главное что вышло из этого - не новый интерфейс, а Jenkinsfile.
Что было до
Стандартная картина для проектов, с которыми мы работаем в рамках managed-инфраструктуры: несколько freestyle-джобов в Jenkins 1.x. Один джоб собирает, другой тестирует, третий деплоит. Конфигурация живёт в Jenkins на сервере в XML-файлах где-то в /var/lib/jenkins/jobs/. Бекапится ли? Формально да. Воспроизводима ли при потере сервера? Примерно на 70%, остальное придётся восстанавливать по памяти.
Самая раздражающая часть - это когда нужно изменить шаги сборки. Заходишь в веб-интерфейс, тыкаешь в поля, сохраняешь. Кто изменил, что изменил, зачем - история в Git не скажет, потому что конфиг там не лежит. Jenkins хранит свою историю изменений, но это не то же самое.
Jenkinsfile - что это и зачем
В 2.0 появился Jenkinsfile: текстовый файл в корне репозитория, который описывает пайплайн в виде кода на Groovy-диалекте Pipeline DSL. Jenkins подхватывает его автоматически, если создать Multibranch Pipeline или Pipeline-джоб, указывающий на репозиторий.
Результат такой: разработчик создаёт pull request, туда же идёт изменение Jenkinsfile если нужно поправить шаги сборки. Всё ревьюируется вместе. CI-конфиг проходит тот же путь что и код - это меняет отношение к нему.
Что мы переносили
Проект на Java, сборка через Gradle, три среды: dev, staging, prod. До обновления было четыре freestyle-джоба: сборка, unit-тесты, интеграционные тесты, деплой на staging. Деплой на prod - отдельный джоб с ручным триггером.
Структура Jenkinsfile получилась такая:
pipeline {
agent any
tools {
jdk 'JDK8'
gradle 'Gradle3'
}
stages {
stage('Build') {
steps {
sh './gradlew clean assemble'
}
}
stage('Unit Tests') {
steps {
sh './gradlew test'
}
post {
always {
junit 'build/test-results/test/*.xml'
}
}
}
stage('Integration Tests') {
steps {
sh './gradlew integrationTest'
}
}
stage('Deploy Staging') {
when {
branch 'develop'
}
steps {
sh './gradlew deployStaging'
}
}
}
post {
failure {
mail to: 'team@example.com',
subject: "FAILED: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: "Build failed. Check ${env.BUILD_URL}"
}
}
}
Декларативный синтаксис (pipeline { }) появился чуть позже чем скриптовый - на август 2016 он в статусе beta, скриптовый стабилен. Выше показан декларативный: мы его используем, потому что он читается как конфиг, а не как Groovy-скрипт, но нужно учитывать что API ещё может измениться.
Где потратили время
Первое - инструменты. В freestyle-джобе инструменты (JDK, Gradle) выбираются галочками в UI. В Jenkinsfile нужно прописать имена через директиву tools, и эти имена должны совпадать с тем что прописано в Global Tool Configuration. Пять минут на разбор, но без документации с ходу не очевидно.
Второе - when и ветки. Условие when { branch 'develop' } работает только в Multibranch Pipeline - там Jenkins знает о ветках. В простом Pipeline-джобе эта конструкция не работает как ожидается. Мы несколько минут потеряли пока разобрались почему деплой не срабатывает.
Третье - credentials. Freestyle-джоб умеет инжектировать credentials через UI в переменные окружения. В Jenkinsfile это делается через withCredentials([...]) - блок вокруг шагов где нужны чувствительные данные. Синтаксис немного другой, но концептуально лучше: сразу видно в коде что конкретный шаг работает с секретами.
Четвёртое - нотификации. Старый Email Extension plugin с кастомными шаблонами в freestyle и блок post { failure { } } в Pipeline - это разные вещи. Шаблоны пришлось адаптировать.
Что реально изменилось
Разработчики стали смотреть в Jenkinsfile. Раньше CI-конфиг был «где-то в Jenkins, идите к devops-ам». Теперь файл лежит в репозитории, и при очередном «а почему сборка делает вот это» ответ очевиден - смотри Jenkinsfile в корне. Пара вопросов отпала сама собой.
Восстановление джоба с нуля: если потерять Jenkins-сервер, новый подхватит пайплайн из репозитория. Конфиг среды (плагины, tool configuration) - отдельная история, там всё сложнее. Но сами шаги пайплайна - сохранены в Git.
Ревью изменений в CI: пока что это работает в обе стороны. Pull request с Jenkinsfile виден всей команде. Хорошо это или создаёт лишний шум - зависит от культуры в команде.
Ограничения, с которыми столкнулись
Отладка Jenkinsfile - не то чтобы приятная вещь. Нет способа быстро проверить синтаксис локально без прогона на Jenkins. Есть Replay (переиграть последний билд с изменённым скриптом прямо из UI) - это спасает, но всё равно медленнее чем хотелось бы.
Groovy как язык для CI-скриптов - специфичный выбор. Для декларативного синтаксиса этого почти не чувствуется, но как только нужна нетривиальная логика - начинается Groovy, и не всем это комфортно.
Shared Libraries (переиспользуемые куски пайплайна между проектами) - есть в 2.0, но мы до них не дошли. Когда Jenkinsfile-ов будет несколько - это следующий шаг.
Где сейчас
Проект работает на Jenkins 2.0 с Jenkinsfile три недели. Откатываться на freestyle никто не предлагает. Остальные проекты пока на 1.x - планируем обновлять по очереди, без спешки. Jenkins 2.0 обратно совместим с существующими джобами, так что можно переносить Jenkinsfile постепенно, не трогая то что работает.
- Prometheus + Alertmanager: первый опыт рядом с Zabbix · 13 июля 2016
- Docker Swarm mode в продакшне: три менеджера, шесть воркеров, rolling update · 15 августа 2016