UMGUM.COM 

Jenkins ( Установка и предварительная настройка сервера "Jenkins v2", с последующим подключением программных агентов "Jenkins Slaves" на серверах, предназначенных для автоматизированного развёртывания приложений. )

7 октября 2019  (обновлено 15 марта 2020)

OS: "Linux Debian 9/10", "Linux Ubuntu 16/18 LTS".
Apps: "Jenkins v.2", "Java", SSH.

Задача: установить и предварительно настроить сервер "Jenkins", с последующим подключением программных агентов "Jenkins Slaves" на серверах, предназначенных для автоматизированного развёртывания приложений, а также протестировать работу пары простейших CI/CD-сценариев.


Для справки (из "Википедии"): "Jenkins" - программная система с открытым исходным кодом на "Java", предназначенная для обеспечения процесса непрерывной интеграции и доставки программного обеспечения. Ответвлена в 2008 году от проекта "Hudson", принадлежащего компании Oracle, основным его автором - Косукэ Кавагути. Распространяется под лицензией MIT.

По моему, "Jenkins" коряв до невозможности, хотя формально на данный момент в нём реализован наиболее богатый набор поддерживаемых возможностей CI/CD, и это совершенно бесплатно (не считая многих часов, потраченных на то, чтобы вникнуть в зачастую нелепую логику функциональности, накрученной разномастными плагинами).

Так что, если у вас уже есть "Bamboo", "TeamCity", "GitLab" - оставайтесь с ними. Если хочется странного, то можно попробовать "Jenkins".


Установка и первичное конфигурирование"Jenkins".

Установка "Jenkins Server" по сути сводится к разворачиванию WAR (Web Application Resource) и наладке его запуска посредством java-интерпретатора. Это просто делается, но мы ускорим процесс и воспользуемся готовым дистрибутивом, адаптированным для "Linux Debian/Ubuntu".

Подключаем ATP-репозиторий от разработчиков:

# wget -q -O - https://pkg.jenkins.io/debian-stable/jenkins.io.key | sudo apt-key add -
# echo -e "# APT-repo "Jenkins" for "Debian/Ubuntu"\ndeb https://pkg.jenkins.io/debian-stable binary/" > /etc/apt/sources.list.d/jenkins.list

Инсталлируем последовательно "Java" и "Jenkins":

# apt-get update && apt-get install openjdk-11-jre && apt-get install jenkins

По умолчанию в "Linux Debian/Ubuntu" файлы данных "Jenkins" сохраняются в директории "/var/lib/jenkins".

Сразу после развёртывания из дистрибутива "Jenkins" доступен на всех сетевых интерфейсах через порт TCP:8080. При первом обращении к web-интерфейсу сервиса (например "http://jenkins.example.net:8080") автоматически запустится "мастер настройки". Всё просто. Вначале потребуется указать пароль разблокировки, который выводится инсталлятором в указанный текстовый файл. Следом определимся с набором устанавливаемых плагинов (в примере я привожу минимальный, но достаточно функциональный). Потом создание аккаунта для администратора сервиса и на этом всё:

Getting Started:
  Unlock Jenkins:
    # cat /var/lib/jenkins/secrets/initialAdminPassword
  Customize Jenkins:
    Select plugins to install:
      ....
      Build Features:
        Credentials Binding (default),
        Timestamper (default),
        Workspace Cleanup (default),
        ....
      Pipelines and Continuous Delivery:
        Pipeline (default),
        Pipeline: Stage View (default),
        ....
      Source Code Management:
        Git (default),
        ....
      Distributed Builds:
        SSH Slaves (default),
        Command Agent Launcher Plugin (default),
        ....
      User Management and Security:
        Role-based Authorization Strategy (manual),
        ....
      Notifications and Publishing:
        Email Extension (default),
        ....
  Create First Admin User:
    Username: admin
    ....

Разработчики "Jenkins" желают получать статистические сведения использования их приложений, но я сразу после запуска сервиса предпочитаю запрещать высылать какие-либо сведения о наших производственных делах наружу:

