Jenkins жив и продолжает работать — и это нормально
Давайте назовём вещи своими именами: Jenkins по-прежнему работает в production во множестве компаний.
Несмотря на появление GitHub Actions, GitLab CI, CircleCI и других современных платформ, Jenkins остаётся основой сборочной инфраструктуры для бесчисленного числа организаций. И, честно говоря, в этом нет ничего страшного.
Миграция с Jenkins не всегда оправдана:
-
Сотни существующих пайплайнов, которые просто работают
-
Кастомные плагины, глубоко встроенные в рабочие процессы
-
Команды с многолетним опытом работы с Jenkins
-
Требования к соответствию нормативам, при которых предпочтительны решения на собственной инфраструктуре
Правильный вопрос — не «Стоит ли нам оставить Jenkins?», а «Как сделать Jenkins лучше?»
Проблемы классического Jenkins
Прежде чем перейти к решению, честно разберём, что именно сломано в традиционных установках Jenkins.
Проблема 1: отслеживать затраты практически невозможно
Когда у вас статические агенты Jenkins (виртуальные машины или физические серверы, работающие круглосуточно), даже простые вопросы превращаются в задачу:
«Сколько стоит сборка проекта X?» «Какая команда потребляет больше всего ресурсов CI/CD?» «Почему в этом месяце резко вырос счёт за инфраструктуру?»
При статических агентах ресурсы выделены заранее. Независимо от того, запускаете вы 100 сборок или ни одной, вы платите за этот сервер. Нет никакой детальной привязки затрат к конкретному проекту, команде или сборке.
Проблема 2: кошмар общих агентов
Статические агенты, как правило, используются сразу несколькими проектами. Это порождает целый ряд проблем.
Ад зависимостей: один проект устанавливает Node.js 18, другому нужен Node.js 20. Одна команда настраивает глобальные npm-пакеты — и сборка другой команды непостижимым образом ломается. Знакомо?
Проблемы безопасности: при совместном использовании агентов чувствительные учётные данные или артефакты одного проекта могут оказаться доступны другому. Инструменты, установленные одной командой, могут быть использованы другой — намеренно или нет.
Игра «кто сломал агент?»: кто-то установил глобальный пакет, кто-то поменял системные настройки — и теперь ничего не работает, а все показывают пальцем друг на друга.
Проблема 3: борьба за ресурсы в самый неподходящий момент
Представьте: день релиза. Команда готова выпускать. Но очередь Jenkins стоит, потому что другая команда прогоняет сотни интеграционных тестов. Ваш критически важный релиз застрял позади чьей-то экспериментальной ветки.
При статических агентах вы соревнуетесь за фиксированный пул ресурсов. Когда всем одновременно нужно выпускать релиз, кто-то неизбежно ждёт.
Проблема 4: налог на выделенные агенты
Альтернатива общим агентам — дать каждому проекту собственный выделенный агент. Но тогда вы платите за:
-
Простаивающие серверы в ожидании сборок, которые могут не прийти
-
Множество команд, каждая из которых поддерживает свою конфигурацию агента
-
Бесконтрольное разрастание инфраструктуры, которым невозможно управлять
Решение: Jenkins на Kubernetes
А что если вместо статических агентов Jenkins умел бы:
-
Поднимать свежие изолированные окружения для сборки по запросу
-
Масштабироваться до нуля при отсутствии активности (никаких затрат на простой)
-
Отслеживать потребление ресурсов в разрезе сборки, проекта и пространства имён (namespace)
-
Гарантировать чистое окружение при каждом запуске
Именно это и обеспечивают агенты Jenkins на базе Kubernetes.
Как это работает
┌─────────────────────────────────────────────────────────────┐ │ Kubernetes Cluster │ │ │ │ ┌──────────────────┐ ┌──────────────────────────────┐ │ │ │ Jenkins Master │ │ Dynamic Build Pods │ │ │ │ │◄──│ (Created per build) │ │ │ │ - UI (8080) │ │ │ │ │ │ - JNLP (50000) │ │ Pod 1: maven:3.9 container │ │ │ │ │ │ Pod 2: node:20 container │ │ │ │ │ │ Pod 3: gradle:8 container │ │ │ └──────────────────┘ │ ... │ │ │ │ (Deleted after build) │ │ │ └──────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘
Когда запускается сборка:
-
Jenkins даёт Kubernetes команду создать новый Pod
-
Pod запускается с точно теми образами контейнеров, которые нужны для сборки
-
Пайплайн выполняется в полностью изолированном окружении
-
Когда сборка завершается, Pod уничтожается
Больше никаких «работает на моём агенте», конфликтов зависимостей и борьбы за ресурсы.
Настройка Jenkins на Kubernetes
Разберём полную процедуру настройки. Все необходимые манифесты собраны в репозитории на GitHub.
Предварительные условия
-
Кластер Kubernetes (Docker Desktop, minikube, kind или облачный провайдер)
-
Настроенный
kubectl -
Базовые знания Kubernetes
Шаг 1: создаём пространство имён
Сначала изолируем Jenkins в отдельном пространстве имён:
# 00-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: jenkins
labels:
name: jenkins
purpose: ci-cd
kubectl apply -f k8s/00-namespace.yaml
Шаг 2: настраиваем RBAC
Jenkins нужны права на создание подов для динамических агентов:
# 01-rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: jenkins
namespace: jenkins
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: jenkins
namespace: jenkins
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["create", "delete", "get", "list", "watch", "patch"]
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create", "delete", "get", "list", "watch", "patch"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: jenkins
namespace: jenkins
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: jenkins
subjects:
- kind: ServiceAccount
name: jenkins
namespace: jenkins
Шаг 3: разворачиваем Jenkins Master
# 03-jenkins-master.yaml (фрагмент)
apiVersion: apps/v1
kind: Deployment
metadata:
name: jenkins
namespace: jenkins
spec:
replicas: 1
template:
spec:
serviceAccountName: jenkins
containers:
- name: jenkins
image: jenkins/jenkins:lts-jdk21
ports:
- containerPort: 8080 # UI
- containerPort: 50000 # JNLP agents
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: 2000m
memory: 2Gi
Шаг 4: устанавливаем плагин Kubernetes
После запуска Jenkins установите плагин Kubernetes:
-
Перейдите в Manage Jenkins → Plugins → Available plugins
-
Найдите «Kubernetes»
-
Установите и перезапустите Jenkins
Шаг 5: настраиваем Kubernetes Cloud
Перейдите в Manage Jenkins → Clouds → New cloud → Kubernetes и заполните параметры:
Name: kubernetes
Kubernetes URL: https://kubernetes.default.svc
Namespace: jenkins
Jenkins URL: http://jenkins:8080
Jenkins tunnel: jenkins:50000
Шаг 6: разворачиваем статический агент с постоянным рабочим пространством
Динамические поды обрабатывают большинство сборок, но статический агент обеспечивает сохранность рабочего пространства для просмотра артефактов и отладки.
1. Применяем манифест агента
# 04-jenkins-agent.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: jenkins-agent
namespace: jenkins
spec:
replicas: 1
selector:
matchLabels:
app: jenkins
component: agent
template:
metadata:
labels:
app: jenkins
component: agent
spec:
containers:
- name: jnlp
image: jenkins/inbound-agent:latest-jdk21
env:
- name: JENKINS_URL
value: "http://jenkins:8080"
- name: JENKINS_AGENT_NAME
value: "workspace-agent"
- name: JENKINS_SECRET
value: "REPLACE_WITH_AGENT_SECRET" # Обновите это значение!
- name: JENKINS_AGENT_WORKDIR
value: "/home/jenkins/agent"
volumeMounts:
- name: workspace
mountPath: /home/jenkins/agent
volumes:
- name: workspace
persistentVolumeClaim:
claimName: workspace
2. Создаём узел агента в интерфейсе Jenkins
Перед развёртыванием агента нужно создать его конфигурацию в Jenkins:
-
Перейдите в Manage Jenkins → Nodes → New Node
-
Введите имя узла:
workspace-agent -
Выберите «Permanent Agent» и нажмите Create
-
Настройте параметры:
Remote root directory: /home/jenkins/agent
Labels: workspace-agent
Usage: Only build jobs with label expressions matching this node
Launch method: Launch agent by connecting it to the controller
-
Нажмите Save
3. Получаем секрет агента
После сохранения Jenkins покажет данные подключения агента:
-
Нажмите на только что созданный узел
workspace-agent -
Найдите секрет в команде запуска агента — он выглядит примерно так:
-secret abc123def456… -
Скопируйте это значение
4. Обновляем манифест и разворачиваем агента
Подставьте полученный секрет в значение JENKINS_SECRET в манифесте и примените его:
# Отредактируйте секрет в 04-jenkins-agent.yaml, затем:
kubectl apply -f k8s/04-jenkins-agent.yaml
# Проверяем подключение агента
kubectl -n jenkins logs -l component=agent -f
В логах должна появиться строка:
INFO: Connected
5. Проверяем в интерфейсе Jenkins
Перейдите в Manage Jenkins → Nodes и убедитесь, что workspace-agent отображается онлайн с зелёной галочкой.
Современные Jenkinsfile-ы
Вот где начинается настоящая магия. Jenkinsfile точно описывает, какие контейнеры нужны вашей сборке.
Базовый пример: сборка Maven
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: maven
image: maven:3.9-eclipse-temurin-17
command: ["sleep", "infinity"]
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "1000m"
'''
defaultContainer 'maven'
}
}
// ... stages
}
Что здесь происходит:
-
Jenkins создаёт Pod с контейнером Maven
-
Команда
sleep infinityудерживает контейнер в рабочем состоянии на протяжении всей сборки -
Команды сборки выполняются внутри этого контейнера
-
После завершения пайплайна Pod удаляется
Продвинутый пример: параллельная сборка с несколькими контейнерами
Нужно одновременно запустить сборку Node.js и Maven? Используйте несколько контейнеров в одном Pod:
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: node
image: node:20.17.0
command: ["sleep", "infinity"]
- name: maven
image: maven:3.9.9-eclipse-temurin-17
command: ["sleep", "infinity"]
'''
}
}
stages {
stage('Build') {
steps {
sh 'mvn clean compile -B'
}
}
stage('Test') {
steps {
sh 'mvn test -B'
}
}
stage('Package') {
steps {
sh 'mvn package -DskipTests -B'
}
}
}
}
Ключевые преимущества:
-
Точный контроль версий:
node:20.17.0иmaven:3.9.9— никаких сюрпризов -
Настоящий параллелизм: обе сборки выполняются одновременно в одном Pod
-
Изоляция: у каждого контейнера своя файловая система и среда выполнения
Сохранение рабочего пространства
Динамические поды эфемерны — они исчезают после сборки. Но иногда нужно сохранять рабочее пространство:
-
Просмотр артефактов сборки в интерфейсе Jenkins
-
Отладка упавших сборок
-
Передача артефактов между пайплайнами
Решение: статический агент для доступа к рабочему пространству
Используя workspace-agent, развёрнутый на шаге 6, можно хранить артефакты сборки на постоянном хранилище. Вот как подключить его в Jenkinsfile:
pipeline {
agent {
kubernetes {
// ... динамический под для сборки
}
}
stages {
stage('Build') {
steps {
sh 'mvn clean package'
}
}
stage('Archive to Workspace') {
agent {
label 'workspace-agent'
}
options {
skipDefaultCheckout()
}
steps {
// Копируем артефакты в постоянное рабочее пространство
sh 'ls -al'
// Выполняется на статическом агенте с постоянным хранилищем
}
}
}
}
Метка workspace-agent направляет задание на статический агент с PersistentVolumeClaim (постоянным томом), давая лучшее из двух миров:
-
Динамические поды для сборки — изолированно, масштабируемо, экономично
-
Статический агент для хранения — просмотр артефактов, отладка
Работа с приватными репозиториями через settings.xml
Для Maven-проектов, обращающихся к приватным репозиториям (например, Nexus или Artifactory), нужен файл settings.xml с учётными данными.
Лучшая практика: секретные файлы на уровне задания
-
Группируйте задания по директориям: объединяйте связанные задания в папки Jenkins
-
Используйте плагин Secret File: добавьте
settings.xmlкак секретный файл, ограниченный этой папкой -
Монтируйте в Pod: ссылайтесь на секрет из Jenkinsfile
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: maven
image: maven:3.9-eclipse-temurin-17
command: ["sleep", "infinity"]
volumeMounts:
- name: maven-settings
mountPath: /root/.m2/settings.xml
subPath: settings.xml
volumes:
- name: maven-settings
secret:
secretName: maven-settings
'''
defaultContainer 'maven'
}
}
stages {
stage('Build with Private Deps') {
steps {
sh 'mvn clean install' // Использует смонтированный settings.xml
}
}
}
}
Почему такой подход?
-
Учётные данные хранятся в секретах Kubernetes, а не в коде
-
У каждого проекта или папки могут быть собственные настройки
-
Не нужно управлять глобальной конфигурацией агентов
Альтернатива: собственные образы агентов с кешем .m2
Для команд с большими деревьями зависимостей стоит рассмотреть создание кастомных образов агентов с предзаполненным кешем .m2:
FROM maven:3.9-eclipse-temurin-17
COPY settings.xml /root/.m2/settings.xml
RUN mvn dependency:go-offline -f /path/to/pom.xml # Предварительно скачиваем зависимости
Важно: не делитесь кешем .m2 между проектами — это нивелирует преимущества изоляции и может привести к конфликтам версий.
Распределение затрат с помощью пространств имён
Вот убийственная функция для отслеживания затрат: запускайте сборки каждой команды в отдельном пространстве имён Kubernetes.
Стратегия: отдельное пространство имён для каждой команды
// team-alpha-app/Jenkinsfile
pipeline {
agent {
kubernetes {
cloud 'kubernetes'
namespace 'team-alpha-builds' // Пространство имён команды
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: maven
image: maven:3.9-eclipse-temurin-17
command: ["sleep", "infinity"]
'''
}
}
// ... stages
}
// team-beta-app/Jenkinsfile
pipeline {
agent {
kubernetes {
cloud 'kubernetes'
namespace 'team-beta-builds' // Другое пространство имён
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: node
image: node:20-alpine
command: ["sleep", "infinity"]
'''
}
}
// ... stages
}
Зачем использовать пространства имён для учёта затрат?
-
Разбивка затрат у облачных провайдеров: большинство облачных провайдеров (AWS, GCP, Azure) умеют отчитываться о затратах в разрезе пространства имён Kubernetes
-
Квоты на ресурсы: задайте лимиты CPU и памяти для каждого пространства имён, чтобы одна команда не захватывала все ресурсы
-
Chargeback/showback: формируйте точные счета для каждой команды или проекта
-
Сетевые политики: изолируйте сборочный трафик между командами в целях безопасности
# Квота ресурсов для пространства имён команды
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-alpha-quota
namespace: team-alpha-builds
spec:
hard:
requests.cpu: "10"
requests.memory: 20Gi
limits.cpu: "20"
limits.memory: 40Gi
pods: "50"
Заключение
Jenkins никуда не денется. Вместо того чтобы бороться с ним, — модернизируйте его.
Перенеся агентов Jenkins на Kubernetes, вы получаете:
-
Прозрачность затрат: точно знаете, во что обходится каждый проект
-
Полную изоляцию: никаких конфликтов зависимостей
-
Неограниченную масштабируемость: тысячи параллельных сборок
-
Чистые окружения: каждая сборка стартует с чистого листа
-
Справедливое распределение ресурсов: конец очередям в день релиза
Компании, которые продолжают использовать Jenkins, не застряли в прошлом — они просто прагматичны. А с Kubernetes они получают современную инфраструктуру, не выбрасывая годами наработанные пайплайны.
Ваш «устаревший» Jenkins не обязан ощущаться устаревшим.