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

Окружения
Я предлагаю использовать следующие окружения:
- 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, набор предустановленых плагинов, доменное имя и прочую конфигурацию
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 для каждой сборки. Благодаря этому команда больше не будет ожидать свободный агент для сборки, а бизнес сможет сэкономить на количестве необходимых серверов.

Так же из коробки настроен PersistenceVolume для сохранения 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

В случае с 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: - ReadWriteMany 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:
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 для управления жизненным циклом приложения, а именно:
- деплоя на любое окружение
- удаления с любого окружения
- promote с staging на production
- отката на предыдущую версию
В Jenkinsfile каждого проекта, можно добавить вызов deploy pipeline который будет выполнятся при каждой успешной компиляции master ветки либо при явном запросе deploy ветки на тестовое окружение.
... 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 здесь.