Jenkins -> Manage Jenkins -> Configure System:
  Usage Statistics (sending anonymous usage statistics): off

В общем, "Jenkins" уже готов к работе: можно создавать задачи, подключать репозитории, собирать приложения и развёртывать проекты - но производственный сервис на "Java" лучше спрятать за более подходящим для терминирования HTTP-запросов легковесным фронтальным web-сервером, заодно наладив шифрование соединений посредством SSL.

Прячем "Jenkins" за web-прокси.

В качестве фронтального web-сервера используем "Lighttpd", просто ради развлечения - не только же "Nginx" в такой роли выступать:

# apt-get install --no-install-recommends lighttpd

Подготовим место для SSL/TLS-сертификатов, сгенерируем ключ "Diffie-Hellman" и пару "самоподписанных" сертификатов:

# mkdir -p -m 500 /etc/ssl/lighttpd && cd /etc/ssl/lighttpd
# openssl dhparam -out ./dhparam-2048.pem 2048
# openssl req -new -x509 -keyout ./jenkins.example.net-ss.pem -out ./jenkins.example.net-ss.pem -days 365 -nodes
# openssl req -new -x509 -keyout ./www.jenkins.example.net-ss.pem -out ./www.jenkins.example.net-ss.pem -days 365 -nodes

В отдельном конфигурационном файле описываем поведение "Lighttpd" при обработке поступающих к сайту "Jenkins" запросов:

# vi /etc/lighttpd/conf-available/99-jenkins.example.net.conf

# Активируем модули проксирования и журналирования
server.modules += ( "mod_proxy", "mod_accesslog" )

# Захватываем запросы определённого FQDN
$HTTP["host"] =~ "(^|www\.)jenkins\.example\.net$" {

  # Отлавливаем нешифрованные запросы и перенаправляем на HTTPS
  $HTTP["scheme"] == "http" {
    $HTTP["host"] =~ ".*" {
      url.redirect = ( ".*" => "https://%0$0" )
    }
  }

  # Активируем прослушивание порта для HTTPS
  $SERVER["socket"] == ":443" {
    ssl.engine  = "enable"

    # Параметры SSL/TLS для сайта по умолчанию
    ssl.dh-file = "/etc/ssl/lighttpd/dhparam-2048.pem"
    ssl.pemfile = "/etc/ssl/lighttpd/jenkins.example.net-ss.pem"

    # Для префикса "www." указываем свой сертификат и перенаправляем на основной FQDN
    $HTTP["host"] =~ "^www\.(.*)" {
      ssl.pemfile = "/etc/ssl/lighttpd/www.jenkins.example.net-ss.pem"
      url.redirect = ( "^/(.*)" => "https://%1/$1" )
    }

    # Весь рабочий трафик проксируем в "Jenkins"
    proxy.server = ( "" => (
      ( "host" => "127.0.0.1", "port" => 8080 ) )
    )

    # Сервис важный, так что пишем журнал посещений
    accesslog.filename = "/var/log/lighttpd/jenkins.example.net.log"
  }
}

Активируем нашу конфигурацию, проверяем её корректность и применяем:

# ln -s /etc/lighttpd/conf-available/99-jenkins.example.net.conf /etc/lighttpd/conf-enabled/99-jenkins.example.net.conf
# lighttpd -t -f /etc/lighttpd/lighttpd.conf
# systemctl restart lighttpd
# systemctl status lighttpd

В конфигурационном файле запуска "Jenkins" задаём параметры, указывающие работать в стеке TCPv4, принимать запросы только на "локальной петле" и отключить широковещательные запросы поиска соседей:

# vi /etc/default/jenkins

JAVA_ARGS="... -Djava.net.preferIPv4Stack=true -Dhudson.udp=-1"
....
JENKINS_ARGS="... --httpListenAddress=127.0.0.1"

Перезапускаем "Jenkins":

# /etc/init.d/jenkins restart
# systemctl status jenkins

Проверяем, все ли необходимые сетевые порты прослушиваются:

# netstat -apn | grep -i listen | grep -i "^tcp" | grep -iE "lighttpd|java"

