- PVSM.RU - https://www.pvsm.ru -

Настраиваем Continuous Integration для Jenkins и Bitbucket с werf

Настраиваем Continuous Integration для Jenkins и Bitbucket с werf - 1

Утилита werf [1] создана так, чтобы её было легко интегрировать с любыми CI/CD-системами. Подробнее об этом процессе в общем случае читайте в эпилоге этой статьи, но основное её содержимое — практический пример по организации CI в Jenkins и Bitbucket.

Подразумевается, что в результате наших действий мы ожидаем получить следующее:

  1. Shared Library для Jenkins, чтобы все сценарии CI хранились в одном месте и их можно было править единым коммитом.
  2. Интеграцию Jenkins с Bitbucket, чтобы запускать CI по коммиту в определенные ветки или по созданию тега.

Поехали!

Конфигурация Jenkins

Для реализации задуманного в статье будут задействованы:

В Jenkins для проектов используется multibranch pipeline.

Начнем с того, что подключим к Jenkins репозиторий, в котором будет храниться наша Shared Library [4]. Shared Library — это единая библиотека, что может содержать в себе код для исполнения CI и хранится отдельно в своем собственном репозитории. Это значительно упрощает процесс модернизации и работы над CI (вместо использования для хранения CI стандартного Jenkinsfile, который нужно подкладывать в каждый проект).

Итак, подключаем: Manage Jenkins → Configure System → Global Pipeline Libraries.

Настраиваем Continuous Integration для Jenkins и Bitbucket с werf - 2

Нужно указать имя, ветвь репозитория, из которой Jenkins будет забирать код библиотеки, а в Source Code Management указать адрес и доступ до репозитория (в нашем случае — SSH-ключ для доступа ReadOnly).

Структура Shared Library

Теперь приступим к описанию самой библиотеки. Структура очень проста и состоит всего из трёх директорий:

Настраиваем Continuous Integration для Jenkins и Bitbucket с werf - 3

  • 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:

    Настраиваем Continuous Integration для Jenkins и Bitbucket с werf - 4

Сам 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 [3] — добавляет Bitbucket как source для multibranch pipeline;
  • Basic Branch Build Strategies Plugin [2] — позволит запуск тегов по webhook. По умолчанию Jenkins не позволяет любые автоматизированные действия с тегами, т.к. не понимает какой из тегов — последний.

Если вы используете cloud-версию Bitbucket, то нужно только поставить разрешение на создание webhook'ов автоматически.

Также требуется создать служебного пользователя с доступом к репозиториям, т.к. Jenkins будет обнаруживать весь репозиторий через API. Это касается настройки как для cloud-версии, так и для собственного Bitbucket-сервера.

Пример из глобальных настроек Jenkins:

Настраиваем Continuous Integration для Jenkins и Bitbucket с werf - 5

Далее понадобится настроить source в Multibranch Pipeline, что происходит в интерактивном режиме. Это означает, что, когда вы добавите credentials bitbucket пользователя и имя команды или пользователя с проектами, которых мы будем работать, Jenkins найдет все доступные пользователю репозитории и позволит выбрать один из списка.

В самом репозитории мы полагались на поиск только определенных веток, т.к. не уверены, как много веток может быть, а Jenkins может надолго «задуматься», если начнет исследовать каждую ветку. Это накладывает определенные ограничения, т.к. теги тоже попадают под регулярное выражение. Однако Java Regular Expressions — довольно гибкие, так что большой проблемы нет.

Альтернативный путь: если есть желание совсем отделить теги от веток, можно добавить еще один абсолютно такой же Source в репозиторий и настроить его только на обнаружение тегов.

Итак, конфигурация:

Настраиваем Continuous Integration для Jenkins и Bitbucket с werf - 6

После этого Jenkins с помощью сервис-аккаунта сам сходит в Bitbucket и создаст webhook:

Настраиваем Continuous Integration для Jenkins и Bitbucket с werf - 7

Теперь при каждом коммите Bitbucket будет триггерить пайплайны (но только для тех веток и тегов, что мы отфильтровали) и даже посылать статус пайплайна обратно в Bitbucket в последнем столбце коммита:

Настраиваем Continuous Integration для Jenkins и Bitbucket с werf - 8
Статусы — кликабельные: при нажатии перекидывают в нужный пайплайн в Jenkins

Последний штрих — про Jenkins, который находится за nginx proxy и работает с определенного location. Тогда нужно в основных настройках исправить его location, чтобы он сам знал, как выглядит его endpoint:

Настраиваем Continuous Integration для Jenkins и Bitbucket с werf - 9

Без этого ссылки на pipeline в Bitbucket будут генерироваться некорректно.

Заключение

В статье рассмотрен вариант настройки CI с использованием Jenkins, Bitbucket и werf. Это очень общий пример, который не является панацеей для организации процесса разработки, однако даёт представление о том, как вообще подойти к построению своего CI с использованием werf.

Важная деталь: даже учитывая, что статус пайплайна отдается в Bitbucket, нам всё равно приходится «ходить» в Jenkins, чтобы разобрать результат в случае неудачи. Выкат по тегу через webhook, очевидно, может отрабатывать только один раз: любой откат на предыдущий тег придется делать вручную из Jenkins.

