В компаниях с большим количеством проектов часто возникает ситуация, когда при разработке пайплайнов мы начинаем повторять себя, добавляя в разные сервисы одинаковые конструкции. Это противоречит основному принципу программирования DRY (Don’t Repeat Yourself), а ещё усложняет внесение изменений в код. Справиться с описанными проблемами помогает Jenkins Shared Library.
Мы пообщались с Кириллом Борисовым, Infrastructure Engineer технологического центра Deutsche Bank, и узнали, какие задачи решает Jenkins Shared Library и что её внедрение даёт компании. А ещё рассмотрели кейс разработки и тестирования с примерами кода.
Что такое Jenkins Shared Library
Jenkins Shared Library — это библиотека для многократного использования кода, которая позволяет описать методы один раз и затем применять их во всех пайплайнах. Она решает проблемы дублирования кода и унификации. Благодаря ней нам не нужно держать в каждом пайплайне огромное количество кода и постоянно повторять его. Мы можем написать код в одном месте, а потом инклюдить и вызывать его в рамках самого пайплайна.
Как это работает? Первым делом во всех пайплайнах мы выделяем общие части, которые можем объединить. Предположим, мы заметили, что везде используем нотификацию в Telegram, и везде она реализована по-разному. Мы создаем библиотеку, в ней делаем модуль нотификации и внедряем его во все проекты.
Затем смотрим, что ещё можем унифицировать. Например, обращение к внутренней системе, которое тоже везде реализовано по-разному. Мы заносим этот кусочек кода в библиотеку и далее распространяем на каждый микросервис. Так, выделяя общие части в каждом пайплайне, мы постепенно обогащаем нашу библиотеку и имплементируем её в наш пайплайн.
Польза Jenkins Shared Library для компании
Jenkins Shared Library уменьшает количество кода в пайплайне и повышает его читаемость. Для бизнеса это не столь важно, его больше заботит скорость доставки новых версий продукта до конечных пользователей. А здесь многое зависит от скорости разработки. Если каждая команда компании перестанет придумывать своё и начнёт использовать наработки других, вероятнее всего, разработка пайплайнов пойдёт быстрее. Соответственно, процесс доставки обновлений тоже ускорится.
С точки зрения самой команды, Jenkins Shared Library одновременно и усложняет, и упрощает работу. Например, проводить онбординг оказывается проще — вы показываете всё в одном месте. В то же время командам становится сложнее взаимодействовать. Сложность заключается в необходимости постоянной коммуникации. Когда решаем изменить что-то у себя в библиотеке, мы сначала должны синхронизироваться с другой командой, чтобы убедиться, что у неё ничего не сломается. Но обычно это решается регулярными синками, ревью кода и пул-реквестами.
Как команда понимает, что пора внедрять Jenkins Shared Library
На прошлом проекте у нас было 20 микросервисов, очень похожих между собой. Они были написаны по одному скелету на Java, а ещё в каждом из них лежал Jenkins file, где был описан пайплайн сборки проектов. Всякий раз, когда нужно было что-то поменять в пайплайне (а делать это приходилось часто, так как проект динамичный и быстро развивался), мы проходились по 20 репозиториям и поправляли всё вручную. Это довольно геморройный процесс, и мы подумали: «А почему бы нам не сделать что-то общее?».
Так мы перешли на Jenkins Shared Library. У нас появился базовый пайплайн в библиотеке, и, когда требовалось что-то изменить, мы работали с ним. Добавляли какие-то правки, и они автоматически имплементировались в каждый микросервис.
Ограничения Jenkins Shared Library
Любой инструмент призван решать какую-то проблему. Jenkins Shared Library решает проблему дублирования кода и унификации. Если вам эта унификация не нужна, смысла тащить библиотеку в проект нет.
Если, скажем, у вас небольшой проект, где всего 5 микросервисов, писать для него библиотеку с нуля не стоит. Единственный вариант — переиспользовать, если она уже написана. Jenkins Shared Library — всё-таки решение для более крупных проектов с большим количеством микросервисов.
Кейс: разработка и тестирование Jenkins Shared Library
Для разработки и тестирования Jenkins Shared Library нам необходимо установить на свой компьютер gradle. Вот инструкция по установке — https://gradle.org/install/.
Далее в рабочем каталоге мы выполняем команду инициализации «gradle init», говорим установщику, что хотим настроить base каталог с groovy-файлами, и получаем готовый проект.
Следующий шаг — создание каталогов для JSL:
var
src
resources
test
Директория src используется для Groovy классов, которые добавляются в classpath.
Директория resources содержит файлы, которые мы можем загружать в пайплайн.
Директория test содержит тесты для наших скриптом.
Директория vars используется в скриптах, которые определяют глобальные переменные, доступные из пайплайна.
Затем заполянем build.gradle, добавляя в него описание наших каталогов:
–--
sourceSets {
main {
groovy {
srcDirs = ['src','vars']
}
resources {
srcDirs = ['resources']
}
}
test {
groovy {
srcDirs = ['test']
}
}
}
---
И добавляем зависимости, которые понадобятся нам в работе. Полный файл можно посмотреть в репозитории: ссылка на репозиторий.
Пишем простой первый класс, который увеличивает возраст на указанное значение :)
package com.example
class SampleClass {
String name
Integer age
def increaseAge(Integer years) {
this.age += years
}
}
Не забываем про тесты — создаем в директории test файл с именем SampleClassTest.groovy и содержимым.
В секции Before описываем действия, которые необходимо выполнить перед тестом — в нашем случае это объявление класса. Далее в секции Test описываем сам тест. Объявляем age = 7 и выполняем функцию increaseAge с параметром 3. В случае правильного выполнения ожидаем получить 10.
class SampleClassTest {
def sampleClass
@Before
void setUp() {
sampleClass = new SampleClass()
}
@Test
void testIncrease() {
sampleClass.age = 7
def expect = 10
assertEquals expect, sampleClass.increaseAge(3)
}
}
Тест готов, запускаем командой gradle test. Результат будет такой:
Но это синтетические примеры, давайте рассмотрим реальный
Каждый раз для вызова GET или POST запроса в пайплайн нам нужно вызывать класс HttpsURLConnection, передавать в него правильные параметры и проверять валидность сертификата.
Последуем главному принципу программирования DRY и подготовим класс, который позволит нам вызывать get и post запросы в пайплайне.
Создаем в директории src/com/example HttpsRequest.groovy, в нём создаем два метода get и post. В параметрах методов передаём URL-запроса и в случае POST еще и body-запроса.
Корневым методом нашего класса будет метод httpInternal. В нём закладываем логику, добавляем параметры к HttpsURLConnection, отлавливаем ошибку и в результате возвращаем тело ответа и ошибку:
package com.example
import groovy.json.JsonSlurper
import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
class HttpsRequest {
def get(String uri) {
httpInternal(uri, null, false)
}
def post(String uri, String body) {
httpInternal(uri, body, true)
}
def httpInternal(String uri, String body, boolean isPost) {
def response = [:]
def error
try {
def http = new URL(uri).openConnection() as HttpsURLConnection
if (isPost) {
http.setRequestMethod('POST')
http.setDoOutput(true)
if (body) {
http.outputStream.write(body.getBytes("UTF-8"))
}
}
http.setRequestProperty("Accept", 'application/json')
http.setRequestProperty("Content-Type", 'application/json')
http.connect()
if (http.responseCode == 200) {
response = new JsonSlurper().parseText(http.inputStream.getText('UTF-8'))
} else {
response = -1
}
} catch (Exception e) {
println(e)
error = e
}
return [response, error]
}
}
Покрываем тестами и создаем файл в директории test/HttpsRequestTest.groovy.
Для примера делаем get запрос, получаем ответ и проверяем, что он соответствует нашим ожиданиям:
void testGet() {
def expect = json.parseText('{"hello":"slurm"}')
def (result, error) = http.get("https://run.mocky.io/v3/0bd64f74-1861-4833-ad9d-80110c9b5f25")
if (error != null) {
println(error)
}
assertEquals "result:", expect, result
}
Запускаем тесты:
Осталось дело за малым — подключить библиотеку в Jenkins.
Для этого в Jenkins нужно перейти: Manage Jenkins → Configure System (Настроить Jenkins → Конфигурирование системы). В блоке Global Pipeline Libraries, добавить наш репозиторий:
Последний шаг — создаем наш пайплайн:
@Library('jenkins-shared-library') _
import com.example.HttpsRequest
pipeline {
agent any
stages {
stage('Demo') {
steps {
script {
def http = new HttpsRequest()
def (result, error) = http.get("https://run.mocky.io/v3/0bd64f74-1861-4833-ad9d-80110c9b5f25")
if (error != null) {
println(error)
} else {
println result
}
}
}
}
}
}
Выполняем :)
Весь код можно найти в репозитории: ссылка на репозиторий.
Вместо заключения: о чём нужно помнить при работе с Jenkins Shared Library
Jenkins Shared Library — больше инструмент разработки в том плане, что нужно программировать. Чаще всего когда вы первый раз работаете с ней, если вы не разработчик, а администратор или DevOps, вы делаете это по наитию с точки зрения администратора. Это рабочий подход, но у него есть свои недостатки. Например, расширять проект дальше сложнее.
Лучше сразу начинать мыслить как разработчик: выделять классы и методы, правильно их имплементировать в пайплайн и т.д. Если вы не из мира разработки, это осознание приходит с запозданием. И чем позже оно придёт, тем больше времени и сил вы потратите на то, чтобы переписать то, что уже сделали.
Для тех, кто хочет углубиться в тонкости работы с Jenkins Shared Library
6 сентября у нас стартует курс по Jenkins, автором которого выступил Кирилл Борисов, Infrastructure Engineer технологического центра Deutsche Bank. В курсе будет много кейсов и примеров из практики спикера.
Вы научитесь:
автоматизировать процесс интеграции и поставки;
ускорять цикл разработки и внедрять полезные инструменты;
настраивать плагины и создавать пайплайны Jenkins as a code;
работать с Jenkins Shared Library.
Ознакомиться с программой и записаться: https://slurm.club/3a6OEs8
stopper79
Мое мнение, зашивать def (result, error) = http.get("https://run.mocky.io/v3/0bd64f74-1861-4833-ad9d-80110c9b5f25") не есть гуд. Лучше использовать переменные, т.к. я думаю, урл может со временем поменяться. ИМХО