Jenkins: запуск slaves в Kubernetes и билд Docker-образов

Автор: | 02/26/2021
 

Имеется у нас 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:

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

И Docker Compose.

Находим последнюю версию в Github, на момент написания это 1.28.4, и загружаем его:

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

Запуск Jenkins в Docker

На хосте создаём каталог, в котором Jenkins будет хранить свои данные:

root@ip-10-0-4-6:/home/ubuntu# mkdir jenkins_home

Пишем 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"

Запускаем:

root@ip-10-0-4-6:/home/ubuntu# docker-compose -f jenkins-compose.yaml up

И открываем в браузере:

Administrator password есть в выводе Docker Compose при старте сервера, либо можно его взять в самом контейнере в файле /var/lib/jenkins/secrets/initialAdminPassword:

root@ip-10-0-4-6:/home/ubuntu# docker exec -ti ubuntu_jenkins_1 cat /var/lib/jenkins/secrets/initialAdminPassword
15d***730

Логинимся, выполняем начальную установку:

Создаём юзера:

Завершаем установку, и переходим к установке плагина.

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

Деплоим:

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

Jenkins ServiceAccount и kubeconfig

Надо создать kubeconfig, который будет использовать этот ServiceAccount.

Для этого нужны Cluster ARN, Certificate authority, адрес API-сервера, и JWT-токен нашего ServiceAccount.

Находим секрет этого ServiceAccount:

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

Certificate authority и Cluster ARN берём в админке Амазона:

Получаем токен из секрета, декриптим из base64:

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

Пишем 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=

Проверяем доступ:

kubectl -n dev---kubeconfig ../jenkins-dev-1-18-kubeconfig.yaml auth can-i get pod
yes

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:

...
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
...

Под создаётся:

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

И выполнение джобы:

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 .'
        }
    }
}

Собираем:

Готово.

Ссылки по теме