Jenkins на Kubernetes: динамические агенты и учёт затрат

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

  1. Нажмите 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 не обязан ощущаться устаревшим.

© 2026 meganuke