Управление задачами в Jenkins CI
Jenkins сейчас используется, пожалуй, практически в любой компании, где есть необходимость в автоматическом деплое приложений и инфраструктуры, а также в удобном управлении различного рода задач. На рынке сейчас представлено много других инструментов (как платных, так и бесплатных), позволяющих построить процесс непрерывной интеграции максимально комфортно. Jenkins является бесплатным инструментом, обладающим огромными возможностями в виде тысяч плагинов, […]
Jenkins сейчас используется, пожалуй, практически в любой компании, где есть необходимость в автоматическом деплое приложений и инфраструктуры, а также в удобном управлении различного рода задач.
На рынке сейчас представлено много других инструментов (как платных, так и бесплатных), позволяющих построить процесс непрерывной интеграции максимально комфортно.
Jenkins является бесплатным инструментом, обладающим огромными возможностями в виде тысяч плагинов, которые постоянно добавляются и обновляются.
Базовый подход к управлению задачами предлагается нам через веб-интерфейс самого Jenkins, где мы можем что угодно сконфигурировать, но поддерживать это при большом количестве задач и их разнотипности становится сложнее.
Ниже мы рассмотрим, как упростить и ускорить создание задач в Jenkins.
Инструменты, которые будем использовать
Jenkins job builder
Это python-утилита, позволяющая описывать задачи в YAML- или JSON форматах, которые через HTTP API загружаются на сервер. Для работы достаточно установить ее на локальной машине, написать конфигурацию с подключением и описать требуемые задачи.
Актуальная документация проекта
DSL Plugin
Это плагин для Jenkins, с помощью которого мы сможем описывать задачи на специальном языке (DSL, Domain Specific Language) и создавать их на основе этих описаний. Работает локально на самом Jenkins-сервере.
Страница плагина
Jenkins Pipeline
Это тип задачи в Jenkins, с помощью которого мы можем описать необходимый нам процесс деплоя или сборки приложения по стадиям. Описывать pipeline мы будем в специальном Jenkinsfile, который будет храниться в репозитории проекта.
Gitlab и Gitlab CI
Наиболее популярный и развивающийся сейчас selfhosted проект, который также используется во многих компаниях. Он включает в себя версию для сообщества и энтерпрайз-решение.
Внутри имеется собственный CI, написанный на Go, который через специальные подключаемые агенты (runners) позволяет нам проводить различные стадии сборки и деплоя.
Для начала использования достаточно в репозитории проекта создать файл .gitlab-ci.yml, в котором описать необходимую нам последовательность. Для работы отлично может использовать Docker.
Итак, начнем
Для начала поставим необходимые инструменты.
Jenkins-job-builder в нашем случае необходимо установить как на локальную машину, так и на машину, где будет работать агент gitlab. Можно обойтись просто локальной машиной gitlab.
Локально на linux и mac установим последнюю доступную версию:
pip install jenkins-job-builder brew install jenkins-job-builder
Локально для описания нашего seed job мы сможем протестировать его работоспособность.
Добавлю, что seed job в терминологии DSL Plugin является задача типа Free-style project, которая создает остальные задачи из DSL-скриптов.
На gitlab-сервере установка будет ничем не отличаться от локальной установки на linux-машину:
pip install jenkins-job-builder
На всякий случай запустим утилиту и проверим, что она работает:
➜ ~ jenkins-jobs usage: jenkins-jobs [-h] [--conf CONF] [-l LOG_LEVEL] [--ignore-cache] [--flush-cache] [--version] [--allow-empty-variables] [--user USER] [--password PASSWORD] .
Установка плагина
Теперь установим плагин в Jenkins. Для установки можно воспользоваться как классическим UpdateCenter в разделе Manage Jenkins → Manage Plugins → Available, так и используя установку с помощью curl/wget, скачав плагин с официальной страницы плагина:
- Версия gitlab — 9.4.4-ce.0
- Версия gitlab-ci-multi-runner — 9.4.2
Создание репозитория
Создадим репозитории под код с описанием задач.
Для начала подготовим наш seed job — так называется задача, которая генерирует из себя все остальные. Репозиторий будет состоять из следующих файлов:
jenkins.ini
[job_builder] ignore_cache=True keep_descriptions=False include_path=. recursive=False allow_duplicates=False [jenkins] user=username password=secret-pass url=http://example-ci.org
job-generator.yml
--- - job: name: job-generator description: 'Do not edit this job through the web!' scm: - git: url: git@gitlab-selfhosted.org:group/repo-dsl.git branches: - origin/master credentials-id: some-credentials-id timeout: 20 basedir: dsl triggers: - gitlab: trigger-push: true trigger-merge-request: false trigger-open-merge-request-push: both ci-skip: false set-build-description: false add-note-merge-request: false add-vote-merge-request: false add-ci-message: true allow-all-branches: true branch-filter-type: RegexBasedFilter target-branch-regex: '.*master*.' builders: - dsl: target: "dsl/*.groovy" ignore-existing: "false" removed-job-action: "DISABLE" removed-view-action: "DELETE" lookup-strategy: "SEED_JOB"
.gitlab-ci.yml
job: script: 'jenkins-jobs --conf jenkins.ini update job-generator.yml'
Подключим gitlab-runner к нашему проекту. Поскольку интерфейс gitlab последнее время часто меняется, то рекомендуем обращаться к официальной документации .
После подключения он будет выглядеть примерно так:

Теперь закоммитим изменения и в разделе Jobs у нашего проекта увидим следующее:


