Делаем Jenkins Pipeline: шаг за шагом
Привет. Если ты обратил внимание на этот материал, то значит ты начинаешь разбираться в мире Jenkins. Самым сложным в любом деле является начало. На первом этапе окружает много незнакомых и непонятных терминов, сложно понять логику процесса и последовательность действий. Кажется, что это какая-то магия. Чтобы первые шаги были чуточку легче, я опишу простой пример, который можно использовать в качестве основы для реальных задач.
Я буду отталкиваться от того, что у тебя уже установлен Jenkins. Установка Jenkins’а неплохо описана и показана в сети. Мы же будем смотреть на то, как создается Jenkins Pipeline или труба (конвейер) Jenkins.
Для чего всё это
Что вообще такое Jenkins? Jenkins это крутая программа, которая реализует практики DevOps. Практики DevOps, в свою очередь, это логичные действия, которые позволяют повысить качество создаваемых продуктов. Под «качеством» будем понимать сразу много преимуществ – уменьшение количества ошибок в продукте, снижение Time-to-Market, повышение пользовательской лояльности и т.д.
Jenkins является «конвейером» поставки ПО от среды разработки в промышленную среду. «Конвейер», конечно же, условный. Jenkins выполняет шаг за шагом простейшие операции над кодом словно это автомобильный завод, на котором ваш фольксваген или форд движется по цехам и проходит разные этапы превращения металла в автомобиль. Такой «конвейер» в Jenkins называют Pipeline.
О DevOps и его практиках
DevOps, грубо говоря, регламентирует «завод» и «конвейер» на нём.
Одной из практик DevOps является CI/CD/CDP. Эта практика описывает шаги «конвейера».
CI (Continuous Integration, непрерывная интеграция) – начальная стадия «конвейера» по сборке кода и загрузке собранного ПО в среду разработки.
CD (Continuous delivery, непрерывная поставка) – является продолжением CI. В этой практике производится автоматизированное развертывание на тестовую среду продукта и разнообразные тесты над ним.
CDP (Continuous Deployment, непрерывное развертывание) – поставка результатов работы CI и CD практик в промышленную среду.
Jenkins, в свою очередь, может реализовать CI/CD/CDP на практике.
Описание примера
Теперь, имея представление что такое Jenkins и для чего он нужен, сделаем тестовую трубу, которая реализует в очень урезанном виде CI/CD практику.
В качестве разворачиваемого продукта будем использовать SSIS-пакет. SSIS (SQL Server Integration Services) – это инструмент создания решений по интеграции и преобразованию данных. То есть, решение по переносу данных из одного источника в другой с возможностью преобразования этих данных в процессе переноса.
Подобных примеров в сети я почти не встречал, поэтому убью сразу двух зайцев – построю трубу Jenkins на интересном примере из моей практики. Принцип создания труб одинаковый, поэтому вы сможете применить пример из статьи для своих целей.
Чтобы было ещё интереснее, сделаем задачу со звёздочкой – в одном SSIS-проекте будет сразу несколько SSIS-пакетов.
Опишу шаги, которые будет делать труба:
- Чекаут из Git (в моём случае Bitbucket)
- Сборка SSIS-проекта
- Развертывание собранного файла проекта в среду разработки.
Мой подопытный SSIS-кролик выглядит следующим образом:

Внутри проекта «AUDIT_Import_ALL» находится четыре SSIS-пакета с расширением «.dtsx». Если этот проект собрать, то получится один файл с именем «AUDIT_Import_ALL» и расширением «.ispac». Данный файл предназначен для развертывания проекта в MS SQL Server.
Подготовительные шаги
Проект «AUDIT_Import_ALL» загружен в Bitbucket. В репозиторий проекта я добавлю папку, содержащую Jenkinsfile.
Jenkinsfile – это простой текстовый файл с кодом на языке Groovy, который используется для конфигурации трубы Jenkins.

Теперь я создам тестовую трубу в самом Jenkins. Для этого на главной странице пространства Jenkins нажимаю «New Item», далее выбираю Pipeline и задаю имя.