tcp ... 127.0.0.1:8080 0.0.0.0:* LISTEN .../java          
tcp ... 0.0.0.0:80     0.0.0.0:* LISTEN .../lighttpd
tcp ... 0.0.0.0:443    0.0.0.0:* LISTEN .../lighttpd

Если вынос за web-прокси делается после конфигурирования "Jenkins", то изменяем фронтальный FQDN:

Jenkins -> Manage Jenkins -> Configure System:
  Jenkins Location:
    Jenkins URL: https://jenkins.example.net/

Включение режима ролевого разграничения доступа.

В конфигурации по умолчанию сразу после развёртывания из дистрибутива "Jenkins" позволяет всем и всё. Для тестового стенда или команды их пары-тройки разработчиков это удобно, но на предприятии с десятками сотрудников, группами работающих над не связанными между собой программными продуктами, тестируемыми и публикуемыми на множестве серверов, доступ к ресурсам без ограничений неминуемо приведёт к проблемам - кто-нибудь случайно нажмёт не ту кнопку или, запутавшись, изменит не свою конфигурацию.

Задача разграничения доступа к ресурсам пользователям или группам таковых отчасти решается плагином "Role-based Authorization Strategy". Считая, что плагин мы установили в процессе развёртывания "Jenkins", активируем режим доступа, им предоставляемый:

Jenkins -> Manage Jenkins -> Configure Global Security:
  Enable security: on
  Access Control:
    Security Realm: Jenkins’ own user database
    Authorization: Role-Based Strategy
  Access Control for Builds:
    Strategy: Run as SYSTEM

Первым делом заведём глобальную роль, предоставляющую доступ на чтение корня конфигурации и представлений - без этой минимальной роли пользователь ничего не увидит вообще:

Jenkins -> Manage Jenkins -> Manage and Assign Roles -> Manage Roles -> Global roles -> Add:
  Role: everyone
    Overall: Read
    View: Read

Для корректной работы описываемого далее функционала мы должны принять за данность, что объекты конфигурации "Jenkins" будут именоваться по определённой схеме. Например:

Items:
  project0
    project0_site0.example.net
    project0_site1.example.net
Credentials:
  ....
Nodes:
  node0.example.net
  node1.example.net
  ....

Именуя объекты (задачи) таким образом, логично потом будет их группировать представлениями:

Jenkins -> New View -> List View:
  View name: project0
  Job Filters:
    Regular expression: (?i)project0_.*

Следуя вышеприведённой схеме именования достаточно просто наладить выделение разрешений доступа к иерархии объектов, с вычленением набора таковых из общей массы по регулярному выражению, применяемому к имени.

Заведём роль, дающую полный доступ к набору объектов (группе задач, проекту) - для администратора:

Jenkins -> Manage Jenkins -> Manage and Assign Roles -> Manage Roles -> Item roles -> Add:
  Role: project0_adm
  Patern: (?i)project0_.*
    All: *

Заведём роль, предоставляющую возможность просматривать и запускать уже настроенные процедуры - для разработчика:

Jenkins -> Manage Jenkins -> Manage and Assign Roles -> Manage Roles -> Item roles -> Add:
  Role: project0_dev
  Patern: (?i)project0_.*
    Job: Build, Cancel, Read
    Run: Delete, Replay, Update

Очевидно, что круги доступа для ролей можно очерчивать более детально, уточняя регулярное выражение.

Теперь создадим учётную запись для разработчика проекта:

Jenkins -> Manage Jenkins -> Manage Users -> Create User:
  Username: developer0
  ....

Наделим учётную запись разработчика соответствующими ему ролями:

Jenkins -> Manage Jenkins -> Manage and Assign Roles -> Assign Roles -> Global roles:
  developer0: everyone
Jenkins -> Manage Jenkins -> Manage and Assign Roles -> Assign Roles -> Item roles:
  developer0: project0_developer

О несанкционированном доступе к данным "Jenkins Server".