Вся схема выглядит следующим образом:
Подготовим репозиторий с описанием задач. Создадим его с названием repo-dsl и плоской структурой. Все файлы располагаются как есть.
Положим в него файл с нашим pipeline:
pipelineJob('testpipeline-build') < description('Build test docker image, test and push it to local registry') definition < cpsScm < scm < git < branch('origin/master') remote < url('git@gitlab-selfhosted.org:test-group/sample-project.git') credentials('gitlab-creds') >> > scriptPath('Jenkinsfile') > > >
И настроим webhook по запуску seed-job, который мы создали ранее.
В данной версии gitlab webhooks располагаются в проекте в разделе Settings → Integrations.

Создаем webhook на push ( gitlab:pass@ci.org/project/job-generator ). Схематично это выглядит вот так:
Теперь в нашем Jenkins есть две задачи:
- Задача-генератор seed-job
- Задача типа pipeline с названием testpipeline-build.
В тестовом pipeline мы будем собирать docker-образ, который после будет загружаться в локальный docker registry. Файл с образом будет браться с другого проекта.
В проекте с названием sample-project создадим Jenkinsfile следующего содержания:
node < def app stage('Clone repository') < checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'gitlab-repo-creds', url: 'git@gitlab-selfhosted.org:test-group/docker-image-file.git']]]) >stage('Build image') < app = docker.build("docker-image") >stage('Test image') < sh ''' if ! docker inspect docker-image &> /dev/null; then echo 'docker-image does not exist!' exit 1 fi ''' >stage('Push image') < echo 'Push image' docker.withRegistry('https://local-registry:9666', 'registry-creds') < app.push("$") app.push("latest") > > stage('Clean existing image') < sh "docker rmi docker-image" >>
В итоге готовый pipeline будет выглядеть следующим образом в интерфейсе blueocean в Jenkins:

Или так в классическом интерфейсе:

Для запуска этой задачи автоматически мы можем добавить webhook в проекте с docker-image, где после отправки изменений в репозиторий будет запускаться эта задача.
В заключении отметим, что данная статья описывает лишь один из множества подходов управления задачами в jenkins. За счет гибкости и функционала вы можете использовать разные подходы, которые позволят именно вам управлять задачами максимально просто и удобно исходя из ваших требований и требований инфраструктуры.
Настройка подчиненного узла Jenkins
В этом руководстве мы рассмотрим концепцию распределенных сборок в архитектуре Jenkins. Кроме того, мы узнаем, как настроить архитектуру Jenkins master-slave . Кроме того, мы создадим конвейер на главном сервере Jenkins для запуска заданий сборки на подчиненных узлах.
2. Распределенные сборки
В идеале машина, на которую мы устанавливаем стандартный Jenkins, будет нашим мастером Jenkins . На машине подчиненного узла мы установим программу времени выполнения под названием Agent. Установка агента не будет стандартной установкой Jenkins, но этот агент будет работать на JVM. Его возможностей достаточно для запуска подзадачи или основной задачи Jenkins в выделенном исполнителе:

У нас может быть любое количество узлов-агентов или подчиненных узлов . Кроме того, мы можем настроить главный узел, чтобы решить, какая задача или задание должны выполняться на каком агенте и сколько агентов-исполнителей мы можем иметь. Связь между ведущими и подчиненными узлами Jenkins является двунаправленной и осуществляется по протоколу TCP/IP .
3. Настройка главных и подчиненных узлов Jenkins
Стандартная установка Jenkins включает мастер Jenkins, и в этой настройке мастер будет управлять всеми задачами нашей системы сборки . Если мы работаем над несколькими проектами, мы можем запустить множество заданий для каждого из них. Некоторые проекты требуют использования определенных узлов, что требует использования подчиненных узлов.
Мастер Jenkins отвечает за планирование заданий, назначение подчиненных узлов и отправку сборок на подчиненные узлы для выполнения . Он также будет отслеживать состояние ведомого узла (автономно или онлайн), извлекать результаты сборки из ведомых узлов и отображать их на выходе терминала. В большинстве установок несколько ведомых узлов будут назначены для задачи построения заданий.
Прежде чем мы начнем, давайте еще раз проверим, что у нас есть все необходимые условия для добавления подчиненного узла :
- Сервер Jenkins запущен и готов к использованию.
- Другой сервер для конфигурации подчиненного узла
- Сервер Jenkins и подчиненный сервер подключены к одной сети.
Чтобы настроить главный сервер, мы войдем на сервер Jenkins и выполним следующие действия.
Во-первых, мы перейдем к «Управление Jenkins — > Управление узлами — > Новый узел», чтобы создать новый узел :

На следующем экране мы вводим «Имя узла» (slaveNode1), выбираем «Постоянный агент», затем нажимаем «ОК» :

После нажатия «ОК» мы попадем на экран с новой формой, где нам нужно заполнить информацию о ведомом узле . Мы предполагаем, что подчиненный узел работает в операционных системах Linux, поэтому метод запуска установлен на «Запуск агентов через ssh» .
Точно так же мы добавим соответствующие детали, такие как название, описание и количество исполнителей.
Мы сохраним нашу работу, нажав кнопку «Сохранить» . «Ярлыки» с именем «slaveNode1» помогут нам настроить задания на этом ведомом узле:
4. Сборка проекта на подчиненных узлах
Теперь, когда наши главный и подчиненный узлы готовы, мы обсудим шаги по созданию проекта на подчиненном узле.
Для этого мы начнем с нажатия «Новый элемент» в верхнем левом углу панели инструментов.
Далее нам нужно ввести название нашего проекта в поле «Введите имя элемента» и выбрать «Конвейерный проект», после чего нажать кнопку «ОК».
На следующем экране мы введем «Описание» (необязательно) и перейдем к разделу «Конвейер». Убедитесь, что в поле «Определение» выбран параметр «Сценарий конвейера».
После этого мы копируем и вставляем следующий декларативный скрипт конвейера в поле «скрипт»:
node('slaveNode1') stage('Build') sh '''echo build steps''' > stage('Test') sh '''echo test steps''' > >
Далее нажимаем на кнопку «Сохранить». Это перенаправит вас на страницу просмотра Pipeline.
На левой панели мы нажимаем кнопку «Build Now», чтобы выполнить наш Pipeline. После завершения выполнения Pipeline мы увидим представление Pipeline:

