в deploy, jenkins, scripts, tips

Jenkins as a code. Часть 1


Идея «инфраструктура как код» далеко не нова и широко используется в повседневной жизни большинством компаний. В серии статей «Jenkins as a code» предлагаю разобраться с автоматическим развертыванием и настройкой сервера Jenkins!

Казалось бы, зачем эти статьи, если можно взять готовую роль jenkins для системы управления конфигурациями Ansible или кукбук jenkins для chef, или даже воспользоваться готовым docker-образом?

Развертывание базовой конфигурации — это действительно несложный процесс (мы его рассматривать не будем, тут каждый волен выбирать свои инструменты), поэтому остановимся подробнее именно на настройке Jenkins под собственные нужды (данная статья), автоматической настройке общих библиотек (вторая часть) и импорте задач (третья часть).

Для кастомизации и тонкой настройки вашего экземпляра Jenkins разработчики предлагают использовать хуков (groovy-скриптов), которые нужно размещать в каталоге ${JENKINS_HOME}/init.groovy.d/.

В зависимости от выбранного инструмента развертывания, способ, которым скрипты попадут в нужный каталог будет отличаться. Например, при использовании docker-образа, самым простым будет поместить нужные скрипты в каталог /usr/share/jenkins/ref/init.groovy.d/:

FROM jenkins/jenkins:lts
COPY custom.groovy /usr/share/jenkins/ref/init.groovy.d/custom.groovy

При старте docker-контейнера все, что находится в каталоге /usr/share/jenkins/ref/ копируется в каталог ${JENKINS_HOME} (следовательно, каталог init.groovy.d со всем содержимым будет скопирован в нужное место).

Стоит отметить, что скрипты из каталоге ${JENKINS_HOME}/init.groovy.d/ запускаются при старте Jenkins и выполняются в алфавитном порядке — это очень важный момент, если нужно соблюдать последовательность запуска.

Чаще всего с помощью хуков в Jenkins устанавливают плагины, выполняют глобальную настройку, включают/выключают опции безопасности, добавляют ключи доступа к системе хранения версиями.

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

Скрипт 00-install-plugins.groovy выполняет установку необходимых плагинов с зависимостями и выглядит следующим образом:

/*
    Install required plugins and their dependencies.
*/
import jenkins.model.*
import hudson.model.*
import org.jenkinsci.plugins.*
import hudson.model.UpdateSite
import hudson.PluginWrapper

Set<String> plugins_to_install = [
    "github-pullrequest",
    "google-login",
    "workflow-aggregator",
    "htmlpublisher",
    "locale"
]

Boolean hasConfigBeenUpdated = false
UpdateSite updateSite = Jenkins.getInstance().getUpdateCenter().getById('default')
List<PluginWrapper> plugins = Jenkins.instance.pluginManager.getPlugins()

def install_plugin(shortName, UpdateSite updateSite) {
    println "Installing ${shortName} plugin."
    UpdateSite.Plugin plugin = updateSite.getPlugin(shortName)
    Throwable error = plugin.deploy(false).get().getError()
    if(error != null) {
        println "ERROR installing ${shortName}, ${error}"
    }
    null
}

// Check the update site(s) for latest plugins
println 'Checking plugin updates via Plugin Manager.'
Jenkins.instance.pluginManager.doCheckUpdatesServer()

// Any plugins need updating?
Set<String> plugins_to_update = []
plugins.each {
    if(it.hasUpdate()) {
        plugins_to_update << it.getShortName()
    }
}

if(plugins_to_update.size() > 0) {
    println "Updating plugins..."
    plugins_to_update.each {
        install_plugin(it, updateSite)
    }
    println "Done updating plugins."
    hasConfigBeenUpdated = true
}

// Get a list of installed plugins
Set<String> installed_plugins = []
plugins.each {
    installed_plugins << it.getShortName()
}

// Check to see if there are missing plugins to install
Set<String> missing_plugins = plugins_to_install - installed_plugins
if(missing_plugins.size() > 0) {
    println "Install missing plugins..."
    missing_plugins.each {
        install_plugin(it, updateSite)
    }
    println "Done installing missing plugins."
    hasConfigBeenUpdated = true
}