После нажатия «ОК», открывается окно с настройками. Не будем подробно останавливаться на этом окне. Для тестового примера мне никакие настройки здесь не нужны, кроме раздела с расширенными настройками. А именно, окно настройки «Pipeline».
В нём я свяжу Jenkinsfile, который находится в Bitbucket, с только что созданной трубой Jenkins. Для этого в пункте «Definition» выбираем «Pipeline script from SCM». Этим пунктом я сообщаю Jenkins что мой Jenkinsfile лежит в SCM (source control management – система контроля версий).
На следующем шаге я выбираю SCM, где лежит мой файл. В моём случае это Git (Bitbucket). Далее я прописываю путь до репозитория в SSH формате (возможно и в HTTPS) и указываю учетные данные в поле «Credentials» (или добавляю новую учетную запись через кнопку «Add»).
Jenkinsfile лежит в master-ветке репозитория в Bitbucket, поэтому я не трогаю пункт «Branch Specifier (blank for ‘any’)». Так же, не трогаю следующие два пункта. А вот
пункт «Script Path» мне необходимо поменять, потому что по умолчанию Jenkins будет
искать Jenkinsfile в корне репозитория. Отдельная папка нужна затем, что файлов для работы Jenkins-трубы может быть несколько и содержать их в корне проекта будет просто не удобно.