Не могу не отметить, что у "Jenkins" в борту практически незакрываемая дыра с неконтролируемым доступом к конфигурациям и отчасти данным проектов. Например, простейшая команда "ls /var/lib/jenkins" в блоке "Build" или "Pipeline" любой исполняемой на "Jenkins Server" задачи покажет листинг корневой директории сервиса.

Самый распространённый и рекомендуемый плагин "Role-Based Authorization Strategy" не закрывает всех возможных с несанкционированным доступом проблем. Дело в том, что этот плагин накладывает ограничения только на функциональность web-интерфейса, а сборка и развёртывание таковых осуществляется в контексте единого пользователя несущей операционной системы "jenkins", что позволяет с лёгкостью получить доступ к файлам других проектов "под капотом" web-интерфейса.

Опосредованно проблему бесконтрольного доступа к файлам конфигурации сервера можно было бы снять с повестки запретом запуска задач непосредственно на нём, вынеся все работы на "ноды", считая "Jenkins Server" лишь управляющим инструментом. Для этого вроде как предназначается плагин "Job Restrictions". И да, применение в конфигурации "ноды" ограничения вроде "Restrict jobs execution at node: Regular Expression: (?!).*" действительно приведёт к тому, что задания будут направляться на первую доступную "ноду" или оставаться в ожидании таковой, но на самом сервере "Jenkins" не отработают. Однако, действие плагина "Job Restrictions" не распространяется на задачи типа "Pipeline", которая вначале всегда стартует на "Jenkins Server" и только после этого по желанию может быть отправлена на произвольную "ноду" - а что за CI/CD без "pipeline"?

Кое-как задачу типа "Pipeline" можно загнать с помощью корявого функционала плагина "Node and Label parameter" на произвольную "ноду" сразу, без предварительного запуска на "Jenkins Server", но это делается в конфигурации самой задачи, что лишает действие смысла.

Кроме вышеприведённого рекомендуют ещё воспользоваться плагином "Authorize Project", который вроде как позволяет запускать задачи в контексте произвольного пользователя - но у меня это вульгарно не работало - чтобы я ни делал, задачи типа "Pipeline" стартовали или от имени пользователя "jenkins" или "anonymous".

Ещё больше возможностей разграничения доступа предоставляет плагин "Job and Node ownership", дополняющий и расширяющий функционал трёх других: "Role-Based Authorization Strategy", "Job Restrictions" и "Authorize Project" - но правда, накрученное месиво из плагинов и разбросанных в самых неожиданных местах параметров простейшую задачу глобальной блокировки доступа к данным сервиса так и не решает.

Печально, что в свете обозначенной проблемы "Jenkins" небезопасно применять для поддержания CI/CD проектов сразу нескольких команд. Можно позиционировать этот программный продукт как подсобный инструмент для решения конкретных задач одного проекта, где все в курсе всего и ничего друг от друга не скрывают. В конце концов ничто не мешает запускать на каждый проект по серверу "Jenkins" - это действительно несложно.

Подключение "Jenkins Slaves".

Сам сервер "Jenkins" полезен для CI-процедур, но нам требуется доставка приложений на площадки тестирования и публикации (CD-процедуры; Continuous Delivery & Deployment), для чего предназначается удобная функциональность "Jenkins Slaves" - запускаемые в целевых системах агенты исполнения заданий.

Прежде всего генерируем SSH-ключи, используемые впоследствии только в "Jenkins Server" при подключении к "Jenkins Slave" (для простоты сохраняем их в произвольной директории):

# mkdir -p -m 700 /usr/local/etc/jenkins/.ssh/node-one.example.net && cd /usr/local/etc/jenkins/.ssh/node-one.example.net && ssh-keygen -q -t rsa -b 2048 -f ./id_rsa -P "" -C "User \"jenkins\" for node \"node-one.example.net\"."

В "Jenkins v.2" для хранения частных переменных окружения, паролей, ключей и сертификатов сделали специальную централизованную подсистему "Credentials Provider". Прежде всего добавляем подраздел для хранения "ключей", доступных только при явном указании на связь с указанным именем области:

