[Из песочницы] CI/CD, используя Jenkins на Kubernetes

Добрый день.

На Хабре уже есть несколько статей о jenkins, ci/cd и kubernetes, но в данной я хочу сконцентрироваться не на разборе возможностей этих технологий, а на максимально простой их конфигурации для постройки ci/cd pipeline.

Я подразумеваю, что читатель имеет базовое понимание docker, и не буду останавливаться на темах утановки и конфигурирования kubernetes. Все примеры будут показаны на minikube, но так же могут быть применены на EKS, GKE либо подобные без значительных изменений.

geqoubunc_7bpd9dwq5bpftps2m.png


Окружения

Я предлагаю использовать следующие окружения:


  • test — для ручного деплоя и тестирования веток
  • staging — окружение, куда автоматически деплоятся все изменения попавшие в master
  • production — окружение используемое реальными пользователями, куда изменения попадут только после подтверждения их работоспособности на staging

Окружения будут организованы используя kubernetes namespaces в рамках одного кластера. Такой подход является максимально простым и быстрым на старте, но так же имеет свои недостатки: namespaces не полностью изолированы друг от друга в kubernetes.

В это примере каждый namespace будет иметь одинаковый набор ConfigMaps с конфигураций данного окружения:

apiVersion: v1
kind: Namespace
metadata:
  name: production
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: environment.properties
  namespace: production
data:
  environment.properties: |
    env=production


Helm

Helm это приложение которое помогает управлять ресурсами установелнными на kubernetes.
Инструкцию по установке можно найти здесь.
Для начала работы необходимо инициализировать tiller pod для использования helm с кластером:

helm init


Jenkins

Я буду использовать Jenkins так как это достаточно простая, гибкая и популярная платформа для сборки проектов. Он будет установлен в отдельном namespace для изоляции от других окружений. Так как я планирую использовать helm в дальнейшем, то можно упростить установку Jenkins используя уже имеющиеся open source charts:

helm install --name jenkins --namespace jenkins -f jenkins/demo-values.yaml stable/jenkins

demo-values.yaml содержат версию Jenkins, набор предустановленых плагинов, доменное имя и прочую конфигурацию


demo-values.yaml
Master:
  Name: jenkins-master
  Image: "jenkins/jenkins"
  ImageTag: "2.163-slim"

  OverwriteConfig: true

  AdminUser: admin
  AdminPassword: admin

  InstallPlugins:
    - kubernetes:1.14.3
    - workflow-aggregator:2.6
    - workflow-job:2.31
    - credentials-binding:1.17
    - git:3.9.3
    - greenballs:1.15
    - google-login:1.4
    - role-strategy:2.9.0
    - locale:1.4

  ServicePort: 8080
  ServiceType: NodePort
  HostName: jenkins.192.168.99.100.nip.io
  Ingress:
    Path: /

Agent:
  Enabled: true
  Image: "jenkins/jnlp-slave"
  ImageTag: "3.27-1"
  #autoadjust agent resources limits
  resources:
    requests:
      cpu: null
      memory: null
    limits:
      cpu: null
      memory: null

#to allow jenkins create slave pods
rbac:
  install: true

Данная конфигурация использует admin/admin в качестве имени пользователя и пароля для входа, и может быть перенастроена в дальнейшем. Один из возможных вариантов — SSO от google (для этого необходим плагин google-login, его настройки находятся в Jenkins > Manage Jenkins > Configure Global Security > Access Control > Security Realm > Login with Google).

Jenkins будет сразу же настроен на автоматическое создание одноразовых slave для каждой сборки. Благодаря этому команда больше не будет ожидать свободный агент для сборки, а бизнес сможет сэкономить на колличестве необходимых серверов.

it8_oo1aownty_iuuihofbo0q_4.png

Так же из коробки настроен PersistanceVolume для сохранения pipelines при перезапуске либо обновлении.

Для корректной работы скриптов автоматического деплоя понадобится дать разришение cluster-admin для Jenkins для получения списка ресурсов в kubernetes и манипулирвоания с ними.

kubectl create clusterrolebinding jenkins --clusterrole cluster-admin --serviceaccount=jenkins:default