Jenkins SSIS стенд
Хочу затронуть архитектурное решение моего Jenkins-стенда. Для тех, кто не будет строить DevOps для SSIS, можно перейти сразу к следующему разделу.
Архитектурно Jenkins строится по принципу master-slave. Есть один ведущий узел, который управляет множеством ведомых. У меня уже была развернута master-нода Jenkins. К ней я подключил slave-ноду, которая была создана под задачу DevOps SSIS. На этом сервере находится программа SSISBuild.exe, предоставляемая Microsoft как раз для наших нужд. SSISBuild предназначена для удаленной сборки SSIS-проектов и не требует установленной Visual Studio или среды выполнения SSIS.
Сервер, куда я буду развертывать SSIS-проект, содержит установленный MS SQL Server с установленной службой Integration Services. На этом сервере установлена вторая программа для DevOps SSIS – SSISDeploy.exe. SSISDeploy предназначена для развертывания файлов проектов (файлов с расширением ispac).
Более подробную информацию о SSISBuild и SSISDeploy вы сможете найти по ссылке.
Jenkinsfile
Итак, труба настроена, связь с Bitbucket установлена, необходимые сервера на связи. Осталось совсем чуть-чуть до запуска. Разберем Jenkinsfile чтобы понимать, как будут происходить те шаги, которые я описал в начале статьи.
Наиболее фундаментальной частью конвейера является «step». По сути, шаги говорят Jenkins, что делать, и служат базовым строительным блоком для синтаксиса конвейеров.
Все операции с конвейером должны быть заключены в блок «pipeline»:
pipeline < /* Ваши инструкции для конвейера */ >
Блок «pipeline» содержит разделы. Первым разделом является «agent». Раздел «agent» указывает, где будет выполняться труба Jenkins. Раздел должен быть определен на верхнем уровне внутри блока «pipeline».
В моём случае конвейер будет работать на сервере TestNode:
agent < node < label 'TestNode' >>
Раздел «options» позволяет настраивать параметры конвейера непосредственно из Jenkinsfile. Эти параметры также можно задавать в графическом интерфейсе Jenkins.
ansiColor('xterm') timestamps() disableConcurrentBuilds() timeout(time: 1, unit: 'HOURS') buildDiscarder(logRotator(artifactDaysToKeepStr: '7', artifactNumToKeepStr: '10', daysToKeepStr: '7', numToKeepStr: '50')) >
Раздел «parameters» задает список параметров, которые пользователь должен предоставить при запуске трубы. Заданные пользователем значения становятся доступными для шагов конвейера через объект params. Я создам два булевых параметра, с помощью которых можно будет управлять запуском шагов конвейера:
parameters
Раздел «environment» определяет глобальные и локальные (в пределах конкретного «step») переменные в Jenkinsfile. Глобальные переменные удобно использовать для подстановки статических значений:
environment
Раздел «stages» является местом, где будет происходить основная часть работы конвейера. Раздел делится на этапы, которые содержат описание шагов. У меня будет три этапа:
stages < // Извлекаем проект из Bitbucket stage('Checkout') < >// Собираем проект. Получаем на выходе пакетный файл AUDIT_Import_ALL.ispac stage('Build') < >// Загружаем архив с проектом на удаленный сервер. Деплоим его на MS SQL Server stage('Deploy') < >>
Теперь рассмотрим шаги на каждом этапе.
1) Checkout stage('Checkout') < steps < // Удаление всех файлов из рабочего каталога на сервере TestNode bat 'del /F /S /Q *.*' // Удаление всех папок из рабочего каталога на сервере TestNode bat 'for /d %%x in (.\\*) do @rd /s /q %%x' // Вывод echo в консоль Jenkins echo 'step Git Checkout' // Извлечение из системы контроля версий в рабочий каталог на сервере TestNode checkout scm >> 2) Build stage('Build') < // Команда when позволяет конвейеру определять, следует ли выполнять этап в зависимости от заданного условия. when < // Заданное условие expression. Оно задается пользователем при запуске конвейера и передается в скрипт через параметр "BUILD" expression < return params.BUILD >> // Начало шага сборки SSIS-проекта steps < // Вызов функции PrintStage(). Её мы рассмотрим далее. PrintStage() echo "step Build Solution" /* Вызов SSISBuild.exe на TestNode. Кстати, здесь применяется три подстановки: - переменная среды $- абсолютный путь рабочей области. - глобальные переменные $ и $ - они задавались в разделе environment */ bat "$ -p:$\\$" > > 3) Deploy stage("Deploy to Dev Env") < when < expression < return params.DEPLOY >> steps < PrintStage() // Использование стандартного плагина Jenkins для архивации собранного SSIS-проекта zip zipFile: 'archive.zip', archive: false // Самое интересное. Для деплоя полученного файла будем применять службу удаленного управления Windows WinRM в PowerShell // Используем учетную запись 'WINRM_PASS' для извлечения SecretText из Jenkins. Учетная запись заведена в Jenkins. Как её добавить я покажу ниже withCredentials([string(credentialsId: 'WINRM_PASS', variable: 'WINRM_PASS')]) < // Применение скриптового синтаксиса внутри декларативного. script < // Определяем переменные def err def stdout = powershell label: 'deploy', returnStdout: true, script: ''' # Задаем пароль учетной записи WinRM в SecureString $pw = convertto-securestring -AsPlainText -Force -String $env:WINRM_PASS # Задаем учетные данные для создания сессии WinRM $cred = new-object -typename System.Management.Automation.PSCredential -argumentlist "Domain\\User",$pw # Открываем сессию $s = New-PSSession -ComputerName -Credential $cred #Создаем папку на удаленном сервере для копирования архива $remotePath = \'D:\\DIGAUD\\TestSSISPipeline\' $job = Invoke-Command -Session $s ` -ScriptBlock < if (!(Test-Path -Path \'D:\\DIGAUD\\TestSSISPipeline\')) > ` -AsJob Wait-Job $job $r = Receive-Job $job # Копируем архив $path = Get-Location $dest = Join-Path $path "archive.zip" Copy-Item -Path $dest ` -Destination $remotePath ` -ToSession $s # Распаковываем архив $job = Invoke-Command -Session $s ` -ScriptBlock < Expand-Archive -LiteralPath \'D:\\Jenkins\\TestSSISPipeline\\archive.zip' -DestinationPath \'D:\\Jenkins\\TestSSISPipeline\' >` -AsJob Wait-Job $job $r = Receive-Job $job #Деплоим $job = Invoke-Command -Session $s ` -ScriptBlock < C:\\SSIS_DEV_OPS\\ssisdeploy.exe -s:\"D:\\Jenkins\\TestSSISPipeline\\AUDIT_Import_ALL\\bin\\Development\\AUDIT_Import_ALL.ispac\" -d:\"catalog;/SSISDB/AUDIT_Import_ALL;mssql_server,port\" -at:win >` -AsJob Wait-Job $job $r = Receive-Job $job Remove-PSsession $s ''' > > > > >
Раздел «post» определяет дополнительные действия, которые будут выполняться после завершения работы основного кода конвейера или этапа (в зависимости где стоит post).
post < // Код ниже будет выполнен вне зависимости от статуса сборки трубы или этапа always < script < # Устанавливаю результат сборки трубы currentBuild.result = currentBuild.result ?: 'SUCCESS' # Уведомляю Bitbucket о результате сборки notifyBitbucket() >> >
На этом мой блок «pipeline» в Jenkinsfile заканчивается.
Далее располагается метод «PrintStage», который встречался на этапах Build и Deploy:
// Метод, который принимает на вход один текстовый аргумент. По умолчанию аргумент пустой void PrintStage(String text="")< // Применение тернарного оператора // Если аргумент text пустой, в консоль Jenkins будет выведено десять звёздочек, название этапа, десять звёздочек // Если аргумент text не пустой, в консоль Jenkins будет выведено его значение text=="" ? println ('* '*10 + env.STAGE_NAME.toUpperCase() + " *"*10) : println (text) >
Теперь всё готово к запуску. Открываю окно нашего конвейера и нажимаю кнопку «Собрать с параметрами».

