Оптимизируем системные ресурсы при развёртывании за счёт перехода на динамику

Всем привет! Если в компании растёт количество продуктов, а для их развёртывания используются виртуальные машины, то рано или поздно возникает задача оптимизации ресурсов. Скажем, вы используете для оркестрации Jenkins. Количество агентов на ВМ при этом статично, а количество развёртываний в разное время разное. В этом случае при массовых установках агенты периодически упираются в установленный лимит исполнителей (executor), а в свободные часы ВМ простаивают, занимая ресурсы.

Мы, команда Run4Change в СберТехе, сопровождаем тестовые среды. В наши задачи входит в том числе развёртывание продуктов облачной платформы Platform V на стендах для последующего тестирования. Расскажем, как мы решили проблему использования системных ресурсов и отказались от виртуальных машин в пользу cloud‑native‑решения. Статья может быть полезна тем, кто планирует начать использование динамических агентов Jenkins, и может использоваться как первоначальное руководство.

Проблема баланса «затраты‑производительность»

Обычно для развёртывания мы используем набор конвейеров, которые оркеструются с помощью Jenkins. До 2022 года используемый нами КТС для развёртывания выглядел так:

  1. Контроллер Jenkins на отдельном хосте (виртуальная машина) под управлением Red Hat Enterprise Linux.

  2. Набор агентов, каждый на отдельной виртуальной машине с ОС 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» и жмём «Создать»:

ef56653f60c26f0644c78bbca6cc18e4.png

На следующем экране раскрываем детали и указываем URL API‑сервера кластера Platform V DropApp, пространство имён кластера, при использовании HTTPS указываем ключ сертификата (Kubernetes server certificate key) или же вообще запрещаем проверку сертификатов (Disable https certificate check).

9339b019d2066e3c14f4e78246fcf1e5.png

В «Credentials» нужно добавить токен, сгенерированный на шаге подготовки кластера Platform V DropApp. Жмём »+Add» и в глобальном домене для учётных данных добавляем запись с типом «Secret text»: сам токен в поле Secret, его идентификатор (ID) и описание, если надо.

f46e12f6d58bdab6f77e59ff8ff62167.png

Остальные параметры можно не заполнять или оставить со стандартными значениями. После сохранения параметров можно зайти в созданное облако и проверить соединение с кластером через кнопку «Test connection».

a3492a7e9a050be13b9872115b2c6102.png

Далее переходим в раздел «Pod templates» и создаём шаблон пода динамического агента.

d8c1664d10df44bc6a5c9707f7b726bc.png

Добавляем контейнер в шаблон пода через «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 образа.

42edcb308cc6d151a47eca47ef64dcb3.png

Есть ещё множество параметров, которые можно заполнить в шаблоне пода или контейнера в 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:

112f06bb9de9c297a7a7f086350a6f83.png

Из журнала видно, что на основе шаблона агента с именем sbel‑agent‑p3 в кластере PlatformVDropApp создался под с именем sbel‑agent‑p3-k7rsr. Jenkins‑агент, запущенный в поде, соединился с контролером Jenkins, получил от него задание выполнить указанную в конвейере команду. По завершении задачи под удалился, освободив системные ресурсы.

Вместо заключения

Сейчас мы полностью отказались от агентов на виртуальных машинах и используем исключительно реализацию на динамике. Каких преимущества это даёт:

  • Динамические агенты позволяют легко масштабировать систему в зависимости от нагрузки.

  • Динамические агенты обеспечивают эффективно управлять ресурсами, позволяя останавливать и запускать контейнеры по мере необходимости.

  • Контейнеры имеют свои собственные настройки и ограничения и изолированы друг от друга.

В дальнейшем рассматриваем возможность переноса контроллера Jenkins с виртуальной машины в кластер Platform V DropApp. Например, в случае запуска большого количества параллельных развёртываний может снижаться производительность и самого контролера. В таком случае в этот период можно запускать дополнительный экземпляр контроллера для распределения нагрузки между ними.

Надеемся, что статья была вам полезна! Если появились вопросы о технических деталях, которые мы могли упустить в статье, добро пожаловать в комментарии.

© Habrahabr.ru