м. Київ, вул. Кирилівська 102

2 жовтня 2026 р.

Jenkins Pipeline від початку до продакшену

Jenkins Pipeline від початку до продакшену

Jenkins Pipeline перетворює набір ручних команд на контрольований процес, який зберігається поруч із кодом. Після кожного commit команда отримує однакову послідовність дій: checkout, перевірку залежностей, тести, складання артефакту, публікацію образу, розгортання на staging і керований перехід у production. Головна перевага тут не в самій автоматизації, а у відтворюваності: видно, який commit пройшов перевірки, який image digest розгорнуто та на якому кроці зупинився невдалий release.

У цій інструкції побудуємо Declarative Pipeline для застосунку в Docker. Jenkins збиратиме образ один раз, позначатиме його незмінним тегом на основі Git commit, перевірятиме staging, вимагатиме окремого підтвердження перед production і залишатиме зрозумілий шлях для rollback. Такий pipeline можна адаптувати до Docker Compose, Kubernetes або власного deployment API. Якщо потрібна комплексна реалізація процесу, ITheal надає DevOps-послуги, а проєктування stages, credentials і release policy входить у налаштування CI/CD.

Архітектура Jenkins і підготовка репозиторію

Не виконуйте важкі build jobs на Jenkins controller. Controller має керувати чергою, конфігурацією та станом Pipeline, а команди складання повинні працювати на окремих agents із потрібними labels. Для нашого прикладу agent із label `docker` має Git, Docker CLI та доступ до Docker daemon. Доступ до `/var/run/docker.sock` фактично дає високі привілеї на host, тому такий agent потрібно ізолювати, не використовувати для недовірених pull requests і регулярно перевстановлювати з відомого образу.

Pipeline зберігаємо у файлі `Jenkinsfile` у корені репозиторію. Поруч потрібні Dockerfile, lock-файл пакетного менеджера, тести та deployment scripts. Орієнтовна структура:

text
.
├── Jenkinsfile
├── Dockerfile
├── package.json
├── package-lock.json
├── src/
├── test/
└── deploy/
    ├── deploy.sh
    ├── smoke-test.sh
    └── rollback.sh

Deployment scripts повинні приймати конкретний image reference, перевіряти обов’язкові параметри та завершуватися з ненульовим exit code при помилці. Не ховайте в них паролі, приватні ключі чи адреси, які відрізняються між середовищами. Секрети створіть у Jenkins Credentials, а в `Jenkinsfile` використовуйте лише їхні IDs. Мінімальний набір для прикладу:

- `registry-credentials` — username/password або robot account для container registry; - `staging-ssh` — окремий SSH key з доступом лише до staging deployment command; - `production-ssh` — інший SSH key із мінімальними production-правами.

SSH host keys заздалегідь додайте до `known_hosts` на agent image або передавайте як керований secret file. Не вимикайте `StrictHostKeyChecking`: це прибирає перевірку сервера саме в момент, коли Pipeline передає йому release.

Створіть Multibranch Pipeline або Pipeline from SCM і вкажіть шлях до `Jenkinsfile`. Multibranch job автоматично знаходить branches та merge requests, але production stage все одно має бути обмежений умовою для основної гілки. Webhook від Git-сервера кращий за постійний polling: build починається після події, а не за випадковим розкладом.

Declarative Jenkinsfile: checkout, перевірки та тести

Перший варіант Pipeline має бути простим і повністю читабельним. Ми вимикаємо автоматичний checkout, щоб виконати його явно, забороняємо паралельні releases одного job, обмежуємо загальний час і зберігаємо лише потрібну історію. Значення `BUILD_NUMBER` не використовуємо як єдиний ідентифікатор образу: воно належить Jenkins job, а не Git. Короткий SHA однозначно пов’язує артефакт із кодом.

groovy
pipeline {
  agent { label 'docker' }

  options {
    skipDefaultCheckout(true)
    disableConcurrentBuilds(abortPrevious: true)
    timeout(time: 45, unit: 'MINUTES')
    buildDiscarder(logRotator(numToKeepStr: '30'))
    timestamps()
  }

  environment {
    REGISTRY = 'registry.example.com'
    IMAGE_REPOSITORY = 'platform/api'
  }

  stages {
    stage('Checkout') {
      steps {
        checkout scm
        script {
          env.GIT_SHA = sh(
            script: 'git rev-parse --short=12 HEAD',
            returnStdout: true
          ).trim()
          env.IMAGE = "${env.REGISTRY}/${env.IMAGE_REPOSITORY}:${env.GIT_SHA}"
        }
      }
    }

    stage('Install and test') {
      steps {
        sh '''
          set -eu
          npm ci
          npm run lint
          npm test -- --ci
        '''
      }
      post {
        always {
          junit allowEmptyResults: true, testResults: 'reports/junit/*.xml'
        }
      }
    }

    stage('Build image') {
      steps {
        sh '''
          set -eu
          docker build --pull --tag "$IMAGE" .
          docker image inspect "$IMAGE" >/dev/null
        '''
      }
    }
  }

  post {
    always {
      cleanWs(deleteDirs: true, notFailBuild: true)
    }
  }
}