Откроется окно с параметрами, которые я определил в разделе «parameters». Выбираю этапы, которые хочу запустить и нажимаю «собрать».

Далее, труба начинает собираться и если всё сделано правильно, то Jenkins нарисует красивую картинку:

Как добавить учетную запись в Jenkins
Для этого на главной странице вашего пространства нажмите кнопку «Credentials», далее выбрать «Folder».

Наведите курсор на пункт «Global credentials (unrestricted)» и у вас появится «галочка» для вызова выпадающего списка. Нажмите на неё. Всплывет маленькое окошко «Add credentials» — нажимайте!

Здесь вы можете задать вашу учетную запись. В моём примере с учетной записью WinRM, я использовал «Secret text» для хранения пароля.

Выводы
По итогу, мы сделали трубу, которая собирает и деплоит SSIS-пакеты на MS SQL Server. Данный пример является учебным , но его можно взять за основу и доработать под ваши задачи. Например, если вы делаете веб-приложение на ASP.NET, то вместо ssisdeploy можно использовать webdeploy. По аналогии можно добавить шаги с разнообразными тестами, созданием release notes, загрузкой дистрибутива в централизованное хранилище и т.д.
Какие преимущества даёт Jenkins? Он автоматизирует многие рутинные процессы, связанные с разработкой программного обеспечения. Например, запуск трубы с прохождение различных тестов при коммите в нужную ветку в Bitbucket. Такой подход уменьшит количество ошибок. Применение Jenkins объединяет процессы сборки, тестирования и внедрения и создает четко выстроенный подход к созданию продукта.
Буду рад, если моя статья была кому-то полезна.
Если у вас остались вопросы оставляйте их в комментариях – я обязательно отвечу.
Руководство по Jenkins
В руководстве мы расскажем, зачем нужен Jenkins, а также покажем, как установить Jenkins на Ubuntu.
Jenkins — это сервис, с помощью которого можно автоматизировать процесс непрерывной интеграции программного обеспечения. Непрерывная интеграция (Continuous Integration) — один из этапов разработки, на котором происходит сборка рабочих копий проекта в единый макет-черновик, их тестирование, доставка или развёртывание программного обеспечения. Во время интеграции можно выявить слабые места и возможные ошибки в проекте и сразу их исправить.
На этапе интеграции разработчики объединяют код вручную, что занимает много времени. Jenkins позволяет автоматизировать этот этап. Сервис подойдёт как для профессионалов, так и для начинающих специалистов.
Разработка на Jenkins
Инновационное решение на основе Hudson как инструмент для непрерывной интеграции проектов разной сложности.

Преимущества Jenkins:
- имеет открытый исходный код, написанный на Java;
- поддерживает свыше 1000 плагинов для интеграции с инструментами тестирования, разработки и деплоя;
- работает больше чем в двух средах одновременно без потери эффективности;
- Jenkins хорошо подойдёт для проектов, которые написаны на Python;
- оптимизирует рабочий процесс: вам не нужно нанимать штат профессиональных программистов, в Jenkins можно разобраться даже без специальной подготовки;
- выявляет и устраняет нестандартные ошибки без привлечения человека;
- минимизирует количество ошибок, возникающих в связи с человеческим фактором.
Jenkins можно установить на Windows, macOS, Debian, Ubuntu, CentOS и другие операционные системы. Также Jenkins можно установить через системные пакеты, Docker или запустить автономно на любом компьютере с настроенной Java Runtime Environment (JRE).
Jenkins можно установить с официального сайта одним из двух способов: скачать из раздела «Download» или использовать команды из раздела «Documentation». Для Jenkins документация на русском не разработана, однако именно в этом разделе можно найти рекомендации для быстрой установки. Поэтому установим Jenkins на Ubuntu версий 16.04/18.04/20.04 вторым способом.
Как установить Jenkins
Для Jenkins системные требования следующие:
- 256 Мб оперативной памяти,
- минимум 1 Гб дискового пространства при установке на ОС и 10 Гб при запуске в качестве контейнера Docker.
Что такое Jenkins?