В дальнейшем можно обновить Jenkins используя helm, в случае выхода новых версий плагинов либо изменений конфигурации.

helm upgrade jenkins stable/jenkins -f jenkins/demo-values.yaml

Это можно сделать и через интерфейс самого Jenkins, но с helm у вас появится возможность откатится к предыдущим ревизиям используя:

helm history jenkins
helm rollback jenkins ${revision}


Сборка приложениея

В качетве примера я буду собирать и деплоить простейшее spring boot приложение. Аналогично с Jenkins я буду использовать helm.

Сборка будет происходить в такой последовательности:


  • checkout
  • компиляция
  • unit test
  • integration test
  • сборка артефакта
  • деплой артефакта в docker registry
  • деплой артефакта на staging (только для master branch)

Для этого я использую Jenkins file. На мой взгляд это очень гибкий (но, к сожалению, не самый простой) способ сконфигурировать сборку проекта. Одним из его преймуществ является возможность держать конфигрурацию сборки проекта в репозитории с самим проектом.


checkout

tlziisqfairddgvaayk6atfuhhm.png

В случае с bitbucket либо github organization можно настроить Jenkins переодически сканировать целый аккаунт на наличие репозиториев с Jenkinsfile и автоматически создавать сборки для них. Jenkins будет собирать как master, так и ветки. Pull requests будут выведены в отдельную вкладку. Существует и более простой вариант — добавить отдельный git репозиторий, независимо от того где он хостится. В этом примере я именно так и сделаю. Все что необходимо это в меню Jenkins > New item > Multibranch Pipeline выбрать имя сборки и привязать git репозиторий.


Компиляция

Так как Jenkins для каждой сборки создает новый pod, то в случае использования maven либо подобных сборщиков, зависимости будут скачиваться заново каждый раз. Чтобы избежать этого, можно выделить PersistenceVolume для .m2 либо аналогичных кешей и монтировать в pod который осуществляет сборку проекта.

apiVersion: "v1"
kind: "PersistentVolumeClaim"
metadata:
  name: "repository"
  namespace: "jenkins"
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi

В моем случае это позволило ускорить pipeline примерно с 4х до 1й минуты.


Версионирование

Для корректной работы CI/CD каждая сборка нуждается в уникальной версии.

Очень хорошим вариантом может быть использование semantic versioning. Это позволит отслеживать обратно совместимые и не совместимые изменения, но такое версионирование сложнее автоматизировать.

В данном примере я буду генерировать версию из id и даты коммита, а так же названия ветки, если это не master. Например 56e0fbdc-201802231623 или b3d3c143–201802231548-PR-18.

Преимущества данного подхода:


  • простота автоматизации
  • из версии легко получить исходный код и время его создания
  • визуально можно отличить версию релиз кандидата (из мастера) либо эксперементальную (из ветки)
    но:
  • такую версию тяжелее использовать в устной коммуникации
  • не ясно, были ли несовместимые изменения.

Так как docker image может иметь несколько тегов одновременно то можно совместить подходы: все релизы используют сгенерированные версии, а те, которые попадают на продакшен, дополнительно (вручную) помечаются тегами с semantic versioning. В свою очередь это связано с еще еще большей сложностью реализации и неоднозначностью того, какую версию должно показывать приложение.


Артефакты

Результатом сборки будет:


  • docker image с приложением который будет хранится и загружаться из docker registry. В примере будет использоваться встроенный registry от minikube, который может быть заменен на docker hub либо приватный registry от amazon (ecr) либо google (не забывайте предоставить credentials к ним используя конструкцию withCredentials).
  • helm charts с описанием деплоймента приложения (deployment, service, etc) в директории helm. В идеале они должны хранится на отдельном репозитории артефактов, но, для упрощения, их можно использовать делая чекаут нужного коммита из git.


Jenkinsfile

В результате сборка приложения будет осуществлятся при помощи следующего Jenkinsfile:


Jenkinsfile
def branch
def revision
def registryIp

