[Из песочницы] CI/CD, используя Jenkins на Kubernetes
Добрый день.
На Хабре уже есть несколько статей о 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 для каждой сборки. Благодаря этому команда больше не будет ожидать свободный агент для сборки, а бизнес сможет сэкономить на колличестве необходимых серверов.
Так же из коробки настроен 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
В случае с 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:
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 ветки на тестовое окружение.
...
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 здесь.