Непрерывная интеграция является наиболее важной частью DevOps, которая используется для интеграции различных этапов DevOps. Jenkins — самый известный инструмент непрерывной интеграции, я знаю, что вам любопытно узнать причину популярности Jenkins, и я вполне уверен, что после прочтения этой статьи «Что такое Jenkins» , на все ваши вопросы будут даны ответы.
Что такое Jenkins?
Jenkins — это инструмент автоматизации с открытым исходным кодом, написанный на Java, с плагинами, созданными для непрерывной интеграции. Jenkins используется для непрерывной сборки и тестирования ваших программных проектов, что облегчает разработчикам интеграцию изменений в проект и облегчает пользователям получение новой сборки. Это также позволяет вам непрерывно поставлять программное обеспечение, интегрируя с большим количеством технологий тестирования и развертывания.
С помощью Jenkins организации могут ускорить процесс разработки программного обеспечения за счет автоматизации. Jenkins объединяет процессы жизненного цикла разработки всех видов, включая сборку, документацию, тестирование, пакет, этап, развертывание, статический анализ и многое другое.
Jenkins достигает непрерывной интеграции с помощью плагинов. Плагины позволяют интегрировать различные этапы DevOps. Если вы хотите интегрировать определенный инструмент, вам нужно установить плагины для этого инструмента. Например: Git, проект Maven 2, Amazon EC2, HTML-издатель и т. д.
На изображении ниже показано, что Jenkins интегрирует различные этапы DevOps:

Преимущества Jenkins включают в себя:
- Это инструмент с открытым исходным кодом с большой поддержкой сообщества.
- Его легко установить.
- Он имеет более 1000 плагинов для облегчения вашей работы. Если плагин не существует, вы можете написать свой плагин и поделиться с сообществом.
- Jenkins бесплатен.
- Он построен на Java и, следовательно, работает на всех основных платформах.
В Jenkins есть некоторые вещи, которые отличают его от других инструментов непрерывной интеграции. Давайте рассмотрим эти моменты.
Ключевые моменты Jenkins.
Ниже приведены некоторые факты о Jenkins, которые делают его лучше, чем любые другие инструменты непрерывной интеграции:
- Выбор многих: Jenkins широко распространен, имеет более 147 000 активных установок и более 1 миллиона пользователей по всему миру.
- Плагины: Jenkins связан более чем с 1000 плагинами, которые позволяют интегрировать его с большинством инструментов разработки, тестирования и развертывания.
Из вышесказанного видно, что спрос на Jenkins в мире очень высок. Прежде чем мы углубимся в Jenkins, важно знать, что такое непрерывная интеграция и почему она была введена.
Что такое непрерывная интеграция?

Непрерывная интеграция — это практика разработки, при которой разработчики обязаны фиксировать изменения в исходном коде в общем хранилище несколько раз в день или чаще.
Каждый коммит, сделанный в хранилище, затем создается. Это позволяет командам обнаруживать проблемы на ранней стадии. Помимо этого, в зависимости от инструмента Continuous Integration, есть несколько других функций, таких как развертывание приложения сборки на тестовом сервере, предоставление заинтересованным группам результатов сборки и тестирования и т. д.
Чтобы понять важность непрерывной интеграции, давайте взглянем на один из вариантов использования.

Я уверен, что вы все пользовались телефонами Nokia в какой-то момент вашей жизни. В проекте Nokia по разработке программного продукта был процесс под названием Nightly builds., Ночные сборки можно рассматривать как предшественника непрерывной интеграции. Это означает, что каждую ночь автоматизированная система извлекает код, добавленный в общий репозиторий в течение дня, и создает этот код. Идея очень похожа на Continuous Integration, но, так как код, который создавался ночью, был довольно большим, поиск и исправление ошибок было настоящей болью. В связи с этим Nokia приняла технологию непрерывной интеграции (CI). В результате каждый коммит, сделанный с исходным кодом в репозитории, был собран.