`npm ci` відтворює залежності з lock-файла та завершується помилкою, якщо manifest й lock розходяться. Для Maven, Gradle, Go або Python замініть лише команди всередині stage, залишивши загальний контракт: залежності фіксуються, lint і тести виконуються до складання release artifact. Якщо тести створюють JUnit XML, крок `junit` показує історію результатів у Jenkins, а не залишає її лише в console log.

Не використовуйте Groovy interpolation для секретів у `sh`. Groovy може підставити значення ще до запуску shell, після чого воно потрапить до process arguments або log. Блоки shell у прикладах мають одинарні потрійні лапки: змінні середовища розгортає shell безпосередньо на agent.

Публікація незмінного образу в registry

Наступний stage автентифікується лише на час push. Jenkins маскує відомі credentials у log, але маскування не захищає від навмисного витоку з недовіреного Pipeline. Тому release credentials не можна видавати jobs, які запускають довільний код із зовнішніх branches.

Додайте після `Build image`:

groovy
stage('Push image') {
  when {
    anyOf {
      branch 'main'
      buildingTag()
    }
  }
  steps {
    withCredentials([usernamePassword(
      credentialsId: 'registry-credentials',
      usernameVariable: 'REGISTRY_USER',
      passwordVariable: 'REGISTRY_PASSWORD'
    )]) {
      sh '''
        set +x
        printf '%s' "$REGISTRY_PASSWORD" |
          docker login "$REGISTRY" --username "$REGISTRY_USER" --password-stdin
        docker push "$IMAGE"
        docker logout "$REGISTRY"
      '''
    }
    script {
      env.IMAGE_DIGEST = sh(
        script: 'docker image inspect --format="{{index .RepoDigests 0}}" "$IMAGE"',
        returnStdout: true
      ).trim()
    }
    writeFile file: 'release.env', text: "IMAGE=${env.IMAGE}\nIMAGE_DIGEST=${env.IMAGE_DIGEST}\nGIT_SHA=${env.GIT_SHA}\n"
    archiveArtifacts artifacts: 'release.env', fingerprint: true
  }
}

Tag із commit SHA не потрібно перезаписувати. Якщо registry дозволяє immutable tags, увімкніть цю політику. Для deployment ще надійніше використовувати digest виду `registry.example.com/platform/api@sha256:...`: навіть якщо хтось змінить tag, orchestrator завантажить ті самі bytes, які перевірив Pipeline.

Не передавайте registry password у `docker login -p`: він може з’явитися у списку процесів. `--password-stdin` зменшує цей ризик, а `set +x` не дозволяє shell друкувати команди з розгорнутими значеннями. Після push виконуйте logout, особливо на довгоживучому agent. Кращий варіант для cloud registry — короткоживучий token або workload identity замість постійного пароля.

Від staging до production: gate, smoke test і rollback

Один і той самий image потрібно спочатку розгорнути на staging, а потім просувати в production без повторного build. Повторне складання після тестів створює інший набір bytes і руйнує доказ того, що production отримав перевірений артефакт.

groovy
stage('Deploy staging') {
  when { branch 'main' }
  steps {
    sshagent(credentials: ['staging-ssh']) {
      sh '''
        set -eu
        ssh -o BatchMode=yes -o StrictHostKeyChecking=yes [email protected] \
          /opt/app/bin/deploy.sh "$IMAGE_DIGEST"
        ./deploy/smoke-test.sh https://stage.example.com/health
      '''
    }
  }
}

stage('Approve production') {
  when { branch 'main' }
  steps {
    timeout(time: 30, unit: 'MINUTES') {
      input message: "Deploy ${IMAGE_DIGEST} to production?",
            ok: 'Deploy',
            submitter: 'release-managers'
    }
  }
}

