Привет Хабр! На связи Рустем, IBM Senior DevOps Engineer & Integration Architect.Сегодня я хотел бы поговорить про Jenkins Dynamic Agents в Kubernetes.Jenkins — это инструмент CI/CD с достаточно долгой историей, который продолжает развиваться. Его архитектура Master/Agent отлично подходит для масштабируемости для выполнения распределенных сборок. Существует множество способов интеграции и использования агента Jenkins: от использования физических(bare metal) до виртуальных машин, динамических инстансов EC2, Docker-контейнеров или кластеров Kubernetes. Провести интеграцию между Jenkins и кластером Kubernetes — это было отличным решением. Благодаря этим преимуществам я полностью перенес конвейер CI/CD с агента на основе хоста (VM) на агент на основе Pod. Что я получил в итоге: Динамический агент Jenkins в Kubernetes, который является легковесным и инициализируется по запросу в течение нескольких секунд. Как говорится, always fresh, а главное — воспроизводимая агентская среда Jenkins для каждой сборки, с соблюдением принципа идемпотентности. Экономия ресурсов/затрат благодаря Kubernetes (под с агентом внутри существует только в момент сборки). В этой статье я хотел бы поделиться своим недавним подходом к динамической подготовке агента Jenkins с помощью простых строк кода в пайплайне Jenkins с использованием метода общей библиотеки Jenkins. Первый шаг можно описать двумя способами: декларативно и скриптованно. В этом примере с имя «mini+pg» предназначено для запроса агента Jenkins пода типа «mini» со стандартным контейнером JNLP плюс контейнером «postgresql». Типы «mini» и «pg» являются определенными шаблонами в файлах ресурсов. В библиотеке есть и другие шаблоны, такие как «small», «large», «privileged», которые мы можем смешивать, добавлять или изменять. Помимо возможности динамического запроса агента Jenkins для запуска ваших конвейеров CI/CD, это также и метод динамического определения требуемого агента. Существует 2 вида метода интеграции. Существующий экземпляр Jenkins подключается к новому кластеру Kubernetes. Когда я делал интеграцию в первый раз, у меня уже был запущен рабочий Jenkins, так что это определенно был прямой путь для меня. Но есть и второй метод, как я уже сказал ранее.Можно создать новый экземпляр Jenkins внутри кластера Kubernetes. После запуска первой модели в Dev-environment, я попробовал вторую модель и думаю, что это более практичный метод. Плагин Kubernetes для Jenkins предоставляет документацию и примеры для различных методов определения простого/сложного агента. Агент Jenkins — это под, предоставленный Kubernetes, с несколькими контейнерами, которые запускают указанные образы Docker. Основной контейнер должен быть из jenkins/jnlp-slave или совместимого образа. Лучше собрать свой собственный образ, включая содержимое Dockerfile из jenkins/docker-slave. В основном существует 3 метода предоставления агента: YAML в декларативном пайплайне; Конфигурации в скриптовом пайплайне; Конфигурации в пользовательском интерфейсе Jenkins. Пример декларативного описания: Пример UI-метода Метод конфигурации через UI является для меня отправной точкой, так как он аналогичен другим типам агентов, определенным в мастере Jenkins. Я определил несколько «шаблонов Pod» для различных задач. Например, для большинства сборок подходит «slave» 4 CPU и 8 GB RAM. Некоторым крупным проектам может потребоваться 8 CPU и 16 GB RAM,, что называется «large slave». Некоторым другим может понадобиться дополнительный контейнер PostgreSQL для тестирования, который называется «slave-pg». Некоторые шаблоны похожи тем, что мы даже можем использовать функцию «Наследование шаблонов», чтобы сэкономить усилия, что приятно. В пайплайне легко использовать метку шаблона пода для выбора агента. Однако несколько проблем, которые мы можем наблюдать с течением времени. Шаблон Pod слишком сложен для настройки из пользовательского интерфейса Jenkins. Один шаблон пода может легко содержать более 30 элементов конфигурации. Это демонстрирует большую гибкость Kubernetes, но не очень хорошо для работы в UI. Несколько шаблонов Pod не управляются. По мере увеличения потребности в «разных задачах» — увеличивается и количество настраиваемых шаблонов. Поскольку количество шаблонов увеличивается, то пользовательский интерфейс конфигурации Jenkins будет имеет более 10 страниц для этой секции. Будет достаточно долго и сложно искать правильное место для внесения правильных изменений. По мере того, как я запускаю в производство больше экземпляров Jenkins, становится проблемой настроить эти шаблоны для разных экземпляров. Сомневаюсь, что кому-то захочется копировать целую кучу 10-ти страничных конфигураций с одного Jenkins на другой. Также конфигурация Дженкинса как код (JCasC) еще не является панацеей. То, как проект использует «метку агента А» в своем пайплайне, создает зависимость от «Агента А», определенного в Jenkins, как тайну. Невозможно точно узнать, что находится в агенте A, является ли A сегодня таким же, как вчера, или A отличается среди экземпляров Jenkins. Это проблема, если Агент создан изменяемым образом. По мере развития проекта требования к Агенту также меняются. Например, для проекта версии 1.0.0 может потребоваться только стандартный агент, а для версии 1.5.0 — агент с контейнером PostgreSQL для тестирования. Иногда образы докеров, используемые в агенте, также нуждаются в обновлении. Звучит легко, можно попросить администратора Jenkins изменить агент или создать новый. Но вскоре такие изменения будет все сложнее отслеживать. Даже с этими минусами этот метод по-прежнему “мастхэв” для тех пайплайнов, которые работают на различных типах агентов, отличных от Pod в Kubernetes. Глядя на документацию плагина Kubernetes для Jenkins, я полагаю, что идея состоит в том, чтобы написать шаблон пода в исходном коде пайплайна. Существует множество примеров как для декларативных, так и для скриптовых пайплайнов. По моему наблюдению, по мере обновления версий плагина он предпринимает шаги, чтобы предпочесть декларативный метод для определения агента, а не скриптовый. Я приветствую это изменение, так как очень удобно использовать тот же способ YAML для конвейера Jenkins и других задач Kubernetes. При попытке использовать этот подход я столкнулся с двумя основными проблемами. Ниже приведен пример пайплайна, скопированный из одного из примеров плагина. В этом простом примере чать для определения шаблона пода составляет 20 строк кода, а сборка всего 5 строк. По моему реальному опыту, YAML файл может легко превышать 50 строк. В пайплайне это будет долго отрабатываться. Если у вас есть 100 проектов, сложно изменить 100 проектов для шаблона пода YAML в коде пайплайне. Разные организации имеют разные модели развития. Нет единого ответа, кто будет отвечать за пайплайн проекта(DevOps, System Administrator, Jenkins manager, Build Engineer), особенно за часть шаблона пода в коде конвейера. Если это индивидуальная проектная команда, невозможно вносить изменения одновременно. Например, когда необходимо обновить образ докера для исправления безопасности, то это обновление должно быть выполнено одновременно для всех проектов. Если это выделенная команда, то сложно будет себе представить усилия по эффективной модификации 100 проектов. Когда в коде конвейера Jenkins есть общие вещи, естественно переместить их в общую библиотеку Jenkins. Когда я искал решение для использования библиотеки для подготовки агента Kubernetes, меня вдохновил проект salemove/pipeline-lib. Это общая библиотека Jenkins, которая предоставляет метод inPod. inPod — это тонкая оболочка вокруг podTemplate, обеспечивающая гибкость для объединения предопределенных конфигураций с аргументами. Используя inPod в качестве основы, некоторые другие оболочки определяют различные аргументы, например inDockerAgent, inRubyBuildAgent, для предоставления различных типов агентов. Например, приведенный ниже фрагмент кода предоставляет агент с  образом докера «node: 9-alpine». Основная идея состоит в том, чтобы собрать параметры из слоев-оболочек и объединить их для шаблона podTemplate, который будет использоваться плагином Kubernetes. Здесь реализована ключевая функция addWithoutDuplicates, позволяющая объединить список, отдавая приоритет аргументам по умолчанию. Идея хорошая, и когда я захотел импортировать ее для собственного использования, я обнаружил, что она не подходит для моего случая. Библиотека работает только для скриптовых конвейеров. В моей работе есть как декларативные конвейеры, так и скриптовые конвейеры. Мне нужно одно решение для обоих. Существует множество конфигураций для podTemplate, но это не все, что поддерживает Kubernetes. На самом деле существует конфигурация «yaml» для предоставления raw yaml. Raw yaml будет объединен с другими конфигурациями. Библиотека ориентирована на предоставление агенту различных образов/томов докеров. Это хорошо, но мне нужен более обширный, который поддерживаются только в «raw yaml». С учетом этих соображений я хотел бы изучить новое решение с такими требованиями: На основе декларативного метода, полный формат YAML. Работает как для скриптовых конвейеров, так и для декларативных конвейеров. Гибкость предоставления различных типов агентов. Легко использовать в конвейерном коде(коде пайплайна) и скрывать детали в библиотеке. Простота разработки библиотеки для поддержки растущих требований к типам агентов. После некоторого исследования я считаю, что моя идея осуществима, глядя на эти 2 примера из плагина Kubernetes: dind.groovy и declarative.groovy. Общее между этими двумя примерами, как показано ниже: Цель состоит в том, чтобы сгенерировать Map с желаемым содержимым yaml, которое определяет спецификацию пода для агента. Подход состоит в том, чтобы определить небольшие фрагменты частичного yaml, каждый для конкретного требования к агенту, и объединить их вместе, чтобы составить окончательную спецификацию формата yaml для пода. Например, агенту может потребоваться выполнить сборку maven или gradle, файл ресурсов с именем «maven», чтобы включить контейнер с образом докера «maven», другой файл «gradle», чтобы включить образ докера «gradle». Агенту может потребоваться меньше или больше вычислительной мощности, сопоставление с файлами ресурсов «small»/«large»/«fast» для запроса некоторых ресурсов CPU/memory. Агенту может потребоваться «привилегированный» под в сборке, «привилегированный» файл ресурсов для определения «securityContext» и, в моем случае, вместе с «podAntiAffinity», чтобы убедиться, что только один привилегированный под на одном хост-компьютере. Чтобы сложить их вместе, агент определяется в формате «A+B+C», например, «maven+small+privileged», и библиотека вернет объединенный yaml. Пример кода для «small+pg»: Итак, у нас есть проект Gradle, который имеет следующую структуру: Прогоним тесты: Насколько я понимаю, не существует универсального метода слияния Yaml. Например, основной структурой данных Yaml является map и list. В спецификации Kubernetes List иногда чувствителен к последовательности, иногда нечувствителен. В плагине Kubernetes есть параметр yamlMergeStrategy: merge() или override(), который решает проблему с объединением Yaml в наследовании. В своей реализации я повторно использовал код из OndraZizka/yaml-merge и модифицировал его до уровня «просто работает», чтобы соответствовать моим сценариям. Он разрешает вариант использования, как показано ниже: Осталось добавить NodeSelector И последнее — параметр jnplimage Не так уж и сложно :) В заключение скажу, что сегодня пройдет открытый урок по устройству kubernetes. Изучим, из каких основных и вспомогательных частей состоит кластер-kubernetes, и поймем, чем отличаются компоненты и как взаимодействуют друг с другом. Если актуально, присоединяйтесь по ссылке.  Редактор