Jenkins -> Credentials -> System -> Add domain:
  Domain Name: node-one.example.net

Добавляем ssh-ключи в область домена подключаемой "ноды" ("закрытый ключ" достаём из файла "./id_rsa", полученного в примере выше):

Jenkins -> Credentials -> System -> node-one.example.net -> Add Credentials:
  Kind: SSH Username with private key
  Scope: System (Jenkins and nodes only)
  Description: User for node "node-one.example.net"
  Username: jenkins
  Private Key:
    Enter directly:
      Key: -----BEGIN RSA PRIVATE KEY-----
           MIIEpQIBAAKCAQEA3nh9A9eCwl03...
           ....

На стороне "Jenkins Slave" устанавливаем требуемую среду исполнения "Java" и создаём пользователя, в контексте которого будут исполняться задачи и обеспечиваем возможность подключения с его SSH-ключём:

root@node-one:/# apt-get update && apt-get install openjdk-11-jre
root@node-one:/# groupadd --force --system jenkins
root@node-one:/# useradd --system --shell /bin/bash --create-home --home-dir /var/lib/jenkins --gid jenkins jenkins

На стороне "Jenkins Slave" добавляем "открытый ключ" из пары созданных выше в перечень разрешённых к аутентификации для пользователя "jenkins":

root@node-one:/# mkdir -p -m 700 /var/lib/jenkins/.ssh && chown -R jenkins:jenkins /var/lib/jenkins/.ssh
root@node-one:/# vi /var/lib/jenkins/.ssh/authorized_keys

ssh-rsa AAA...cA5 User "jenkins" for node "node-one.example.net".

Теперь настроим подключение "Jenkins Server" к "Jenkins Slave":

Jenkins -> Manage Jenkins -> Manage Nodes -> Permanent Agent:
  Node name: node-one.example.net
  Remote root directory: /var/lib/jenkins
  Labels: shell node-one master
  Usage: Only build jobs with label expressions matching this node
  Launch method: Launch agent via SSH
    Host: node-one.example.net
    Credentials: jenkins (User for node "node-one.example.net")
    Host Key Verification Strategy: Manuality trusted key Verification Strategy
  Availability: Keep this agent online

В соответствии с выбранным методом дополнительной проверки подлинности соединения SSH-подключение к системе "Jenkins Slave" не состоится до тех пор, пока мы не подтвердим доверие к полученному от удалённого сетевого узла "fingerprint"-у:

Jenkins -> Manage Jenkins -> Manage Nodes -> node-one.example.net -> Trust SSH Host Key:
  Do you want to trust the SSH Host Key with fingerprint ... for future connections to this host? yes