stage('Deploy production') {
  when { branch 'main' }
  steps {
    sshagent(credentials: ['production-ssh']) {
      sh '''
        set -eu
        ssh -o BatchMode=yes -o StrictHostKeyChecking=yes [email protected] \
          /opt/app/bin/deploy.sh "$IMAGE_DIGEST"
        ./deploy/smoke-test.sh https://example.com/health
      '''
    }
  }
}

Група `release-managers` у прикладі умовна: у вашому Jenkins задайте реальних користувачів або зовнішній механізм approval. Сам `input` не компенсує слабкий RBAC. Job configuration, credentials, shared libraries і права на replay Pipeline повинні бути закриті від користувачів, які не мають права змінювати production.

`deploy.sh` на сервері має бути вузькою операцією: прийняти валідний image digest, отримати образ, виконати міграцію за погодженою процедурою, переключити сервіс і дочекатися healthcheck. SSH key не повинен відкривати довільний root shell. Його можна обмежити forced command у `authorized_keys` або замінити SSH на deployment API з аудитом.

Smoke test перевіряє не лише HTTP 200, а й критичну залежність: підключення до бази, доступність черги або читання мінімального ресурсу. Водночас health endpoint не має повертати секрети й внутрішні stack traces. Для поступового release використовуйте rolling, blue-green або canary strategy; Pipeline повинен чекати readiness і зупиняти rollout при деградації.

Rollback не означає повторний build старого commit. Зберігайте попередній успішний digest і передавайте його тому самому deployment script:

bash
/opt/app/bin/rollback.sh \
  'registry.example.com/platform/api@sha256:previous_digest'

Міграції бази проєктуйте backward-compatible: спочатку додавайте нові поля й підтримку обох схем, потім перемикайте застосунок, а видалення робіть окремим release. Інакше повернення старого контейнера не відновить сумісність із уже зміненою схемою. Pipeline, який будує release через GitLab Runner, має ті самі базові вимоги до immutable artifacts і credentials; відмінності механіки можна порівняти з інструкцією про побудову CI/CD через GitLab Runner.

Типові помилки Jenkins Pipeline

Перша помилка — зберігати Pipeline лише в Jenkins UI. Зміни тоді не проходять code review, а відновити процес після втрати controller складніше. `Jenkinsfile`, deployment scripts і конфігурація тестів мають версіонуватися разом із застосунком.

Друга — збирати образ окремо для staging і production. Навіть з одного commit залежності, base image або package repository можуть змінитися між builds. Збирайте один раз, публікуйте immutable tag і просувайте той самий digest.

Третя — додавати passwords, tokens та private keys у repository або глобальний `environment`. Видавайте credential лише конкретному stage, використовуйте різні identities для staging і production та не дозволяйте недовіреному коду виконуватися на agent із release secrets.

Четверта — вимикати SSH host verification або використовувати спільний root key. Це робить автоматизацію зручною ціною перевірки цільового сервера й принципу least privilege. Фіксуйте host keys, обмежуйте команди та регулярно змінюйте credentials.

П’ята — запускати production deployment із кожної гілки. Умови `when`, branch protection і окремий approval мають працювати разом. Перевірте також, хто може змінити job, shared library, credentials і список approvers.

Шоста — не ставити timeout і не чистити workspace. Завислий test або deployment займає executor, а старі файли можуть вплинути на наступний build. Додавайте timeout, retention policy, очищення workspace та скасування застарілого build після нового commit.

Сьома — вважати зелений shell step повною перевіркою release. Публікуйте JUnit reports, зберігайте metadata артефакту, виконуйте smoke test після deployment і передавайте статус у систему моніторингу. Jenkins показує результат процесу, але не замінює спостереження за latency, error rate і ресурсами після релізу.

Висновок

Надійний Jenkins Pipeline починається не з великого Groovy-файла, а з чіткого release contract. Код проходить однакові lint і tests, image будується один раз, отримує tag за Git SHA та публікується в registry. Staging і production використовують той самий digest, production захищений branch condition і явним approval, а rollback повертає попередній перевірений артефакт без повторного складання.

Controller слід відокремити від build agents, credentials видавати мінімально й лише на потрібному етапі, SSH host keys перевіряти, а deployment scripts обмежувати однією передбачуваною операцією. Додайте build retention, timeout, reports, healthcheck і аудит approval — тоді Pipeline буде не набором команд, а керованим шляхом від commit до production.

Починайте з короткого Jenkinsfile, який команда може прочитати повністю. Після стабілізації винесіть повторювані кроки в versioned shared library, додайте security scanning, підпис образів, canary release та автоматичну перевірку метрик. Ускладнюйте процес лише там, де новий контроль зменшує реальний ризик або скорочує час відновлення.

📞