Если результат сборки показывает, что в коде есть ошибка, то разработчикам нужно только проверить этот конкретный коммит. Это значительно сократило время, необходимое для выпуска нового программного обеспечения.
Сейчас самое время понять, как в Jenkins работает непрерывная интеграция.
Непрерывная интеграция с Jenkins
Давайте представим сценарий, в котором полный исходный код приложения был собран, а затем развернут на тестовом сервере для тестирования. Это звучит как идеальный способ разработки программного обеспечения, но этот процесс имеет много недостатков. Я постараюсь объяснить их один за другим:
- Разработчики должны подождать, пока не будет разработано полное программное обеспечение для результатов испытаний.
- Существует высокая вероятность того, что результаты теста могут показать несколько ошибок. Разработчикам было трудно найти эти ошибки, потому что они должны проверить весь исходный код приложения.
- Это замедляет процесс доставки программного обеспечения.
- Непрерывная обратная связь по таким вопросам, как проблемы с кодированием или архитектурой, сбоями сборки, состоянием тестирования и загрузкой выпусков файлов, отсутствовала, из-за чего качество программного обеспечения может ухудшиться.
- Весь процесс был ручным, что увеличивает риск частого отказа.
Из вышеуказанных проблем видно, что не только процесс доставки программного обеспечения стал медленным, но и качество программного обеспечения также ухудшилось. Это приводит к недовольству клиента. Таким образом, для преодоления такого хаоса существовала острая необходимость в существующей системе, в которой разработчики могли бы непрерывно запускать сборку и тестирование для каждого изменения, внесенного в исходный код. Вот что такое CI.Jenkins является наиболее зрелым из доступных инструментов CI, поэтому давайте посмотрим, как непрерывная интеграция с Jenkins преодолела вышеуказанные недостатки.
Сначала я объясню вам общую схему непрерывной интеграции с Jenkins, чтобы она стала понятной, как Jenkins преодолевает вышеуказанные недостатки:
- Сначала разработчик фиксирует код в хранилище исходного кода. Тем временем сервер Jenkins регулярно проверяет наличие изменений в хранилище.
- Вскоре после того, как происходит фиксация, сервер Jenkins обнаруживает изменения, произошедшие в репозитории исходного кода. Дженкинс потянет эти изменения и начнет готовить новую сборку.
- Если сборка не удалась, соответствующая команда будет уведомлена.
- Если сборка прошла успешно, Jenkins развертывает встроенный тестовый сервер.
- После тестирования Jenkins генерирует обратную связь и затем уведомляет разработчиков о результатах сборки и тестирования.
- Он продолжит проверять хранилище исходного кода на предмет изменений, внесенных в исходный код, и весь процесс будет повторяться.
Теперь вы знаете, как Jenkins преодолевает традиционные недостатки SDLC. В таблице ниже показано сравнение между «До и после Jenkins».
| До Jenkins | После Jenkins |
| Весь исходный код был построен и затем протестирован. Поиск и исправление ошибок в случае сбоя сборки и тестирования было трудным и занимало много времени, что, в свою очередь, замедляло процесс доставки программного обеспечения. |
Каждый коммит, сделанный в исходном коде, создается и тестируется. Таким образом, вместо проверки всего исходного кода разработчикам нужно сосредоточиться только на конкретном коммите. Это приводит к частым выпускам нового программного обеспечения. |
| Разработчики должны ждать результатов испытаний | Разработчики знают результат тестирования каждого коммита, сделанного в исходном коде на ходу. |
| Весь процесс ручной | Вам нужно только зафиксировать изменения в исходном коде, и Jenkins автоматизирует остальную часть процесса для вас. |
Свежие записи
- Начало работы с Liquibase
- Укрощение высокой загрузки ЦП cAdvisor
- Понимание действий GitHub
- Добавление собственных бегунов (GitHub Runers)
- VPN, Proxy и Tor: сохранение анонимности в сети в 2022 году
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»! 🙂