pipeline {

    agent {
        kubernetes {
            label 'build-service-pod'
            defaultContainer 'jnlp'
            yaml """
apiVersion: v1
kind: Pod
metadata:
  labels:
    job: build-service
spec:
  containers:
  - name: maven
    image: maven:3.6.0-jdk-11-slim
    command: ["cat"]
    tty: true
    volumeMounts:
    - name: repository
      mountPath: /root/.m2/repository
  - name: docker
    image: docker:18.09.2
    command: ["cat"]
    tty: true
    volumeMounts:
    - name: docker-sock
      mountPath: /var/run/docker.sock
  volumes:
  - name: repository
    persistentVolumeClaim:
      claimName: repository
  - name: docker-sock
    hostPath:
      path: /var/run/docker.sock
"""
        }
    }
    options {
        skipDefaultCheckout true
    }

    stages {
        stage ('checkout') {
            steps {
                script {
                    def repo = checkout scm
                    revision = sh(script: 'git log -1 --format=\'%h.%ad\' --date=format:%Y%m%d-%H%M | cat', returnStdout: true).trim()
                    branch = repo.GIT_BRANCH.take(20).replaceAll('/', '_')
                    if (branch != 'master') {
                        revision += "-${branch}"
                    }
                    sh "echo 'Building revision: ${revision}'"
                }
            }

        }
        stage ('compile') {
            steps {
                container('maven') {
                    sh 'mvn clean compile test-compile'
                }
            }
        }
        stage ('unit test') {
            steps {
                container('maven') {
                    sh 'mvn test'
                }
            }
        }
        stage ('integration test') {
            steps {
                container ('maven') {
                    sh 'mvn verify'
                }
            }
        }
        stage ('build artifact') {
            steps {
                container('maven') {
                    sh "mvn package -Dmaven.test.skip -Drevision=${revision}"
                }
                container('docker') {
                    script {
                        registryIp = sh(script: 'getent hosts registry.kube-system | awk \'{ print $1 ; exit }\'', returnStdout: true).trim()
                        sh "docker build . -t ${registryIp}/demo/app:${revision} --build-arg REVISION=${revision}"
                    }
                }
            }
        }
        stage ('publish artifact') {
            when {
                expression {
                    branch == 'master'
                }
            }
            steps {
                container('docker') {
                    sh "docker push ${registryIp}/demo/app:${revision}"
                }
            }
        }
    }
}


Дополнительные Jenkins pipelines для управления жизненным циклом приложения

Предположим, что репозитории организованы так что:


  • содержат отдельное приложение в виде docker image
  • могут быть задеплоены использую helm файлы, которые рассположены в директории helm
  • версионируются используя один и тот же подход и имеют файл helm/setVersion.sh для установки ревизии в helm charts

Тогда мы можем построить несколько Jenkinsfile pipelines для управления жизненным циклом приложения, а именно:

В Jenkinsfile каждого проекта, можно добавить вызов deploy pipeline который будет выполнятся при каждой успешной компиляции master ветки либо при явном запросе deploy ветки на тестовое окружение.


Jenkins file deploy pipeline call
...
        stage ('deploy to env') {
            when {
                expression {
                    branch == 'master' || params.DEPLOY_BRANCH_TO_TST
                }
            }
            steps {
                build job: './../Deploy', parameters: [
                        [$class: 'StringParameterValue', name: 'GIT_REPO', value: 'habr-demo-app'],
                        [$class: 'StringParameterValue', name: 'VERSION', value: revision],
                        [$class: 'StringParameterValue', name: 'ENV', value: branch == 'master' ? 'staging' : 'test']
                ], wait: false
            }
        }
...

Тут можно найти Jenkinsfile с учетом всех шагов.

Таким образом можно построить continuous deployment на выбраное тестовое либо боевое окружение, также используя jenkins либо его email/slack/etc нотификации, иметь аудит того, какое приложение, какой версии, кем, когда и куда было задеплоено.


Заключение

Используя Jenkinsfile и helm можно достаточно просто построить ci/cd для вашего приложеня. Этот способ может быть наиболее актуальным для небольших команд которые недавно начали использовать kubernetes и не имеют возможности (независимо от причины) использовать сервисы которые могут предоставлять такую функциональность из коробки.

Примеры конфигурации для окружений, Jenkins и pipeline для управления жизненным циклом приложения вы можете найти здесь и пример приложения с Jenkinsfile здесь.

© Habrahabr.ru