Оптимизируем системные ресурсы при развёртывании за счёт перехода на динамику
Всем привет! Если в компании растёт количество продуктов, а для их развёртывания используются виртуальные машины, то рано или поздно возникает задача оптимизации ресурсов. Скажем, вы используете для оркестрации Jenkins. Количество агентов на ВМ при этом статично, а количество развёртываний в разное время разное. В этом случае при массовых установках агенты периодически упираются в установленный лимит исполнителей (executor), а в свободные часы ВМ простаивают, занимая ресурсы.
Мы, команда Run4Change в СберТехе, сопровождаем тестовые среды. В наши задачи входит в том числе развёртывание продуктов облачной платформы Platform V на стендах для последующего тестирования. Расскажем, как мы решили проблему использования системных ресурсов и отказались от виртуальных машин в пользу cloud‑native‑решения. Статья может быть полезна тем, кто планирует начать использование динамических агентов Jenkins, и может использоваться как первоначальное руководство.
Проблема баланса «затраты‑производительность»
Обычно для развёртывания мы используем набор конвейеров, которые оркеструются с помощью Jenkins. До 2022 года используемый нами КТС для развёртывания выглядел так:
Контроллер Jenkins на отдельном хосте (виртуальная машина) под управлением Red Hat Enterprise Linux.
Набор агентов, каждый на отдельной виртуальной машине с ОС RHEL.
Сами агенты были практически идентичны как по ресурсам, так и по набору установленного на них ПО.
В таком виде эта часть производственного конвейера обладала всеми описанными выше недостатками: при массовых установках возникала проблема лимита, а в незагруженные часы ВМ просто занимала ресурсы, не принося никакой пользы.
Увеличивать количество виртуальных машин для агентов было нецелесообразно, а после 2022 года RHEL к тому же перестала поддерживаться в России. Всё это стало толчком для решительных действий: мы поняли, что нужно не только оптимизировать ресурсы, но и менять RHEL на другую ОС.
Решение рядом: переезжаем с ВМ на динамические агенты
Альтернатива ОС от RedHat была очевидна. В 2023 году в СберТехе появилась собственная операционная система Platform V OS SberLinux. Jenkins — как сам контроллер, так и агент — это Java‑приложение, поэтому переход с одной ОС на другую выглядел тривиальной задачей. А с учётом того, что Platform V SberLinux в своей основе совместим с RHEL, нам не нужно было даже менять набор пакетов для установки.
Виртуальные машины решили заменить динамическими агентами, работающими в кластере Platform V DropApp, совместимом с Kubernetes. Это должно было решить описанные недостатки при использовании виртуальных машин. Агент создаётся по запросу мастера Jenkins и уничтожается сразу после завершения конвейера, освобождая используемые ресурсы. Количество запускаемых агентов ограничивается только ресурсами самого кластера Platform V DropApp.
При этом дополнительно можно гарантировать, что на одном агенте одновременно работает только один конвейер. Это обеспечивает изолированность разных развёртываний друг от друга, что является дополнительным преимуществом и минимизирует возникновение потенциальных «коллизий».
Собираем образ, разбираемся с параметрами
Работа динамических агентов в Jenkins реализуется с помощью плагина Kubernetes. Динамический агент инициируется запросом от контроллера Jenkins через плагин в сторону API‑сервера кластера Platform V DropApp. По сути, в кластер передаётся полностью готовый манифест ресурса PodTemplate со всеми необходимыми для работы параметрами, включая имя агента, URL контроллера, секрет для подключения и другие параметры.
По этому шаблону в кластере создаётся под, который после запуска инициирует подключение к контроллеру Jenkins. Как только агент подключился к контроллеру, начинается обычное взаимодействие по JNLP‑протоколу, как и с обычным статическим агентом. По окончании задания контроллер инициирует удаление пода, отправляя команду в API‑сервер кластера.
Итак, у нас есть работающий контроллер Jenkins и чистый кластер Platform V DropApp. В первую очередь нам необходим образ агента. Создадим Dockerfile для сборки образа.
# В качестве базового образа возьмем образ SberLinux
FROM localregistry/sblnxos/container-8-ubi-sbt:8.8.2-189
#В базовый образ требуется установить необходимое ПО:
# - Java Development Kit для запуска агента Jenkins
# - Python для выполнения кода деплоя
# - Git для работы с Source Code Management
# - Вспомогательные утилиты (jq, zip, unzip, gcc, rsync, gettext и т.д.)
# Полный набор необходимого ПО:
RUN yum -y install glibc-langpack-ru glibc-langpack-en java-17-openjdk openssh git sudo openssl sshpass time jq wget zip unzip python36 python36-devel gcc rsync gettext
#Если используется стороннее зеркало с модулями Python, как у нас, то следует не забыть #принести информацию о нем, например, через определение зеркала в /etc/pip.conf (https://pip.pypa.io/en/stable/topics/configuration/). Копируем свой pip.conf в образ.
COPY add/pip.conf /etc
#Дальше устанавливаем модули для Python.
RUN pip3 install --no-deps ansible==2.9.24 asn1crypto==0.24.0 certifi==2021.10.8 cffi==1.12.3 charset-normalizer==2.0.10 cryptography==2.8 cssselect==0.9.1 Genshi==0.7 html5lib==0.999999999 hvac==0.11.2 idna==3.3 Jinja2==2.11.0 jmespath==0.10.0 lxml==4.4.2 MarkupSafe==2.0.1 pycparser==2.19 pycrypto==2.6.1 pyOpenSSL==18.0.0 python-ntlm==1.1.0 PyYAML==6.0 requests==2.27.1 six==1.12.0 urllib3==1.26.8 webencodings==0.5.1 dnspython==1.16.0
#Добавим пользователя, от которого будет запускаться агент, и его рабочий каталог
RUN useradd -u 1000 jenkins && mkdir -p /u01/jenkins && chown -R jenkins:jenkins /u01/jenkins
#Cкопируем файл агента в образ.
COPY add/agent.jar /usr/share/java
#Следующие шаги будут выполняться в окружении пользователя jenkins
USER jenkins
# Определим переменные окружения:
ENV HOME=/home/jenkins
ENV JAVA_HOME=/usr/lib/jvm/jre/
ENV LANGUAGE=en_US:en
ENV LANG=en_US.UTF-8
ENV AGENT_WORKDIR=/u01/jenkins
ENV TZ=Europe/Moscow
ENV ANSIBLE_HOST_KEY_CHECKING=False
#И наконец команда для запуска процесса агента:
ENTRYPOINT [‘java -cp / usr/share/java/slave.jar -headless $TUNNEL $URL $WORKDIR $OPT_JENKINS_SECRET $OPT_JENKINS_AGENT_NAME "$@"’]
Мы специально используем опцию ‑no‑deps
, чтобы запретить пакетам приносить зависимости других версий. Правда, в этом случае требуемый набор зависимостей нужно установить здесь же самим.
Файл клиента agent.jar «прибиваем гвоздями» в образе. Вообще, версия агента должна соответствовать версии контроллера Jenkins. Актуальный для этой версии контроллера агент всегда можно получить по адресу ${JENKINS_URL}/jnlpJars/agent.jar. Но так как нам важна стабильность, мы не обновляем Jenkins при каждом его релизе, и необходимость актуализации агента в образе возникает не чаще раза в квартал.
Пересборка образа с новым агентом занимает от силы минут 5. Поэтому вариант выкачивания актуального образа напрямую с контроллера при каждом запуске пода мы отмели для экономии времени и ресурсов.
Осталось собрать образ агента. Переходим в каталог с Dockerfile и выполняем
docker build. ‑t dockerregistry/jenkins/sbel‑agent:p3
Далее нам нужно «подружить» контроллер с кластером Platform V DropApp. На стороне кластера потребуется завести учётку (Service Account) и роль (Role), и связать их (RoleBinding). Описание манифестов можно взять из примера.
Применим файл с манифестами:
kubectl ‑f service‑account.yml
Токен для Service Account можно сгенерировать следующим YAML‑манифестом:
apiVersion: v1
kind: Secret
metadata:
name: jenkins-secret
annotations:
kubernetes.io/service-account.name: jenkins
type: kubernetes.io/service-account-token
И также применить его:
kubectl ‑f jenkins‑secret.yaml
Сам токен можно получить, выполнив команду:
kubectl describe secret jenkins‑secret
Для скачивания образа кластером Platform V DropApp из Docker‑репозитория требуется создать секрет типа kubernetes.io/dockerconfigjson — это обычный JSON‑конфиг для Docker, который можно создать и сразу же применить такой конструкцией:
kubectl create secret docker-registry dockerregistry-secret --docker-server=dockerregistry --docker-username=$DOCKER_USER --docker-password=$DOCKER_PASSWORD --docker-email=$DOCKER_EMAIL -o yaml | kubectl apply -f -
На стороне кластера DropApp работы завершены. Переходим к настройке контроллера Jenkins. Переходим по пути «Настроить Jenkins — Nodes — Clouds — New cloud». Указываем любое имя, выбираем тип «Kubernetes» и жмём «Создать»:
На следующем экране раскрываем детали и указываем URL API‑сервера кластера Platform V DropApp, пространство имён кластера, при использовании HTTPS указываем ключ сертификата (Kubernetes server certificate key) или же вообще запрещаем проверку сертификатов (Disable https certificate check).
В «Credentials» нужно добавить токен, сгенерированный на шаге подготовки кластера Platform V DropApp. Жмём »+Add» и в глобальном домене для учётных данных добавляем запись с типом «Secret text»: сам токен в поле Secret, его идентификатор (ID) и описание, если надо.
Остальные параметры можно не заполнять или оставить со стандартными значениями. После сохранения параметров можно зайти в созданное облако и проверить соединение с кластером через кнопку «Test connection».
Далее переходим в раздел «Pod templates» и создаём шаблон пода динамического агента.
Добавляем контейнер в шаблон пода через «Add Container»:
Name — имя, обязательно.
Namespace — пространство имён в Platform V DropApp. Необязательное, будет использовано пространство, указанное в общих настройках облака.
ImagePullSecrets — имя секрета в кластере, который содержит учётные данные для извлечения Docker‑образа из репозитория (значение dockerregistry-secret
в примере выше).
Label — метка агента, использующаяся для связи задачу Jenkins и агента.
Name — имя контейнера, исторически и для обратной совместимости это «jnlp».
Docker image — образ контейнера, в нашем примере это ранее нами созданный dockerregistry/jenkins/sbel‑agent: p3.
Always pull image — рекомендую всегда использовать эту опцию. Она соответствует строке манифеста шаблона пода imagePullPolicy: Always
.
При отсутствии опции политика пуллинга будет IfNotPresent: скачивание образа при его отсутствии на воркер‑ноде кластера. И в этом случае можно столкнуться с тем, что на стороне кластера будет использоваться ранее кешированный на воркере образ агента, который может не соответствовать актуальной версии собранного образа.
При установленной опции даже в случае большого образа длительность из‑за скачивания не увеличится. Реальное скачивание произойдёт только в том случае, если дайджест кешированного на воркере образа не будет соответствовать дайджесту текущего актуального образа в docker‑registry.
Working directory — рабочий каталог на агенте с полным доступом для пользователя в образе, от которого запущен процесс агента.
Command to run — можно не указывать, потому что в образе мы использовали инструкцию ENTRYPOINT
, которая и будет запускать процесс на агенте.
Arguments to pass to the command —, а эта опция важна, потому что через неё передаются «агентозависимые» параметры, использующиеся для подключения агента к контроллеру, такие как:
${computer.name} — имя агента;
${computer.jnlpmac} — секрет для подключения агента к контроллеру (вычисляется контроллером по алгоритму на основе имени агента). Эта строка заменит собой специальный параметр
$@
при вызовеENTRYPOINT
образа.
Есть ещё множество параметров, которые можно заполнить в шаблоне пода или контейнера в Jenkins. Все они будут транслированы плагином в соответствующие ключи манифеста шаблона пода или контейнера. При незаполненном значении параметры будут отсутствовать в шаблоне, а значит заполнятся уже на стороне Platform V DropApp стандартными значениями.
Обратите внимание на параметр Raw YAML for the Pod
и сопутствующий ему параметр Yaml merge strategy
. Они позволяют принести в шаблон пода любой, даже неопределённый в плагине параметр. Достаточно дописать YAML‑фрагмент, который необходимо слить с манифестом шаблона, а также выбрать стратегию слияния: Override (переопределить) или Merge (объединить).
При заполнении важно соблюсти необходимое количество пробелов, чтобы результирующий YAML‑файл в конечном итоге был корректен. В качестве примера приведём добавление в контейнер реквестов и лимитов и монтирование ConfigMap через параметр Raw YAML for the Pod
:
spec:
containers:
- resources:
limits:
cpu: '4'
memory: 8Gi
requests:
cpu: '4'
memory: 8Gi
name: jnlp
volumeMounts:
- name: config
mountPath: /u01/config
readOnly: true
volumes:
- name: config
configMap:
name: cm-jenkins
Проверяем результат
Ну и, наконец, проверка работы. Создадим в Jenkins тестовый джоб (New Item) типа Pipeline со следующим скриптом:
pipeline {
agent {
label('sbel-agent-p3')
}
stages {
stage('Testing of dynamic agent') {
steps {
sh ('echo "Hello from dynamic agent $HOSTNAME"')
}
}
}
}
Запустим задачу и посмотрим результат в выводе консоли Jenkins:
Из журнала видно, что на основе шаблона агента с именем sbel‑agent‑p3 в кластере PlatformVDropApp создался под с именем sbel‑agent‑p3-k7rsr. Jenkins‑агент, запущенный в поде, соединился с контролером Jenkins, получил от него задание выполнить указанную в конвейере команду. По завершении задачи под удалился, освободив системные ресурсы.
Вместо заключения
Сейчас мы полностью отказались от агентов на виртуальных машинах и используем исключительно реализацию на динамике. Каких преимущества это даёт:
Динамические агенты позволяют легко масштабировать систему в зависимости от нагрузки.
Динамические агенты обеспечивают эффективно управлять ресурсами, позволяя останавливать и запускать контейнеры по мере необходимости.
Контейнеры имеют свои собственные настройки и ограничения и изолированы друг от друга.
В дальнейшем рассматриваем возможность переноса контроллера Jenkins с виртуальной машины в кластер Platform V DropApp. Например, в случае запуска большого количества параллельных развёртываний может снижаться производительность и самого контролера. В таком случае в этот период можно запускать дополнительный экземпляр контроллера для распределения нагрузки между ними.
Надеемся, что статья была вам полезна! Если появились вопросы о технических деталях, которые мы могли упустить в статье, добро пожаловать в комментарии.