вступление в Declarative Jenkins Pipelines


Если вы работаете с Jenkins и не слышали про Declarative Pipelines, то я как раз попытаюсь объяснить что к чему. Declarative Pipeline — это развитие старых Pipelines. Declarative Pipelines должны заменить старые Pipelines, они предоставляют более простой синтаксис и инструменты для более удобной работы, такие как автоматическая проверка синтаксиса при вводе, форматирование через API или CLI, интеграцию с Docker, визуальный редактор BlueOcean.

Обычно, declarative pipelines представляют в виде Jenkinsfile в корне репозитория, но их можно создавать из любых файлов, а также напрямую, через Job DSL. И так, у вас есть работающий Jenkins сервер с уже настроенной seed job. Если нет, то запустите его в docker:

docker volume create jenkins
docker run --name jenkins -ti \
    -v jenkins:/var/jenkins_home \
    -p 127.0.0.1:8080:8080 \
    jenkins/jenkins:lts

Скопируйте из консоли пароль администратора, перейдите на http://localhost:8080 и продолжите установку.

Перед тем, как приступить к написанию pipeline, продумайте какие этапы (stages) и шаги (steps) будут присутствовать. Можете даже нарисовать схему, что за чем будет выполняться. Например.

  1. Сборка.
    1. Клонирование git репозитория с кодом.
    2. Настройка переменных окружения и выставления имени сборки/билда.
    3. Сборка образа docker.
    4. Загрузка его в репозиторий.
  2. Деплой.

Создайте через веб интерфейс Jenkins Pipeline: http://localhost:8080/view/All/newJob

Теперь разберем синтаксис pipeline по частям. Рассмотрим обертку и параметры.

Так мы определяем, что pipeline declarative

pipeline {

Задаем слэйв, на котором будет запускаться билд, подробнее можете прочитать здесь: https://jenkins.io/doc/book/pipeline/syntax/#agent

    agent {
        node {
            label "devops"
        }
    }

Определяем параметры, с которыми будем запускать pipeline. Хочу обратить внимание на то, что, если вы определяете pipeline через Job DSL, то параметры лучше задать там, т.к. при первом запуске параметры, определенные внутри pipeline недоступны. В данном примере мы опрелеляем один параметр типа string с пустым значением по умолчанию. Подробнее здесь https://jenkins.io/doc/book/pipeline/syntax/#parameters

    parameters {
        string(
            name: 'app',
            defaultValue: '',
            description: 'application name')
    }

Опции самой pipeline, по порядку:

    options {
        buildDiscarder(logRotator(numToKeepStr: '30'))
        timeout(time: 1, unit: 'HOURS')
        timestamps()
        ansiColor('xterm')
    }

Получаем пользователя и пароль в виде строки, разделенной двоеточием из Jenkins credentials

    environment {
        ARTIFACTORY = credentials('artifactory')
    }

Когда будет запускаться pipeline, в нашем случае тригером будет служить push в master, подробнее здесь https://jenkins.io/doc/book/pipeline/syntax/#triggers Сам репозиторий задается на уровень выше или в Job DSL, или через UI

    triggers {
        pollSCM('* * * * *')
    }

Далее будут идти непосредственно этапы и шаги сборки и деплоя. Stages - необходимый блок, даже если у вас всего один этап и шаг, нужно его обернуть в

stages
  stages {

Определяем первый этап сборки

    stage('Build docker image')

У нас будет несколько шагов, первым делом клонируем вспомогательный репозиторий с кодом terraform для деплоя

    steps {

        git([
            url: "ssh://git@github.com/myorg/deployment.git",
            branch: 'master',
            credentialsId: 'github-ssh-key'
        ])

Определим еще переменных окружения, тут можно развернуться во всю силу groovy

        script {
          env.registry = "777.dkr.ecr.eu-west-1.amazonaws.com/java"
          env.app_name = "${app}".replace("-service", "")
          currentBuild.displayName = "#${env.BUILD_NUMBER} - ${app}"
          env.artifactory_url = "https://artifactory.local/artifactory"
        }

Сборка будет осуществляться посредством bash скрипта, который мы видим ниже. Обратите внимание на двойное экранирование, это требование Jenkins

        sh('''#!/bin/bash
echo "Trying to obtain latest version"
VERSION=`curl --connect-timeout 10 -u "\$ARTIFACTORY" "\${artifactory_url}/api/search/latestVersion?a=${app}&repos=libs-snapshot-local&v=*&g=services"` || exit 1
if `echo \$VERSION | grep -q '^{'`; then
    echo "version not found"
    echo "\$VERSION"
    exit 1
else
    echo "version: \$VERSION found"
    echo \$VERSION > version
fi

echo "Download artifact"
snapshot=`echo \$VERSION | cut -d- -f1`
curl -q --connect-timeout 10 -O -u "\$ARTIFACTORY" "\${artifactory_url}/libs-snapshot-local/services/${app}/\$snapshot-SNAPSHOT/${app}-\$VERSION.jar"

echo "Login to ECR"
eval `aws ecr get-login --no-include-email`

echo "Building docker image"
docker build --pull --force-rm \\
  --build-arg app=${app} \\
  --build-arg app_name=\$app_name \\
  --build-arg version=\$VERSION -t \$registry/\${app_name}:latest .

echo "Pushing docker image"
aws ecr create-repository --repository-name="java/${app_name}" || true
docker tag \$registry/\${app_name}:latest \$registry/\${app_name}:\$VERSION
docker push \$registry/\${app_name}:latest
docker push \$registry/\${app_name}:\$VERSION

echo "Cleanup"
rm -v ${app}-\$VERSION.jar
''')
            }
        }

Теперь деплой из только что загруженного образа

        stage('Terraform deploy') {
            steps {
                sh('''#!/bin/bash
VERSION=`cat version`
rm -v version
sed -i 's/service/'${app}'/g' backend.conf
[ -d .terraform ] && rm -rf .terraform
[ -f terraform.tfstate.backup ] && rm terraform.tfstate.backup
terraform init -backend-config=backend.conf -force-copy
terraform env select qa || terraform env new qa
terraform plan \\
    -var-file=qa.tfvars \\
    -var="service_name=\"${app_name}\"" \\
    -var="version=\"\$VERSION\"" -out=ecs-deploy.plan
terraform apply ecs-deploy.plan

echo "Cleanup"
git checkout -- .
rm -v ecs-deploy.plan
''')
            }
        }

Убедимся, что деплой прошел успешно, см. http://docs.aws.amazon.com/cli/latest/reference/ecs/wait/services-stable.html

    stage('ECS wait for deploy') {
        steps {
            sh('''#!/bin/bash
aws ecs wait services-stable --services java-${app_name}
''')
        }
    }
}

Теперь настроим уведомления, чтоб знать о неудачных сборках. Уведомления будут приходить на почту.

post {
  failure {
// отправим последние 50 строк неудачной сборки
    script {
      env.logs = currentBuild.rawBuild.getLog(50).join('\n')
    }
    emailext (
      subject: "FAILURE: ${env.JOB_NAME} Build # ${env.BUILD_NUMBER} ",
      body: """FAILED: Job ${env.JOB_NAME} [${env.BUILD_NUMBER}]':
Check console output at ${env.BUILD_URL}console

...
${env.logs}
""",
      to: 'builds@myorg.com,me@myorg.com'
    )
  }
}
}

Вы увидели пример pipeline, используемого в QA. Конечно, можно еще добавить шаги с выполнениями тестов и т.п. Если вы хотите настроить больше этапов, шагов, уведомлений и действий, обратитесь к документации вашего Jenkins по http://localhost:8080/pipeline-syntax/