Как вариант, можно было предварительно подключиться к "Jenkins Slave", используя известные нам ssh-ключи (например командой "sudo -u jenkins ssh -i /usr/local/etc/jenkins/.ssh/node-one.example.net/id_rsa jenkins@node-one.example.net", в результате чего на стороне "Jenkins Server" в файле "/var/lib/jenkins/.ssh/known_hosts" добавилась бы строка с типом используемого для шифрования алгоритмом и открытым ключём "ноды", что позволило бы использовать защиту от атак "man-in-the-middle" для соединения сервера с клиентом типа "Known hosts file Verification Strategy" - но это несколько менее удобно, так как требует действий в командной строке несущего сервера.

По звершению настройки подключения к "Jenkins Slave" велим запустить агента, после чего "Jenkins Server" соединиться с целевой "нодой" по SSH, загрузит туда посредством SFTP исполняемый файл агента "remoting.jar" и запустит его в режиме ожидания команд.

Выдержка из журнала успешного подключения:

....
... [SSH] Opening SSH connection to node-one.example.net:22.
... [SSH] SSH host key matches key seen previously for this host. Connection will be allowed.
... [SSH] Authentication successful.
... [SSH] The remote user's environment is:
BASH=/bin/bash
....
HOME=/var/lib/jenkins
....
USER=jenkins
....
... [SSH] java -version returned 11.0.5.
... [SSH] Starting sftp client.
... [SSH] Copying latest remoting.jar...
... [SSH] Copied 876,668 bytes.
... [SSH] Starting agent process: cd "/var/lib/jenkins" && java  -jar remoting.jar -workDir /var/lib/jenkins -jar-cache /var/lib/jenkins/remoting/jarCache
....
Agent successfully connected and online

Создание задачи типа "Freestyle", исполняемой на "ноде".

New Item (Job):
  Item name: project0_site0.example.net
  Freestyle project:
    General:
      Restrict where this project can be run:
        Label Expression: shell && node-one && master
    ....
    Source Code Management:
      Git:
        Repositories:
          ssh://git-user@git.example.net/~/repo-test.git
          Credentials: git-user
        Branches to build: */master
    ....
    Build Environment:
      Add timestamps to the Console Output: on
    ....
    Build:
      Execute shell:
        Command:
          echo "123" >> ${WORKSPACE}/123
          ls -l ${WORKSPACE}
          echo 'Show'
        ....
    Post-build Actions:
      ....

Создание задачи типа "Pipeline", исполняемой на "ноде".

В отличии от задачи типа "Freestyle", здесь мы всю автоматизацию и организацию очерёдности процедур сборки и развёртывания возлагаем на подсистему "pipeline", руководствующуюся описанием в специальном формате. В "Jenkins" возможны два варианта применения инструкций: их можно расписать прямо в задаче или скачать из репозитория (по умолчанию это текстовый файл с именем "Jenkinsfile").

Для простых проектов возможно нет смысла возиться с отдельным файлом, размещаемым во внешнем репозитории, а проще описать "pipeline" (формат "Declarative") прямо в теле задачи "Jenkins":

New Item (Job):
  Item name: project0_site1.example.net
  Pipeline project:
    ....
    Pipeline:
      Definition: Pipeline script
      Script:
        // Jenkins Declarative Pipeline
        pipeline {
          agent {label "master" && "one"}
          stages {
            stage('Git') {
              steps {
                git url: 'ssh://git-user@git.example.net/~/repo-test.git',
                  credentialsId: '0c0...f60',
                  branch: 'master'
              }
            }
            stage('Show') {
              steps {
                sh "echo '123' >> ${WORKSPACE}/123"
                sh "ls -l ${WORKSPACE}"
                echo 'Show'
              }
            }
          }
        }
  ....

Если проект большой, или в команде заведено строгое правило хранить абсолютно все конфигурации и исходный код в репозитории, то укажем в задаче "Jenkins" источник получения файла описания "pipeline":

New Item (Job):
  Item name: project0_site1.example.net
  Pipeline project:
    ....
    Pipeline:
      Definition: Pipeline script from SCM
        SCM: Git
          Repositories:
            ssh://git-user@git.example.net/~/repo-test.git
            Credentials: git-user
          Branches to build: */master
        Script Path: Jenkinsfile

Очевидно, что содержимое выделенного из конфигурации задачи описания "pipeline" полностью переносится в "Jenkinsfile":

$ vi ~/repo-test.git/Jenkinsfile

// Jenkins Declarative Pipeline
pipeline {
  agent {label "master" && "one"}
  stages {
    stage('Git') {
      steps {
        git url: 'ssh://git-user@git.example.net/~/repo-test.git',
          credentialsId: '0c0...f60',
          branch: 'master'
      }
    }
    stage('Show') {
      steps {
        sh '''
          echo '123' >> ${WORKSPACE}/123
          ls -l ${WORKSPACE}
        '''
        echo 'Show'
      }
    }
  }
}

Резервное копирование "Jenkins".

Как выше уже упоминалось, все файлы "Jenkins", в том числе и конфигурационные, по умолчанию размещаются в директории "/var/lib/jenkins". Для резервного копирования достаточно сохранить их всех, за исключением временных и пользовательских данных (на вскидку это директории ".cache", ".java", "caches", "logs", "updates", "userContent" и "workflow-libs").


Заметки и комментарии к публикации:


Оставьте свой комментарий ( выразите мнение относительно публикации, поделитесь дополнительными сведениями или укажите на ошибку )