Имеется у нас Jenkins, который запускает в Docker-контейнерах свои задачи.
Со временем столкнулись с тем, что инстанс t2.2xlarge (8 CPU, 32 RAM) при пиковых нагрузках уже не справляется – забиваются и память, и процессорное время.
Варианты – либо продолжать вертикальный скейлинг одного мастер-инстанса, и на нём дальше в Docker запускать джобы – или вынести запуск джоб на внешние слейвы.
Сейчас у нас есть три внешних слейва для Jenkins – Android-билды выполняются на машинке в офисе, на которой установлен Android Studio (128 ГБ памяти, кажется), и пачка MacMini для iOS билдов, плюс выделенный AWS EC2 для запуска UI-тестов нашей QA-команды.
Добавим к этому зоопарку запуск слейвов в Kubernetes-кластере.
Используем AWS Elastic Kubernetes Service и Kubernetes Plugin для Jenkins.
Содержание
Jenkins-master: подготовка
Для тестов создадим ЕС2 с Ubuntu 20.04, на нём в Docker поднимем тестовый мастер-инстанс Jenkins, в котором установим плагин.
Docker install
Запускаем ЕС2, подключаемся, устанавливаем Docker:
[simterm]
root@ip-10-0-4-6:/home/ubuntu# apt update && apt -y upgrade root@ip-10-0-4-6:/home/ubuntu# curl https://get.docker.com/ | bash
[/simterm]
И Docker Compose.
Находим последнюю версию в Github, на момент написания это 1.28.4, и загружаем его:
[simterm]
root@ip-10-0-4-6:/home/ubuntu# curl -L "https://github.com/docker/compose/releases/download/1.28.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose root@ip-10-0-4-6:/home/ubuntu# chmod +x /usr/local/bin/docker-compose root@ip-10-0-4-6:/home/ubuntu# docker-compose --version docker-compose version 1.28.4, build cabd5cfb
[/simterm]
Запуск Jenkins в Docker
На хосте создаём каталог, в котором Jenkins будет хранить свои данные:
[simterm]
root@ip-10-0-4-6:/home/ubuntu# mkdir jenkins_home
[/simterm]
Пишем Docker Compose файл:
version: '3.5' networks: jenkins: name: jenkins services: jenkins: user: root image: jenkins/jenkins:2.249.3 networks: - jenkins ports: - '8080:8080' - '50000:50000' volumes: - /home/ubuntu/jenkins_home/:/var/lib/jenkins - /var/run/docker.sock:/var/run/docker.sock - /usr/bin/docker:/usr/bin/docker - /usr/lib/x86_64-linux-gnu/libltdl.so.7:/usr/lib/x86_64-linux-gnu/libltdl.so.7 environment: - JENKINS_HOME=/var/lib/jenkins - JAVA_OPTS=-Duser.timezone=Europe/Kiev logging: driver: "journald"
Запускаем:
[simterm]
root@ip-10-0-4-6:/home/ubuntu# docker-compose -f jenkins-compose.yaml up
[/simterm]
И открываем в браузере:
Administrator password есть в выводе Docker Compose при старте сервера, либо можно его взять в самом контейнере в файле /var/lib/jenkins/secrets/initialAdminPassword
:
[simterm]
root@ip-10-0-4-6:/home/ubuntu# docker exec -ti ubuntu_jenkins_1 cat /var/lib/jenkins/secrets/initialAdminPassword 15d***730
[/simterm]
Логинимся, выполняем начальную установку:
Создаём юзера:
Завершаем установку, и переходим к установке плагина.
Jenkins Slaves in Kubernetes
Находим плагин Kubernetes:
Устанавливаем, переходим в Manage Nodes and Clouds > Configure Clouds:
Выбираем Kubernetes:
Задаём URL API-сервера и Namespace:
Jenkins ServiceAccount
Создаём Namespace и ServiceAccount, под которым будем авторизировать наш Jenkins-мастер.
На Production-инстансе можно сделать через EC2 Instance Profile, которому будет подключаться нужная IAM-роль.
Тут же описываем Kubernetes RoleBinding на дефолтную админ-роль (или пишем свою роль) в нашем неймспейсе dev-1-18-devops-jenkins-slaves-ns:
--- apiVersion: v1 kind: Namespace metadata: name: dev-1-18-devops-jenkins-slaves-ns --- apiVersion: v1 kind: ServiceAccount metadata: name: jenkins-slaves-service-account namespace: dev-1-18-devops-jenkins-slaves-ns --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: jenkins-slaves-rolebinding namespace: dev-1-18-devops-jenkins-slaves-ns roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: admin subjects: - kind: ServiceAccount name: jenkins-slaves-service-account namespace: dev-1-18-devops-jenkins-slaves-ns
Деплоим:
[simterm]
$ kubectl apply -f jenkins-slaves-sa.yaml namespace/dev-1-18-devops-jenkins-slaves-ns created serviceaccount/jenkins-slaves-service-account created rolebinding.rbac.authorization.k8s.io/jenkins-slaves-rolebinding created
[/simterm]
Jenkins ServiceAccount и kubeconfig
Надо создать kubeconfig, который будет использовать этот ServiceAccount.
Для этого нужны Cluster ARN, Certificate authority, адрес API-сервера, и JWT-токен нашего ServiceAccount.
Находим секрет этого ServiceAccount:
[simterm]
$ kubectl -n dev-1-18-devops-jenkins-slaves-ns get sa jenkins-slaves-service-account -o jsonpath='{.secrets[0].name}' jenkins-slaves-service-account-token-jsbb7
[/simterm]
Certificate authority и Cluster ARN берём в админке Амазона:
Получаем токен из секрета, декриптим из base64
:
[simterm]
$ kubectl -n dev-1-18-devops-jenkins-slaves-ns get secret jenkins-slaves-service-account-token-jsbb7 -o jsonpath='{.data.token}' | base64 --decode eyJ...s7w
[/simterm]
Пишем kubeconfig, например в файл jenkins-dev-1-18-kubeconfig.yaml
:
apiVersion: v1 clusters: - cluster: certificate-authority-data: LS0...LQo= server: https://676***892.gr7.us-east-2.eks.amazonaws.com name: arn:aws:eks:us-east-2:534***385:cluster/bttrm-eks-dev-1-18 contexts: - context: cluster: arn:aws:eks:us-east-2:534***385:cluster/bttrm-eks-dev-1-18 user: jenkins-slaves-service-account namespace: dev-1-18-devops-jenkins-slaves-ns name: jenkins-slaves-service-account@bttrm-dev-1-18 current-context: jenkins-slaves-service-account@bttrm-dev-1-18 kind: Config users: - name: jenkins-slaves-service-account user: token: ZXl...N3c=
Проверяем доступ:
[simterm]
$ kubectl -n dev---kubeconfig ../jenkins-dev-1-18-kubeconfig.yaml auth can-i get pod yes
[/simterm]
Jenkins Kubernetes Credentials
Возвращаемся к Jenkins, добавляем Credential:
Проверяем подключение – кликаем Test Connection, получаем ответ Connected to Kubernetes 1.18+:
Сохраняем.
Jenkins Slaves Pod Template
Переходим в Pod templates:
Заполняем шаблон пода и дефолтного контейнера, в качестве образа используем jenkinsci/jnlp-slave
:
Jenkins Job
Создаём тестовую джобу, тип Pipeline:
Пишем скрипт:
podTemplate { node(POD_LABEL) { stage('Run shell') { sh 'echo hello world' } } }
Запускаем, смотрим логи Jenkins:
[simterm]
... jenkins_1 | 2021-02-26 08:36:32.226+0000 [id=277] INFO hudson.slaves.NodeProvisioner#lambda$update$6: k8s-1-b5j7g-glscn-v0tfz provisioning successfully completed. We have now 2 computer(s) jenkins_1 | 2021-02-26 08:36:32.522+0000 [id=276] INFO o.c.j.p.k.KubernetesLauncher#launch: Created Pod: dev-1-18-devops-jenkins-slaves-ns/k8s-1-b5j7g-glscn-v0tfz ...
[/simterm]
Под создаётся:
[simterm]
$ kubectl --kubeconfig ../jenkins-dev-1-18-kubeconfig.yaml get pod NAME READY STATUS RESTARTS AGE k8s-1-b5j7g-glscn-v0tfz 0/1 ContainerCreating 0 12s
[/simterm]
И выполнение джобы:
Docker в Docker через Docker в Kubernetes
Наш Jenkins работает в Docker-контейнере.
И свои билды он тоже запускает в Docker-контейнерах. Способ испытанный, удобный – не захламляем хост-машину библиотеками, билды независимы, девелоперы сами могут настраивать свои среды сборок.
Теперь нам надо повторить тоже самое, но со слейвами в Kubernetes.
В Jenkins устанавливаем Docker pipeline plugin:
И пишем пайплайн.
Тут нам понадобится новый образ – docker.dind
, а для него – новый шаблон пода.
Можем создать его в UI, как делали в начале, можем описать прямо в пайплайне используя podTemplate
.
Например – соберём образ NGINX:
podTemplate(yaml: ''' apiVersion: v1 kind: Pod spec: containers: - name: docker image: docker:19.03.1-dind securityContext: privileged: true env: - name: DOCKER_TLS_CERTDIR value: "" ''') { node(POD_LABEL) { git 'https://github.com/nginxinc/docker-nginx.git' container('docker') { sh 'docker version && cd stable/alpine/ && docker build -t nginx-example .' } } }
Собираем:
Готово.