В этом уроке разбираем три инструмента, которые решают ровно эти боли в declarative pipeline: parallel - чтобы ускорять, lock - чтобы сериализовать доступ к общему ресурсу, milestone - чтобы старая сборка никогда не обгоняла новую. Плюс опции на уровне всего job, ограничивающие число одновременных прогонов. Материал основан на главе 3 книги Ластера, но синтаксис и практики приведены к реальности 2026 года.
parallel в Jenkins pipeline: запускаем ветки одновременно
Самый понятный способ ускорить конвейер - выполнять независимые задачи параллельно, а не цепочкой. В declarative pipeline для этого есть директива parallel внутри stage. Каждая вложенная ветка - это самостоятельный stage со своими шагами, и Jenkins разводит их по разным агентам (или исполнителям) одновременно.
Код: Выделить всё
pipeline {
agent none
stages {
stage('Tests') {
parallel {
stage('Unit') {
agent { label 'linux' }
steps {
sh './gradlew test'
}
}
stage('Lint') {
agent { label 'linux' }
steps {
sh './gradlew checkstyleMain'
}
}
stage('Integration') {
agent { label 'docker' }
steps {
sh './gradlew integrationTest'
}
}
}
}
}
}

Jenkins lock: один деплой на стенд за раз
Параллелизм ускоряет, но порождает конфликты за общие ресурсы: один staging-стенд, одна тестовая БД, один порт, одна железка в лабе. Решение - шаг lock из плагина Lockable Resources. Он работает как мьютекс: пока одна сборка держит ресурс, остальные ждут в очереди, а не лезут параллельно.
Ресурсы заводятся двумя способами. Первый - объявить их глобально (Manage Jenkins, дальше конфигурация плагина), задав имя и метки (labels). Второй - сослаться на несуществующее имя прямо в pipeline, тогда создаётся эфемерный ресурс, живущий только пока его кто-то держит. Для именованных стендов лучше первый путь - так их видно в UI и понятно, кто чем занят.
Код: Выделить всё
pipeline {
agent any
stages {
stage('Build') {
steps { sh 'make build' }
}
stage('Deploy to staging') {
steps {
lock(resource: 'staging-env', reason: "deploy ${env.BUILD_TAG}") {
sh './deploy.sh staging'
sh './smoke-test.sh staging'
}
}
}
}
}
Код: Выделить всё
lock(label: 'staging-pool', quantity: 1, variable: 'STAND') {
sh "./deploy.sh ${env.STAND}"
}
milestone и concurrent builds: защита от гонки деплоев
Lock сериализует доступ, но не решает проблему обгона. Вернёмся к сценарию "старая сборка деплоит поверх новой". Здесь нужен шаг milestone. Идея простая: milestone - это пронумерованный рубеж в конвейере. Правило железное - когда какая-то сборка проходит milestone с номером N, Jenkins автоматически отменяет (abort) все более ранние сборки этого job, которые ещё не дошли до N. То есть старый прогон физически не сможет добраться до деплоя, если его уже обогнал свежий.
Код: Выделить всё
pipeline {
agent any
stages {
stage('Build') {
steps {
milestone(ordinal: 1, label: 'built')
sh 'make build'
}
}
stage('Deploy prod') {
steps {
milestone(ordinal: 2, label: 'before-deploy')
lock('prod-env') {
sh './deploy.sh prod'
}
}
}
}
}
Третий инструмент - ограничение числа одновременных прогонов всего job. Если конкурентность тебе вообще не нужна, ставь в options:
Код: Выделить всё
options {
disableConcurrentBuilds()
}
Типичные грабли
- lock без milestone. Lock честно сериализует деплои, но в очереди может стоять устаревшая сборка, и она своё отдеплоит, просто позже. Lock без milestone не спасает от обгона - нужны оба.
- milestone в неправильном порядке. Номера ordinal должны строго возрастать по ходу конвейера. Если поставить milestone(ordinal: 2) раньше, чем ordinal: 1, поведение станет нелогичным. Проще не указывать ordinal вручную - тогда Jenkins нумерует рубежи автоматически в порядке появления.
- Эфемерный ресурс вместо именованного. Опечатка в имени ресурса в lock не вызовет ошибку - создастся новый эфемерный ресурс, и сериализация просто не сработает. Для критичных стендов объявляй ресурсы глобально и проверяй, что имена совпадают.
- agent any при parallel. Если на верхнем уровне стоит agent any, весь job занимает один исполнитель, и ветки parallel могут не разъехаться по узлам. Для настоящей параллельности используй agent none сверху и назначай agent внутри каждой ветки.
- disableConcurrentBuilds не отменяет уже идущую. По умолчанию он ставит в очередь, а не убивает. Если ждёшь, что старая сборка прервётся, явно добавляй abortPrevious: true.
Чтобы видеть Jenkins в контексте 2026 года: в GitLab CI аналог lock - это resource_group (сериализация job в рамках окружения) плюс interruptible: true для отмены устаревших пайплайнов. В GitHub Actions роль milestone и disableConcurrentBuilds играет блок concurrency с group и cancel-in-progress: true. Идея везде одна и та же - не пускать два деплоя в один контур и гасить обогнавшие друг друга прогоны. Разница в том, что у Jenkins это набор плагинов (Lockable Resources, Throttle) с очень гибкими пулами по меткам и переменными, тогда как в облачных системах механизм встроен, но проще. На современном Jenkins LTS (Java 17 в начале года, переход на Java 21 как базовый в апрельской ветке 2026) весь этот арсенал - часть стандартного declarative pipeline. Blue Ocean как UI давно заброшен и держится лишь как наследие, так что строй визуализацию на штатном Pipeline-графе и Stage View, а логику безопасности - на lock и milestone.
Мини-лаба
- Собери pipeline с тремя параллельными ветками тестов (parallel) и добавь failFast true. Завали одну ветку нарочно и убедись, что остальные прибиваются.
- Заведи глобальный lockable resource staging-env. Оберни стадию деплоя в lock('staging-env'). Запусти две сборки подряд и посмотри в UI плагина, как вторая ждёт.
- Добавь milestone перед lock на стадии деплоя. Запусти три сборки почти одновременно (например, тремя быстрыми пушами) и убедись, что до деплоя дойдёт только самая свежая, а ранние отменятся.
- Поменяй options на disableConcurrentBuilds(abortPrevious: true) и сравни поведение с обычным disableConcurrentBuilds().
- Чем отличается роль lock от роли milestone при защите деплоя, и почему для безопасной выкатки нужны оба?
- Что произойдёт, если в lock указать имя ресурса с опечаткой, не объявив его глобально?
- В чём разница между disableConcurrentBuilds() и disableConcurrentBuilds(abortPrevious: true)?
- Почему для настоящего параллелизма веток parallel лучше ставить agent none на верхнем уровне, а agent назначать внутри веток?
parallel ускоряет конвейер, разводя независимые задачи по агентам. lock сериализует доступ к общему ресурсу - один деплой на стенд за раз. milestone не даёт старой сборке обогнать новую и отменяет устаревшие прогоны на подходе к деплою. А disableConcurrentBuilds регулирует, сколько копий job вообще крутится одновременно. Связка milestone + lock на стадии выката - это и есть рабочий стандарт безопасного jenkins pipeline в 2026 году, который закрывает гонку деплоев и руками, и автоматически.