Утилита werf создана так, чтобы её было легко интегрировать с любыми CI/CD-системами. Подробнее об этом процессе в общем случае читайте в эпилоге этой статьи, но основное её содержимое — практический пример по организации CI в Jenkins и Bitbucket.
Подразумевается, что в результате наших действий мы ожидаем получить следующее:
- Shared Library для Jenkins, чтобы все сценарии CI хранились в одном месте и их можно было править единым коммитом.
- Интеграцию Jenkins с Bitbucket, чтобы запускать CI по коммиту в определенные ветки или по созданию тега.
Поехали!
Конфигурация Jenkins
Для реализации задуманного в статье будут задействованы:
- Jenkins 2.249.1;
- Bitbucket Cloud;
- werf 1.1 stable;
- Basic Branch Build Strategies Plugin 1.3.2;
- Bitbucket Branch Source Plugin 2.9.4.
В Jenkins для проектов используется multibranch pipeline.
Начнем с того, что подключим к Jenkins репозиторий, в котором будет храниться наша Shared Library. Shared Library — это единая библиотека, что может содержать в себе код для исполнения CI и хранится отдельно в своем собственном репозитории. Это значительно упрощает процесс модернизации и работы над CI (вместо использования для хранения CI стандартного Jenkinsfile
, который нужно подкладывать в каждый проект).
Итак, подключаем: Manage Jenkins → Configure System → Global Pipeline Libraries.
Нужно указать имя, ветвь репозитория, из которой Jenkins будет забирать код библиотеки, а в Source Code Management указать адрес и доступ до репозитория (в нашем случае — SSH-ключ для доступа ReadOnly).
Структура Shared Library
Теперь приступим к описанию самой библиотеки. Структура очень проста и состоит всего из трёх директорий:
-
vars
— директория для глобальных методов библиотеки, что будут вызываться из пайплайна; -
src
— тоже директория для скриптов, но в основном используется для вашего кастомного кода; -
resources
— всё, что не является скриптом и может понадобиться в исполнении.
Для наших целей в Jenkins будет достаточно только нескольких методов в директории vars
, потому как мы настроим сам werf, что и сделает всю основную работу.
К тому же, хотелось бы, чтобы весь пайплайн был полностью описан внутри библиотеки, а в Jenkinsfile
мы передавали только некоторые параметры деплоя, которые в 99,9% случаев вообще не будут меняться.
Реализуем методы
Итак, реализуем 2 метода.
Для вызова утилиты werf — -runWerf.groovy
.
#!/usr/bin/env groovy
def call(String dockerCreds, String werfargs){
// логин в registry
// первый аргумент - url (пуст, т.к. используем DockerHub)
// второй - имя Jenkins-секрета, где лежат доступы (login, password)
docker.withRegistry("", "${dockerCreds}") {
sh """#!/bin/bash -el
set -o pipefail
type multiwerf && source <(multiwerf use 1.1 stable --as-file)
werf version
werf ${werfargs}""".trim()
}
}
Все параметры в библиотеку для пайплайна передаются как Map
, что удобно:
#!/usr/bin/env groovy
def call( Map parameters = [:] ) { // функция принимает в качестве аргумента Map с параметрами
def namespace = parameters.namespace // имя неймспейса для выката
// имя ключа по умолчанию для расшифровки секретов (если не указан в параметрах)
def werf_secret_key = parameters.werfCreds != null ? parameters.werfCreds : "werf-secret-key-default"
// имя секрета по умолчанию для логина в docker registry
def dockerCreds = parameters.dockerCreds != null ? parameters.dockerCreds : "docker-credentials-default"
// получаем имя проекта из имени multibranch pipeline
def PROJ_NAME = "${env.JOB_NAME}".split('/').first()
// имя registry в docker hub или адрес до кастомного registry
def imagesRepo = parameters.imagesRepo != null ? parameters.imagesRepo : "myrepo"
if( namespace == null ) { // единственный обязательный аргумент и проверка на его наличие
currentBuild.result = 'FAILED'
return
}
pipeline {
agent { label 'werf' }
options { disableConcurrentBuilds() } // запрещаем параллельную сборку для пайплайна
environment { // переменные для работы werf
WERF_IMAGES_REPO="${imagesRepo}"
WERF_STAGES_STORAGE=":local"
WERF_TAG_BY_STAGES_SIGNATURE=true
WERF_ADD_ANNOTATION_PROJECT_GIT="project.werf.io/git=${GIT_URL}"
WERF_ADD_ANNOTATION_CI_COMMIT="ci.werf.io/commit=${GIT_COMMIT}"
WERF_LOG_COLOR_MODE="off"
WERF_LOG_PROJECT_DIR=1
WERF_ENABLE_PROCESS_EXTERMINATOR=1
WERF_LOG_TERMINAL_WIDTH=95
PATH="$PATH:$HOME/bin"
WERF_KUBECONFIG="$HOME/.kube/config"
WERF_SECRET_KEY = credentials("${werf_secret_key}")
}
triggers {
// Execute weekdays every four hours starting at minute 0
cron('H 21 * * *')
// для werf cleanup, что будет чистить registry и хост-раннер от устаревших кэшей и образов
}
stages {
stage('Checkout') {
steps {
checkout scm // получаем код из репозитория
}
}
stage('Build & Publish image') {
when {
not { triggeredBy 'TimerTrigger' } // чтобы stage не запускался по крону
}
steps {
script {
// запуск нашего метода из runWerf.groovy
runWerf("${dockerCreds}","build-and-publish")
}
}
}
stage('Deploy app') {
when {
not { triggeredBy 'TimerTrigger' }
}
environment {
// название окружения, куда осуществляется деплой (важно для шаблонизации Helm-чарта)
WERF_ENV="production"
}
steps {
runWerf("${dockerCreds}","deploy --stages-storage :local --images-repo ${imagesRepo}")
}
}
stage('Cleanup werf Images') {
when {
allOf {
triggeredBy 'TimerTrigger'
branch 'master'
}
}
steps {
sh "echo 'Cleaning up werf images'"
runWerf("${dockerCreds}","cleanup --stages-storage :local --images-repo ${imagesRepo}")
}
}
}
}
}
Примечания:
- Сборка и выкат происходят для любой ветки, указанной в секции
discover
у Jenkins. После наших манипуляций в следующей главе это будет происходить автоматически. - Все секреты, такие как
werf-secret-key-default
иdocker-credential-default
, хранятся в Jenkins Credentials:
Сам Jenkinsfile
, что находится внутри репозитория с проектом, в большинстве случаев теперь выглядит так:
@Library('common-ci') _
multiStage ([
namespace: 'yournamespace'
])
Имя метода — это название файла в каталоге vars
.
Если необходимо выкатывать на несколько окружений, можно добавить условие для определенных веток в самом начале, где идет определение пространства имен. И убрать проверку на наличие аргумента namespace в Map
, а также само его определение по умолчанию.
Пример реализации:
def namespace = "test"
def werf_env = "test"
if (env.JOB_BASE_NAME == 'master') {
namespace = "stage"
werf_env = "stage"
}
if (env.TAG_NAME) {
namespace = "production"
werf_env = "production"
}
# и добавляем в environment стадии
environment {
WERF_ENV="${werf_env}"
}
Если вы хотите автоматический запуск stage со всех веток, а с тегов в production — только при нажатии кнопки в Jenkins, то можно использовать такое условие: currentBuild.rawBuild.getCauses()[0].toString().contains('UserIdCause')
. Оно позволяет отследить, сборка была запущена человеком или началась как событие от webhook'а.
Триггеры по коммитам из Bitbucket
По умолчанию Jenkins сам не умеет интегрироваться в Bitbucket. Для этого нужно установить уже упомянутые плагины:
- Bitbucket Branch Source Plugin — добавляет Bitbucket как source для multibranch pipeline;
- Basic Branch Build Strategies Plugin — позволит запуск тегов по webhook. По умолчанию Jenkins не позволяет любые автоматизированные действия с тегами, т.к. не понимает какой из тегов — последний.
Если вы используете cloud-версию Bitbucket, то нужно только поставить разрешение на создание webhook'ов автоматически.
Также требуется создать служебного пользователя с доступом к репозиториям, т.к. Jenkins будет обнаруживать весь репозиторий через API. Это касается настройки как для cloud-версии, так и для собственного Bitbucket-сервера.
Пример из глобальных настроек Jenkins:
Далее понадобится настроить source
в Multibranch Pipeline, что происходит в интерактивном режиме. Это означает, что, когда вы добавите credentials bitbucket пользователя и имя команды или пользователя с проектами, которых мы будем работать, Jenkins найдет все доступные пользователю репозитории и позволит выбрать один из списка.
В самом репозитории мы полагались на поиск только определенных веток, т.к. не уверены, как много веток может быть, а Jenkins может надолго «задуматься», если начнет исследовать каждую ветку. Это накладывает определенные ограничения, т.к. теги тоже попадают под регулярное выражение. Однако Java Regular Expressions — довольно гибкие, так что большой проблемы нет.
Альтернативный путь: если есть желание совсем отделить теги от веток, можно добавить еще один абсолютно такой же Source
в репозиторий и настроить его только на обнаружение тегов.
Итак, конфигурация:
После этого Jenkins с помощью сервис-аккаунта сам сходит в Bitbucket и создаст webhook:
Теперь при каждом коммите Bitbucket будет триггерить пайплайны (но только для тех веток и тегов, что мы отфильтровали) и даже посылать статус пайплайна обратно в Bitbucket в последнем столбце коммита:
Статусы — кликабельные: при нажатии перекидывают в нужный пайплайн в Jenkins
Последний штрих — про Jenkins, который находится за nginx proxy и работает с определенного location. Тогда нужно в основных настройках исправить его location, чтобы он сам знал, как выглядит его endpoint:
Без этого ссылки на pipeline в Bitbucket будут генерироваться некорректно.
Заключение
В статье рассмотрен вариант настройки CI с использованием Jenkins, Bitbucket и werf. Это очень общий пример, который не является панацеей для организации процесса разработки, однако даёт представление о том, как вообще подойти к построению своего CI с использованием werf.
Важная деталь: даже учитывая, что статус пайплайна отдается в Bitbucket, нам всё равно приходится «ходить» в Jenkins, чтобы разобрать результат в случае неудачи. Выкат по тегу через webhook, очевидно, может отрабатывать только один раз: любой откат на предыдущий тег придется делать вручную из Jenkins.
У данного подхода также есть большой плюс — это гибкость. Мы буквально можем прописать в CI всё что угодно. Хотя и порог вхождения для того, чтобы понимать, как именно это сделать, чуть выше, чем у других CI-систем.
Эпилог: про werf и CI/CD в целом
Общий подход к интеграции werf с CI/CD-системами описан в документации. Вкратце рекомендуемые для любых проектов шаги сводятся к следующим:
- Создание временного
DOCKER_CONFIG
для исключения конфликтов между параллельными job'ами на одном runner'е (подробнее здесь). - Выполнение авторизации Docker для используемых Docker Registry. Это может быть родная реализация Docker Registry внутри CI-системы либо какая-то сторонняя. В случае со встроенными имплементациями (к примеру, GitLab Container Registry или GitHub Docker Package) все необходимые параметры доступны среди переменных окружения. Выполнять авторизацию для альтернативных registry можно вручную на каждом runner'е или через параметры, хранящиеся в секретах (также для каждого job'а).
- Простановка
WERF_IMAGES_REPO
,WERF_STAGES_STORAGE
, а также необходимых параметров, которые варьируются в зависимости от имплементации. Утилита werf должна знать, с какой реализацией работает, так как часть требует использования нативного API. Стоит отметить, что по умолчанию werf пытается определить, с какой имплементацией работает, исходя из адреса registry, но это задача часто невыполнима (и тогда требует явного указания имплементации). - Простановка опций тегирования
WERF_TAG_*
: используя переменные окружения CI, определяем, чем инициирован текущий job, и выбираем подходящую опцию тегирования или всегда используем content-based тегирование (рекомендованный путь). - Использование окружения CI-системы для последующего использования при выкате. Для понимания — environment в GitLab.
- Простановка автоматических аннотаций для всех выкатываемых ресурсов
WERF_ADD_ANNOTATION_*
. Среди этих аннотаций могут быть произвольные данные, которые помогут вам работать и отлаживать ресурсы приложения в Kubernetes. Мы пришли к тому, что все ресурсы должны содержать следующий набор:-
WERF_ADD_ANNOTATION_PROJECT_GIT
— адрес проекта в Git; -
WERF_ADD_ANNOTATION_CI_COMMIT
— коммит, соответствующий выкату; -
WERF_ADD_ANNOTATION_JOB
илиWERF_ADD_ANNOTATION_PIPELINE
— адрес job или pipeline (зависит от CI-системы и желания), который связан с выкатом.
-
- Простановка по умолчанию комфортной работы с логом werf:
-
WERF_LOG_COLOR_MODE=on
— включение цветного вывода (werf запускается не в интерактивном терминале, по умолчанию цвета отключены); -
WERF_LOG_PROJECT_DIR=1
— вывод полного пути директории проекта; -
WERF_LOG_TERMINAL_WIDTH=95
— установка ширины вывода (werf запускается не в интерактивном терминале, по умолчанию ширина равна 140).
-
За время применения werf в большом количестве проектов у нас сформировался набор решений, который унифицирует конфигурацию, решает общие проблемы и делает сопровождение проще и нагляднее. В настоящий момент все описанные выше шаги с учетом этих решений уже встроены в команду werf ci-env
для GitLab CI/CD и GitHub Actions. Пользователям других CI-систем необходимо реализовывать аналогичные действия самостоятельно — подобно тому, как описано в этой статье для примера с Jenkins.
P.S.
Читайте также в нашем блоге:
- «GitLab CI для непрерывной интеграции и доставки в production. Часть 1: наш пайплайн»;
- «Организация распределенного CI/CD с помощью werf»;
- «Запускаем тесты на GitLab Runner с werf — на примере SonarQube».
Автор: Andrey Koregin