У данного подхода также есть большой плюс — это гибкость. Мы буквально можем прописать в CI всё что угодно. Хотя и порог вхождения для того, чтобы понимать, как именно это сделать, чуть выше, чем у других CI-систем.

Эпилог: про werf и CI/CD в целом

Общий подход к интеграции werf с CI/CD-системами описан в документации [5]. Вкратце рекомендуемые для любых проектов шаги сводятся к следующим:

  1. Создание временного DOCKER_CONFIG для исключения конфликтов между параллельными job'ами на одном runner'е (подробнее здесь [6]).
  2. Выполнение авторизации Docker для используемых Docker Registry. Это может быть родная реализация Docker Registry внутри CI-системы либо какая-то сторонняя. В случае со встроенными имплементациями (к примеру, GitLab Container Registry [7] или GitHub Docker Package [8]) все необходимые параметры доступны среди переменных окружения. Выполнять авторизацию для альтернативных registry можно вручную на каждом runner'е или через параметры, хранящиеся в секретах (также для каждого job'а).
  3. Простановка WERF_IMAGES_REPO, WERF_STAGES_STORAGE, а также необходимых параметров, которые варьируются в зависимости от имплементации [9]. Утилита werf должна знать, с какой реализацией работает, так как часть требует использования нативного API. Стоит отметить, что по умолчанию werf пытается определить, с какой имплементацией работает, исходя из адреса registry, но это задача часто невыполнима (и тогда требует явного указания имплементации).
  4. Простановка опций тегирования WERF_TAG_*: используя переменные окружения CI, определяем, чем инициирован текущий job, и выбираем подходящую опцию тегирования или всегда используем content-based тегирование (рекомендованный путь).
  5. Использование окружения CI-системы для последующего использования при выкате. Для понимания — environment в GitLab [10].
  6. Простановка автоматических аннотаций для всех выкатываемых ресурсов WERF_ADD_ANNOTATION_*. Среди этих аннотаций могут быть произвольные данные, которые помогут вам работать и отлаживать ресурсы приложения в Kubernetes. Мы пришли к тому, что все ресурсы должны содержать следующий набор:
    1. WERF_ADD_ANNOTATION_PROJECT_GIT — адрес проекта в Git;
    2. WERF_ADD_ANNOTATION_CI_COMMIT — коммит, соответствующий выкату;
    3. WERF_ADD_ANNOTATION_JOB или WERF_ADD_ANNOTATION_PIPELINE — адрес job или pipeline (зависит от CI-системы и желания), который связан с выкатом.
  7. Простановка по умолчанию комфортной работы с логом werf:
    1. WERF_LOG_COLOR_MODE=on — включение цветного вывода (werf запускается не в интерактивном терминале, по умолчанию цвета отключены);
    2. WERF_LOG_PROJECT_DIR=1 — вывод полного пути директории проекта;
    3. WERF_LOG_TERMINAL_WIDTH=95 — установка ширины вывода (werf запускается не в интерактивном терминале, по умолчанию ширина равна 140).

За время применения werf в большом количестве проектов у нас сформировался набор решений, который унифицирует конфигурацию, решает общие проблемы и делает сопровождение проще и нагляднее. В настоящий момент все описанные выше шаги с учетом этих решений уже встроены в команду werf ci-env для GitLab CI/CD [11] и GitHub Actions [12]. Пользователям других CI-систем необходимо реализовывать аналогичные действия самостоятельно — подобно тому, как описано в этой статье для примера с Jenkins.

P.S.

Читайте также в нашем блоге:

Автор: Andrey Koregin

Источник [16]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/jenkins/359866

Ссылки в тексте:

[1] werf: https://ru.werf.io/

[2] Basic Branch Build Strategies Plugin: https://plugins.jenkins.io/basic-branch-build-strategies

[3] Bitbucket Branch Source Plugin: https://plugins.jenkins.io/cloudbees-bitbucket-branch-source

[4] Shared Library: https://www.jenkins.io/doc/book/pipeline/shared-libraries/

[5] документации: https://ru.werf.io/documentation/using_with_ci_cd_systems.html

[6] здесь: https://ru.werf.io/documentation/reference/working_with_docker_registries.html#%D0%B0%D0%B2%D1%82%D0%BE%D1%80%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F-docker

[7] GitLab Container Registry: https://docs.gitlab.com/ee/user/packages/container_registry/

[8] GitHub Docker Package: https://github.com/features/packages

[9] варьируются в зависимости от имплементации: https://ru.werf.io/documentation/reference/working_with_docker_registries.html

[10] environment в GitLab: https://docs.gitlab.com/ee/ci/environments/

[11] GitLab CI/CD: https://ru.werf.io/documentation/advanced/ci_cd/gitlab_ci_cd.html

[12] GitHub Actions: https://ru.werf.io/documentation/advanced/ci_cd/github_actions.html

[13] GitLab CI для непрерывной интеграции и доставки в production. Часть 1: наш пайплайн: https://habr.com/ru/company/flant/blog/332712/

[14] Организация распределенного CI/CD с помощью werf: https://habr.com/ru/company/flant/blog/504390/

[15] Запускаем тесты на GitLab Runner с werf — на примере SonarQube: https://habr.com/ru/company/flant/blog/526702/

[16] Источник: https://habr.com/ru/post/529750/?utm_source=habrahabr&utm_medium=rss&utm_campaign=529750