Мы можем проверить историю выполненной сборки в разделе «История сборки», щелкнув номер сборки . Как показано выше, когда мы нажимаем на номер сборки и выбираем «Консольный вывод», мы видим, что конвейер работает на нашей машине slaveNode1 .
5. Вывод
В этом руководстве мы рассмотрели, что такое распределенная сборка в Jenkins, и увидели, как главный узел Jenkins вызывает подчиненные узлы. Кроме того, мы узнали, как настроить конфигурацию Jenkins master-slave. Наконец, мы проверили конфигурацию подчиненного узла.
- 1. Обзор
- 2. Распределенные сборки
- 3. Настройка главных и подчиненных узлов Jenkins
- 4. Сборка проекта на подчиненных узлах
- 5. Вывод
Jenkins Pipeline. Что это и как использовать в тестировании
Меня зовут Александр Михайлов, я работаю в команде интеграционного тестирования компании ЮMoney.
Наша команда занимается приемочным тестированием. Оно включает в себя прогон и разбор автотестов на критичные бизнес-процессы в тестовой среде, приближенной по конфигурации к продакшену. Еще мы пишем фреймворк, заглушки, сервисы для тестирования — в целом, создаем экосистему для автоматизации тестирования и обучаем ручных тестировщиков автоматизации.
Надеюсь, что эта статья будет интересна как новичкам, так и тем, кто съел собаку в автоматизации тестирования. Мы рассмотрим базовый синтаксис Jenkins Pipeline, разберемся, как создать джобу на основе пайплайна, а также я расскажу про опыт внедрения неочевидной функциональности в CI — запуска и дожатия автотестов по условию.
Запуск автотестов на Jenkins — инструкция
Не новость, что автотесты эффективнее всего проводить после каждого изменения системы. Запускать их можно локально, но мы рекомендуем делать это только при отладке автотестов. Больший профит автотесты принесут при запуске на CI. В качестве CI-сервера у нас в компании используется Jenkins, в качестве тестового фреймворка — JUnit, а для отчетов — Allure Report.
Чтобы запускать тесты на Jenkins, нужно создать и сконфигурировать джобу.
Для этого достаточно выполнить несколько несложных шагов.
1) Нажать «Создать», выбрать задачу со свободной конфигурацией и назвать ее, например, TestJob.
Естественно, для этого у вас должны быть права в Jenkins. Если их нет, нужно обратиться к администратору Jenkins.
2) Указать репозиторий, откуда будет выкачиваться код проекта: URL, credentials и branch, с которого все будет собираться.
3) Добавить нужные параметры, в этом примере — количество потоков (threadsCount) и список тестов для запуска (testList).
Значение “*Test” для JUnit означает «Запустить все тесты».
4) Добавить команду для запуска тестов.
Наш вариант запускается на Gradle: мы указываем таску теста и передаем параметры в тесты.
./gradlew test -PthreadsCount=$threadsCount -PtestList=$testList
Можно выполнить шаг сборки «Выполнить команду shell», либо через Gradle Plugin использовать шаг «Invoke Gradle Script».
5) Нужно добавить Allure-report (должен быть установлен https://plugins.jenkins.io/allure-jenkins-plugin/) в «Послесборочные операции», указав путь к артефактам Allure после прогона (по умолчанию allure-result).
Создав джобу и прогнав с ее помощью тестов, мы можем получить такой результат.
На скрине — реальный прогон. По таймлайну видно, как тесты разделяются по потокам и где были падения.
Несложно заметить, что тесты у нас падают.
Почему падают тесты
Падения могут случаться по разным причинам. В нашем случае на это влияют:
- ограниченные ресурсы тестового стенда,
- большое число микросервисов (~140); если при запуске интеграционных тестов какой-то один микросервис подтормаживает, тесты начинают валиться,
- большое число интеграционных тестов (>3000 E2E),
- врожденная нестабильность UI-тестов.
В итоге проблемы по этим причинам мультиплицируются, и мы получаем комбинаторный взрыв, проявляющийся ошибками в прогонах тестов. Что делать? Дожимать.
Что такое дожим
Дожим — это перезапуск автотестов в рамках одного прогона. При успешном прохождении автотеста можно считать его пройденным, не учитывая предыдущие падения в прогоне.
«Дожимать? Опасно же!»
Возразите вы и будете полностью правы. Безусловно, дожим автотестов может спровоцировать пропуск дефекта на продакшн. Баг может быть плавающим — при повторном запуске успешно просочиться и попасть на продакшн.
Но мы проанализировали статистику падений автотестов за длительный период и увидели, что большинство падений было связано с нестабильной тестовой средой, тестовыми данными. Поэтому мы взяли на себя риск и стали дожимать тесты. Прошло уже больше года, и не было ни одного дефекта, пропущенного на продакшн по этой причине. Зато стабильность тестов повысилась ощутимо.
Как решать задачу с дожимами
Мы пробовали разные решения: использовали модификацию поведения JUnit 4, JUnit 5, писали обертки на Kotlin. И, к сожалению, каждый раз реализация завязывалась на фичах языка или фреймворка.
Если процесс запускался с помощью JUnit 4 или JUnit 5, возможность перезапустить тесты была только сразу при падении. Тест упал, перезапустили его несколько раз подряд — и если сбоил какой-то микросервис из-за нагрузки, либо настройки тестовой среды были некорректные, то тест все три раза падал.
Это сильно удлиняло прогон, особенно когда проблема была в настройке тестовой среды, что приводило к провалу множества тестов.
Мы взглянули на проблему шире, решили убрать зависимость от тестового фреймворка или языка и реализовали перезапуск на более высоком уровне — на уровне CI. И сделали это с помощью Jenkins Pipeline.
Для этого подходил следующий алгоритм: получаем список упавших тестов и проверяем условия перезапуска. Если упавших тестов нет, завершаем прогон. Если есть, решаем, запускать их повторно или нет. Например, можно реализовать логику, чтобы не перезапускались тесты, если их упало больше какого-то критического числа. Или перед запуском можно сделать проверку доступности тестируемого сервиса.
Что такое Jenkins Pipeline
Jenkins Pipeline — набор плагинов, позволяющий определить жизненный цикл сборки и доставки приложения как код. Он представляет собой Groovy-скрипт с использованием Jenkins Pipeline DSL и хранится стандартно в системе контроля версий.
Существует два способа описания пайплайнов — скриптовый и декларативный.
node < stage('Example') < try < sh 'exit 1' >catch (exc) < throw exc >> >
pipeline < agent any stages < stage("Stage name") < steps <>> > >
Они оба имеют структуру, но в скриптовом она вольная — достаточно указать, на каком слейве запускаться (node), и стадию сборки (stage), а также написать Groovy-код для запуска атомарных степов.
Декларативный пайплайн определен более жестко, и, соответственно, его структура читается лучше.
Рассмотрим подробнее декларативный пайплайн.
- В структуре должна быть определена директива pipeline.
- Также нужно определить, на каком агенте (agent) будет запущена сборка.
- Дальше идет определение stages, которые будут содержаться в пайплайне, и обязательно должен быть конкретный стейдж с названием stage(“name”). Если имени нет, тест упадет в runtime с ошибкой «Добавьте имя стейджа».
- Обязательно должна быть директива steps, в которой уже содержатся атомарные шаги сборки. Например, вы можете вывести в консоль «Hello».
pipeline < // определение декларативного pipeline agent any // определяет, на каком агенте будет запущена сборка stages < // содержит стейджи сборки stage("Stage name") < // отдельный стейдж сборки steps < // набор шагов в рамках стейджа echo "Hello work" // один из шагов сборки >> > >
Мне нравится декларативный вид пайплайна тем, что он позволяет определить действия после каждого стейджа или, например, после всей сборки. Я рекомендую использовать его при описании пайплайнов на верхнем уровне.
pipeline < stages < stage("Post stage") < post < // определяет действия по завершении стейджа success < // триггером исполнения секции является состояние сборки archiveArtifacts artifacts: '**/target/*' >> > > post < // после всей сборки cleanup < cleanWs() >> >
Если сборка и стейдж завершились успешно, можно сохранить артефакты или почистить workspace после сборки. Если же при таких условиях использовался бы скриптовый пайплайн, пришлось бы за этим «флоу» следить самостоятельно, добавляя обработку исключений, условия и т.д.
При написании своего первого пайплайна я, естественно, использовал официальный мануал — там подробное описание синтаксиса и степов.
- https://jenkins.io/doc/book/pipeline/syntax
- https://jenkins.io/doc/pipeline/steps/
Сначала я даже растерялся и не знал, с чего начать — настолько там много информации. Если вы первый раз сталкиваетесь с написанием пайплайна, начать лучше со знакомства с генераторами фрагментов пайплайна из UI-интерфейса.
Если к URL вашего веб-интерфейса Jenkins добавить ендпойнт /pipelines-syntax, откроется страница, в которой есть ссылки на документацию и два сниппет-генератора, позволяющие генерировать пайплайн даже без знания его синтаксиса:
- Declarative sections generator
- Snippet Generator
Генераторы фрагментов — помощники в мире Jenkins
Для создания пайплайна сначала нужно декларативно его описать, а затем наполнить степами. Давайте посмотрим, как вспомогательные инструменты нам в этом помогут.
- Declarative sections generator (JENKINS-URL/directive-generator) — генератор фрагментов для декларативного описания пайплайна.
Для добавления стадии нужно написать ее имя и указать, что будет внутри (steps). После нажатия кнопки «Сгенерировать» будет выведен код, который можно добавлять в пайплайн.
stage(“start tests”) < steps < //One or more steps needs to be included within the steps block >>
Также нам нужны шаги, в которых будут выполняться различные действия — например, запуск джобы Jenkins с тестами.
- Snippet Generator (JENKINS-URL/pipeline-syntax) — поможет сгенерировать фрагменты шагов.
- В Sample Step выбрать build: Build a job.
- (Дальше функционал подсказывает) — необходимо определить параметры, которые будут переданы в джобу (для примера задано branch, project).
- Нажать кнопку «Generate» — в результате сформируется готовый рабочий код.
Изменим параметры джобы на те, которые определили при ее создании.
build job ‘QA/TestJob’, parameters: [ string(name: 'threadsCount', value: 16), string(name: 'testList', value: *Test), string(name: 'runId', value: runId)]
где threadsCount — кол-во потоков для распараллеливания тестов, testList — список тестов для запуска, runId — идентификатор прогона тестов. Для чего нужны эти параметры, расскажу далее.
Snippet Generator удобен тем, что подсвечивает шаги в зависимости от установленных плагинов. Если у вас есть, например, Allure-report, то плагин сразу покажет наличие расширения и поможет сгенерировать код, который будет работать.
Вставим сгенерированный код степа в пайплайн на следующем шаге.
Запуск тестов с помощью Pipeline — инструкция
Итак, давайте с помощью Declarative sections generator создадим пайплайн. В нем нужно указать директивы: pipeline, agent (агент, на котором будет запускаться пайплайн), а также stages и steps (вставка ранее сгенерированного кода).
Так получится пайплайн, который запустит джобу, созданную на предыдущем шаге через UI.
pipeline < agent < label any >stages < stage("start test") < steps< build job: '/QA/TestJob', parameters: [ string(name: 'threadsCount', value: threadsCount), string(name: 'runId', value: runId), string(name: 'testList', value: testList)] >> > >
Напомню, что в параметры для запуска тестов мы передавали количество потоков и список тестов. Теперь к этому добавляем параметр runId (идентификатор прогона тестов) — он понадобится позднее для перезапуска конкретного сьюта тестов.
Чтобы запустить пайплайн, нужно создать проект.
- New Item -> Pipeline.
Для этого нужно нажать на кнопку «Создать проект» и выбрать не джобу со свободной конфигурацией, а джобу Pipeline — осталось указать ей имя.
- Добавить параметры runId, threadsCount, testList.
- Склонировать из Git.
Пайплайн можно описать непосредственно как код и вставить в поле, но для версионирования нужно затягивать пайплайн из Git. Обязательно указываем путь до скрипта с пайплайном.
Готово, джобу можно запускать.
Хотим добавить немного дожатий
На этом этапе у нас уже готова джоба для запуска проекта с автотестами. Но хочу напомнить, что наша задача — не просто запускать тесты, а добавить им стабильности, исключив падения из-за внешних факторов. Для достижения этого результата было принято решение дожать автотесты.
Для реализации нужно:
- вынести шаг запуска тестов в библиотечную функцию (shared steps),
- получить упавшие тесты из прогона,
- добавить условия перезапуска.
Теперь немного подробнее про каждый из этих шагов.
Многократное использование шагов — Shared Steps
В процессе написания пайплайнов я столкнулся с проблемой постоянного дублирования кода (часто используемых степов). Этого хотелось избежать.
Решение нашлось не сразу. Оказывается, для многократного использования кода в Jenkins есть встроенный механизм — shared libraries, который позволяет описать методы один раз и затем применять их во всех пайплайнах.
Существуют два варианта подключения этой библиотеки.
- Написанный проект/код подключить через UI Jenkins. Для этого требуются отдельные права на добавление shared libraries или привлечение девопс-специалистов (что не всегда удобно).
- Хранить код в отдельном проекте или в проекте с пайплайнами. При использовании этот код подключается как динамическая библиотека и выкачивается каждый раз при запуске пайплайна.
Мы используем второй вариант — размещаем shared steps в проекте с пайплайнами.
Для этого в проекте нужно:
- создать папку var,
- в ней создать файл с названием метода, который планируется запускать — например, gradlew.groovy,
- стандартно определить имя метода (должен называться call), то есть написать «def call» и определить входящие параметры,
- в теле метода можно написать произвольный Groovy-код и/или Pipeline-степы.
//Подключение библиотеки //https://www.jenkins.io/doc/book/pipeline/shared-libraries/ - описание с картинками library identifier: 'pipeline-shared-lib' pipeline < stages < stage("Build") < steps < gradlew(tasks: ["build"]) // вызов метода из библиотеки >> > >
def call(Map> parameters) < // стандратное имя для глобального метода def tasks = parameters["tasks"] def args = parameters["args"] ?: [] sh "./gradlew $$" // произвольный groovy код + pipeline-методы >
Вынесение запуска тестов в shared steps в /var
- Выносим startTests.groovy в /var.
Во-первых, нужно вынести запуск тестов в отдельный метод. Выглядит это так — создаем файл, называем метод def call, берем кусок кода, который был в пайплайне, и выносим его в этот step.
def call(Map params) < def threadsCount = params["threadsCount"] ?: "3" def testList = params["testList"] ?: "*Test" stage("start test job") < runTest = build job: '/QA/TestJob', parameters: [ string(name: 'threadsCount', value: threadsCount), string(name: 'runId', value: runId), string(name: 'testList', value: testList)], propagate: false >>
Для передачи параметров используется Map. Почему не передавать каждый параметр отдельно? Это не очень удобно, т.к. в Groovy параметры не обозначены по названиям. При использовании Map синтаксис позволяет указать “key:value“ через двоеточие. В коде (в месте вызова метода) это отображается наглядно.
Структура проекта будет выглядеть так.
- Подключение shared steps как внешней библиотеки.
Дальше нужно добавить вынесенные шаги в пайплайн. При динамической подгрузке библиотек (во время запуска пайплайна) эти шаги выкачиваются из репозитория и подключаются на лету.
Сделать это можно с помощью сниппет-генератора — выбираем степ library и указываем ветку, в которую все будет собираться и репозиторий. Дальше нажимаем кнопку «Сгенерировать» и вставляем получившийся пайплайн.
library changelog: false, identifier: 'shared-lib@master', retriever: modernSCM([ $class : 'GitSCMSource', remote : 'ssh://git@bitbucket.ru/qa/jenkins-groovy-scripts.git'])
Теперь после подключения shared steps вместо шага запуска тестов build нужно вставить startTest. Не забудьте, что имя метода должно совпадать с именем файла.
Теперь наш пайплайн выглядит так.
//Динамическое подключение библиотеки library changelog: false, identifier: 'shared-lib@master', retriever: modernSCM([ $class : 'GitSCMSource', remote : 'ssh://git@bitbucket.ru/qa/jenkins-groovy-scripts.git']) pipeline < agent < label any >stages < stage("start test") < steps< startTests(runId: runId ) //Вызов метода из библиотеки >> > >
Первый шаг реализован, теперь можно многократно запускать тесты в разных местах. Переходим к 2 шагу.
Получение упавших тестов из прогона
Теперь нам нужны упавшие тесты. Каким образом их извлечь?
- Установить в Jenkins плагин JUnit Test Result Report и использовать его API.
- Взять результаты прогона JUnit (обычно в формате XML), распарсить и извлечь нужные данные.
- Запросить список упавших тестов из нужного места.
В нашем случае таким местом является собственный написанный сервис — Reporter. Во время прогона тестов результат по каждому из них отправляется именно туда. В конце прогона делаем http-запрос и получаем упавшие тесты.
Добавление условий перезапуска
На этом шаге следует добавить getFailedTests.groovy в /var. Представим, что у вас есть такой сервис — Reporter. Нужно назвать файл getFailedTests, сделать запрос httpRequest в этот сервис и распарсить его.
def call(String runId) < def response = httpRequest httpMode: 'GET', url: "http://reporter:8080/failedTests/$runId" def json = new JsonSlurper().parseText(response.content) return json.data >
Отлично, второй шаг выполнен. Осталось добавить ту самую изюминку — условия перезапуска. Но сначала посмотрим, что получается.
Есть запуск тестов и получение результатов прогона. Теперь нужно добавить те самые условия, которые будут говорить: «Если все хорошо, завершай прогон. Если плохо, давай перезапускать».
Условия перезапуска
Какие условия для перезапуска можно реализовать?
Приведу часть условий, которые используем мы.
1) Если нет упавших тестов, прогон завершается.
if (countFailedTests == 0)
2) Как я уже писал выше, на тестовой среде ресурсы ограничены, и бывает такое, что ТС захлебывается в большом количестве параллельных тестов. Чтобы на дожатии избежать падений тестов по этой причине, понижаем число потоков на повторном запуске. Именно для этого при создании джобы и в самом пайплайне мы добавили параметр threadsCount.
Если и после уменьшения потоков тесты не дожимаются (количество упавших тестов на предыдущем прогоне равно числу упавших тестов на текущем), тогда считаем, что дело не во влиянии ТС на тесты. Останавливаем прогон для анализа причин падений — и тем самым предотвращаем последующий холостой запуск.
if (countFailedTests == previousCountFailedTests)
3) Третья и самая простая проверка состоит в том, что если падает большое количество тестов, то дожимать долго. Скорее всего, причина падений какая-то глобальная, и ее нужно изучать.
Для себя мы определили: если тестов > 40, дожимать не автоматически не будем, потому что 40 наших E2E могут проходить порядка 15 минут.
if (countFailedTests > FAILEDTESTSTRESHOLD)
Получился метод:
def call(Map params) < assert params["runId"] def threadsCount = params["threadsCount"] ?: "8" def testList = params["testList"] ?: "*Test" def runId = params["runId"] int FAILED_TESTS_TRESHOLD = 40 def countFailedTests = 0 def failedTests int run = 1 boolean isFinished = false int threads = threadsCount as int while (run else < if (countFailedTests >0) < threads = reduceThreads(threads) testList = failedTests.toString().minus('[').minus(']').minus(' ') startTests() >> stage("check $_run result ") < failedTests = getFailedTests(runId) def previousCountFailedTests = countFailedTests countFailedTests = failedTests.size() if (countFailedTests == 0) < echo "FINISHED" isFinished = true >if (countFailedTests > FAILED_TESTS_TRESHOLD) < echo "TERMINATED - too much failed tests >$" isFinished = true > if (countFailedTests == previousCountFailedTests) < echo "TERMINATED - no one new passed test after retry" isFinished = true >> run += 1 > >
Последние два условия — так называемые fail fast. Они позволяют при глобальных проблемах на тестовом стенде не делать прогоны, которые не приведут к дожиму тестов, но будут занимать ресурсы тестового стенда.
Итоговый pipeline
Итак, все 3 шага реализованы — итоговый пайплайн выглядит так.
library changelog: false, identifier: 'shared-lib@master', retriever: modernSCM([ $class : 'GitSCMSource', remote : 'ssh://git@bitbucket.ru/qa/jenkins-groovy-scripts.git']) assert runId != null pipeline < agent < label any >stages < stage("start test") < steps < testsWithRerun(runId: runId) >> > >
Визуализация с Blue Ocean
Как все это выглядит при прогоне в Jenkins? У нас, к примеру, для визуализации в Jenkins установлен плагин Blue Ocean.
На картинке ниже можно увидеть, что:
- запустился метод testwith_rerun,
- прошел первый запуск,
- прошла проверка упавших тестов,
- запустился второй прогон,
- после успешной проверки джоба завершилась.
Вот так выглядит визуализация нашего настоящего прогона.
В реальном примере две ветки: мы параллельно запускаем два проекта с тестами (на Java и Kotlin). Можно заметить, что тесты одного проекта прошли с первого раза, а вот тесты другого пришлось дожимать еще раз. Таким образом визуализация помогает найти этап, на котором падают тесты.
А так выглядит реальный timeline приемки релиза.
После первого прогона отправили дожимать упавшие тесты. Во второй раз упавших тестов намного меньше, дожимаем в третий раз — и вуаля, успешный build.
Итог
Мы перенесли логику перезапусков упавших тестов из тестового проекта на уровень выше — на CI. Таким образом сделали механизм перезапуска универсальным, более гибким и независимым от стека, на котором написаны автотесты.
Раньше наши тесты дожимались безусловно, по несколько раз, с неизменным количеством параллельных потоков. При перегрузке тестового стенда, некорректных настройках тестового окружения либо каких-то других проблемах — красные тесты перезапускались фиксированное число раз без шансов дожаться. В худшем случае прогоны могли длиться часами. Добавив условия fail fast, мы сократили это время.
При падении тестов инженер, ответственный за приемку, в некоторых ситуациях вручную стартовал джобу перезапуска, выполняя прогон с меньшим числом потоков. На это тоже уходило время. Добавив в условия пайплайна уменьшение числа потоков на перезапуске, мы сократили и это время.
Какой профит мы получили:
- уменьшили time-to-market тестируемых изменений,
- сократили длительность аренды тестового стенда под приемочное тестирование,
- увеличили пропускную способность очереди приемочного тестирования,
- не завязаны на тестовый фреймворк («под капотом» может быть что угодно — дожатия будут работать),
- поделились знаниями об использовании Jenkins Pipeline.
Примеры кода выложены на GitHub. Если будут вопросы, задавайте — обязательно отвечу.
P.S.
А еще в нашей компании принято награждать ачивкой за какое-то достижение. Статья получилось достаточно подробной и многословной, а потому вас, дочитавших до конца этот лонгрид, хотелось бы поощрить.
Помимо +100 к опыту и знаниям Jenkins вы получаете ачивку «Ю Academic»! 🙂
Jenkins Pipeline: заметки об оптимизации. Часть 1

Меня зовут Илья Гуляев, я занимаюсь автоматизацией тестирования в команде Post Deployment Verification в компании DINS.
В DINS мы используем Jenkins во многих процессах: от сборки билдов до запуска деплоев и автотестов. В моей команде мы используем Jenkins в качестве платформы для единообразного запуска смоук-проверок после деплоя каждого нашего сервиса от девелоперских окружений до продакшена.
Год назад другие команды решили использовать наши пайплайны не только для проверки одного сервиса после его обновления, но и проверять состояние всего окружения перед запуском больших пачек тестов. Нагрузка на нашу платформу возросла в десятки раз, и Jenkins перестал справляться с поставленной задачей и стал просто падать. Мы быстро поняли, что добавление ресурсов и тюнинг сборщика мусора могут лишь отсрочить проблему, но не решат ее полностью. Поэтому мы решили найти узкие места Jenkins и оптимизировать их.
В этой статье я расскажу, как работает Jenkins Pipeline, и поделюсь своими находками, которые, возможно, помогут вам сделать пайплайны быстрее. Материал будет полезен инженерам, которые уже работали с Jenkins, и хотят познакомиться с инструментом ближе.
Что за зверь Jenkins Pipeline
Jenkins Pipeline — мощный инструмент, который позволяет автоматизировать различные процессы. Jenkins Pipeline представляет собой набор плагинов, которые позволяют описывать действия в виде Groovy DSL, и является преемником плагина Build Flow.
Скрипт для плагина Build Flow исполнялся напрямую на мастере в отдельном Java-потоке, который выполнял Groovy-код без барьеров, препятствующих доступу к внутреннему API Jenkins. Данный подход представлял угрозу безопасности, что впоследствии стало одной из причин отказа от Build Flow, и послужило предпосылкой для создания безопасного и масштабируемого инструмента для запуска скриптов — Jenkins Pipeline.
Подробнее об истории создания Jenkins Pipeline можно узнать из статьи автора Build Flow или доклада Олега Ненашева о Groovy DSL в Jenkins.
Как работает Jenkins Pipeline
Теперь разберемся, как работают пайплайны изнутри. Обычно говорят, что Jenkins Pipeline — совершенно другой вид заданий в Jenkins, непохожий на старые добрые freestyle-джобы, которые можно накликать в веб-интерфейсе. С точки зрения пользователя это, может, и выглядит так, но со стороны Jenkins пайплайны — набор плагинов, которые позволяют перенести описание действий в код.
Сходства Pipeline и Freestyle джобы
- Описание джобы (не шагов) хранится в файле config.xml
- Параметры хранятся в config.xml
- Триггеры тоже хранятся в config.xml
- И даже некоторые опции хранятся в config.xml
Правда в том, что параметры, описанные в Pipeline, автоматически добавятся в раздел настройки в веб-интерфейсе при запуске джобы. Верить мне можно потому, что этот функционал в последней редакции писал я, но об этом подробнее во второй части статьи.
Отличия Pipeline и Freestyle джобы
- На момент старта джобы Jenkins ничего не знает про агента для выполнения джобы
- Действия описываются в одном groovy-скрипте
Запуск Jenkins Declarative Pipeline
Процесс запуска Jenkins Pipeline состоит из следующих шагов:

- Загрузка описания задания из файла config.xml
- Старт отдельного потока (легковесного исполнителя) для выполнения задания
- Загрузка скрипта пайплайна
- Построение и проверка синтаксического дерева
- Обновления конфигурации задания
- Объединение параметров и свойств заданных в описании задания и в скрипте
- Сохранение описания задания в файловую систему
- Выполнение скрипта в groovy-песочнице
- Запрос агента для всего задания или отдельного шага
При старте pipeline-джобы Jenkins создает отдельный поток и направляет задание в очередь на выполнение, а после загрузки скрипта определяет, какой агент нужен для выполнения задачи.
Для поддержки данного подхода используется специальный пул потоков Jenkins (легковесных исполнителей). Можно заметить, что они выполняются на мастере, но не затрагивают обычный пул исполнителей:

Количество потоков в данном пуле не ограничено (на момент написания статьи).
Работа параметров в Pipeline. А также триггеров и некоторых опций
Обработку параметров можно описать формулой:

Из параметров джобы, которые мы видим при запуске, сначала удаляются параметры Pipeline из предыдущего запуска, а уже потом добавляются параметры заданные в Pipeline текущего запуска. Это позволяет удалить параметры из джобы, если они были удалены из Pipeline.
Как это работет изнутри?
Рассмотрим пример config.xml (файл, в котором хранится конфигурация джобы):
jenkins.model.BuildDiscarderProperty parameter_3 false parameter_1 false parameter_2 false parameter_3 false 30 10000 -1 -1 false false /path/to/jenkinsfile/ true Jenkinsfile true false
В секции properties находятся параметры, триггеры и опции, с которыми будет запускаться джоба. Дополнительная секция DeclarativeJobPropertyTrackerAction служит для хранения параметров, заданных только в пайплайне.
При удалении параметра из пайплайна, он будет удлён как из DeclarativeJobPropertyTrackerAction так и из properties, так как Jenkins будет знать, что параметр был определён только в пайплайне.
При добавлении параметра ситуация обратная, параметр добавится DeclarativeJobPropertyTrackerAction и properties, но только в момент исполнения пайплайна.
Именно поэтому если задать параметры только в пайплайне, то они будут недоступны при первом запуске.
Выполнение Jenkins Pipeline
Когда Pipeline-скрипт загружен и скомпилирован, начинается процесс выполнения. Но этот процесс состоит не только из выполнения groovy. Я выделил основные тяжеловесные операции, которые выполняются в момент исполнения джобы:
Выполнение Groovy-кода
Скрипт пайплайна всегда выполняется на мастере — об этом нельзя забывать, чтобы не создавать лишнюю нагрузку на Jenkins. На агенте исполняются только шаги, которые взаимодействуют с файловой системой агента или системными вызовами.

В пайплайнах есть замечательный плагин, который позволяет делать HTTP запросы. В добавок ответ можно сохранять в файл.
httpRequest url: 'http://localhost:8080/jenkins/api/json?pretty=true', outputFile: 'result.json'
Изначально может показаться, что данный код должен полностью выполняться на агенте, послать с агента запрос, а ответ сохранить в файл result.json. Но всё происходит наоборот, и запрос выполняется с самого дженкинса, а для сохранения содержимое файла копируется на агент. Если дополнительная обработка ответа в пайплайне не требуется, то советую заменять такие запросы на curl:
sh 'curl "http://localhost:8080/jenkins/api/json?pretty=true" -o "result.json"'
Работа с логами и артефактами
Независимо от того, на каком агенте выполняются команды, логи и артефакты обрабатываются и сохраняются на файловую систему мастера в режиме реального времени.
Если в пайплайне используются секреты (credentials), то перед сохранением логи дополнительно фильтруются на мастере.

Сохранение шагов (Pipeline Durability)
Jenkins Pipeline позиционирует себя как задача, которая стоит из отдельных кусочков, которые независимы и могут быть воспроизведены при падении мастера. Но за это приходится платить дополнительными записями на диск, потому что в зависимости от настроек задачи шаги с различной степенью детализации сериализуются и сохраняются на диск.

В зависимости от параметра живучести (pipeline durability) шаги графа пайплайна будут храниться в одном или нескольких файлах для каждого запуска джобы. Выдержка из документации:
В плагине workflow-support для хранения степов (FlowNode) используется класс FlowNodeStorage и его реализации SimpleXStreamFlowNodeStorage и BulkFlowNodeStorage.
- FlowNodeStorage использует кэширование в памяти для объединения операций записи на диск. Буфер автоматически записывается во время выполнения. Как правило, вам не нужно беспокоиться об этом, но имейте в виду, что сохранение FlowNode не гарантирует, что он будет немедленно записан на диске.
- SimpleXStreamFlowNodeStorage использует по одному небольшому XML-файлу для каждого FlowNode — хотя мы используем кеш с soft-reference в памяти для узлов, это приводит к гораздо худшей производительности при первом прохождении через степы (FlowNodes).
- BulkFlowNodeStorage использует один XML-файл большего размера со всеми FlowNodes в нем. Этот класс используется в режиме живучести PERFORMANCE_OPTIMIZED, который записывает гораздо реже. Как правило, это намного эффективнее, потому что одна большая потоковая запись выполняется быстрее, чем группа небольших записей, и сводит к минимуму нагрузку на ОС для управления всеми крошечными файлами.
Оригинал
Storage: in the workflow-support plugin, see the ‘FlowNodeStorage’ class and the SimpleXStreamFlowNodeStorage and BulkFlowNodeStorage implementations.
- FlowNodeStorage uses in-memory caching to consolidate disk writes. Automatic flushing is implemented at execution time. Generally, you won’t need to worry about this, but be aware that saving a FlowNode does not guarantee it is immediately persisted to disk.
- The SimpleXStreamFlowNodeStorage uses a single small XML file for every FlowNode — although we use a soft-reference in-memory cache for the nodes, this generates much worse performance the first time we iterate through the FlowNodes (or when)
- The BulkFlowNodeStorage uses a single larger XML file with all the FlowNodes in it. This is used in the PERFORMANCE_OPTIMIZED durability mode, which writes much less often. It is generally much more efficient because a single large streaming write is faster than a bunch of small writes, and it minimizes the system load of managing all the tiny files.
Сохранённые шаги можно найти в директории:
$JENKINS_HOME/jobs/$JOB_NAME/builds/$BUILD_ID/workflow/
4 5 org.jenkinsci.plugins.workflow.support.steps.StageStep name Declarative: Checkout SCM true 1600855071994
Итоги
Надеюсь, данный материал был интересен и помог лучше понять, что из себя представляют пайплайны и как они работают изнутри. Если у вас остались вопросы — делитесь ими ниже, буду рад ответить!
Во второй части статьи я рассмотрю отдельные кейсы, которые помогут найти проблемы с Jenkins Pipeline и ускорить ваши задачки. Узнаем, как решать проблемы параллельного запуска, рассмотрим параметры «живучести» и обсудим, зачем профилировать Jenkins.
- Блог компании DINS
- Системы сборки
- DevOps