if(hasConfigBeenUpdated) {
    println "Saving Jenkins configuration to disk."
    Jenkins.instance.save()
    Jenkins.instance.restart()
} else {
    println "Jenkins up-to-date. Nothing to do."
}

Вторым по счету запускается скрипт 01-global-settings.groovy, устанавливающий количество исполнителей, локаль, глобальные настройки для системы контроля версий и протоколы взаимодействия:

import jenkins.model.*
import org.jenkinsci.plugins.*
import hudson.security.csrf.DefaultCrumbIssuer
import hudson.plugins.locale.PluginImpl

def instance = Jenkins.getInstance()

println("--- Configuring global getting")
instance.setNumExecutors(5)
instance.setCrumbIssuer(new DefaultCrumbIssuer(true))
instance.setNoUsageStatistics(true)
instance.save()

println("--- Configuring locale")
PluginImpl localePlugin = (PluginImpl)instance.getPlugin("locale")
localePlugin.systemLocale = "en_US"
localePlugin.@ignoreAcceptLanguage=true

println("--- Configuring git global options")
def desc = instance.getDescriptor("hudson.plugins.git.GitSCM")
desc.setGlobalConfigName("jenkins")
desc.setGlobalConfigEmail("jenkins@example.com")
desc.save()

println("--- Configuring protocols")
Set<String> agentProtocolsList = ['JNLP4-connect', 'Ping']
if(!instance.getAgentProtocols().equals(agentProtocolsList)) {
    instance.setAgentProtocols(agentProtocolsList)
    println "Agent Protocols have changed.  Setting: ${agentProtocolsList}"
    instance.save()
}
else {
    println "Nothing changed.  Agent Protocols already configured: ${instance.getAgentProtocols()}"
}

Следующим будет выполнен скрипт с именем 02-disable-cli.groovy (как несложно догадаться, отключающий CLI):

import jenkins.*
import jenkins.model.*
import hudson.model.*
import java.util.logging.Logger
import org.jenkinsci.main.modules.sshd.*
Logger logger = Logger.getLogger("")

// Disable CLI access over TCP listener (separate port)
def p = AgentProtocol.all()
p.each { x ->
    if (x.name?.contains("CLI")) {
        logger.info("Removing protocol ${x.name}")
        p.remove(x)
    }
}

// Disable CLI access over /cli URL
def removal = { lst ->
    lst.each { x ->
        if (x.getClass().name.contains("CLIAction")) {
            logger.info("Removing extension ${x.getClass().name}")
            lst.remove(x)
        }
    }
}

def j = Jenkins.instance
removal(j.getExtensionList(RootAction.class))
removal(j.actions)

// Disable CLI over Remoting
jenkins.CLI.get().setEnabled(false)

// Allow SSH connections
def sshdExtension = Jenkins.instance.getExtensionList(SSHD.class)[0]
sshdExtension.setPort(22222)
sshdExtension.save()

// Configure Slave-to-Master Access Control
// https://wiki.jenkins-ci.org/display/JENKINS/Slave+To+Master+Access+Control

def rule = Jenkins.instance.getExtensionList(jenkins.security.s2m.MasterKillSwitchConfiguration.class)[0].rule
if(!rule.getMasterKillSwitch()) {
    rule.setMasterKillSwitch(true);
    logger.info('Disabled agent -> master security for cobertura.');
}
else {
    logger.info('Nothing changed.  Agent -> master security already disabled.');
}

// Do not annoy with Slave-to-Master Access Control warning
Jenkins.instance.getExtensionList(jenkins.security.s2m.MasterKillSwitchWarning.class)[0].disable(true);
Jenkins.instance.save()

И, наконец, скрипт 03-user-service.groovy создает пользователя и добавляет ему ssh-ключ для доступа к системе контроля версий:

public_key = 'ssh-rsa AAAAB3N....TJChv jenkins'
user = hudson.model.User.get('service')
user.setFullName('Service User')
keys = new org.jenkinsci.main.modules.cli.auth.ssh.UserPropertyImpl(public_key)
user.addProperty(keys)
user.save()

На этом с настройкой экземпляра Jenkins под собственные нужды все, в следующей статье рассмотрим автоматическую настройку общих библиотек (Shared Libraries) при запуске Jenkins.

Добавить комментарий