Архив автора: admin

Jenkins as a code. Часть 4

Возникла необходимость разграничения прав доступа пользователей на Jenkins-сервере — давайте разберемся, как можно это сделать без использования webUI!




В моем случае нужно разделить пользователей на две группы — администраторов (с полным уровнем доступа) и обычных пользователей (есть доступ только на просмотр списка заданий, запуск и отмену запущенных задач). Для решения этой задачи отлично подходит плагин Role-based Authorization Strategy. Главной особенностью является наличие в данном плагине двух предустановленных групп пользователей:




  • Anonymous — незалогиненные пользователи;



  • authenticated — залогиненные пользователи.




Использование группы authenticated даст возможность не перечислять всех пользователей нашего jenkins-сервера в явном виде.




Устанавливать данный плагин мы будем при старте jenkins-сервера с помощью инит-скрипта, как описано в этой статье. Для этого в скрипте 00-install-plugins.groovy список устанавливаемых плагинов приводим к следующему виду:




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




Теперь создадим еще один groovy-скрипт (назовем его 06-role-based-auth.groovy) со следующим содержанием:




import hudson.*
import hudson.model.*
import hudson.security.*
import jenkins.*
import jenkins.model.*
import java.util.*
import com.michelin.cio.hudson.plugins.rolestrategy.*
import java.lang.reflect.*

// Roles
def globalRoleRead = "builder"
def globalRoleAdmin = "admin"

// Users and Groups
def access = [
    admins: [
            "admin@example.com",
            "devops@example.com",
            "ealebed@example.com"
    ],
    builders: ["authenticated"],
]

def instance = Jenkins.getInstance()
def currentAuthenticationStrategy = Hudson.instance.getAuthorizationStrategy()

Thread.start {
    sleep 15000
    if (currentAuthenticationStrategy instanceof RoleBasedAuthorizationStrategy) {
        println "Role based authorisation already enabled."
        println "Exiting script..."
        return
    } else {
        println "Enabling role based authorisation strategy..."
    }

    // Set new authentication strategy
    RoleBasedAuthorizationStrategy roleBasedAuthenticationStrategy = new RoleBasedAuthorizationStrategy()
    instance.setAuthorizationStrategy(roleBasedAuthenticationStrategy)

    Constructor[] constrs = Role.class.getConstructors()
    for (Constructor<?> c : constrs) {
        c.setAccessible(true)
    }

    // Make the method assignRole accessible
    Method assignRoleMethod = RoleBasedAuthorizationStrategy.class.getDeclaredMethod("assignRole", String.class, Role.class, String.class)
    assignRoleMethod.setAccessible(true)

    // Create admin set of permissions
    Set<Permission> adminPermissions = new HashSet<Permission>()
    adminPermissions.add(Permission.fromId("hudson.model.Hudson.Administer"))
    adminPermissions.add(Permission.fromId("hudson.model.Hudson.Read"))
    adminPermissions.add(Permission.fromId("com.cloudbees.plugins.credentials.CredentialsProvider.View"))
    adminPermissions.add(Permission.fromId("com.cloudbees.plugins.credentials.CredentialsProvider.ManageDomains"))
    adminPermissions.add(Permission.fromId("com.cloudbees.plugins.credentials.CredentialsProvider.Create"))
    adminPermissions.add(Permission.fromId("com.cloudbees.plugins.credentials.CredentialsProvider.Update"))
    adminPermissions.add(Permission.fromId("com.cloudbees.plugins.credentials.CredentialsProvider.Delete"))
    adminPermissions.add(Permission.fromId("hudson.model.View.Read"))
    adminPermissions.add(Permission.fromId("hudson.model.View.Create"))
    adminPermissions.add(Permission.fromId("hudson.model.View.Configure"))
    adminPermissions.add(Permission.fromId("hudson.model.View.Delete"))
    adminPermissions.add(Permission.fromId("hudson.model.Item.Read"))
    adminPermissions.add(Permission.fromId("hudson.model.Item.Discover"))
    adminPermissions.add(Permission.fromId("hudson.model.Item.Workspace"))
    adminPermissions.add(Permission.fromId("hudson.model.Item.Create"))
    adminPermissions.add(Permission.fromId("hudson.model.Item.Move"))
    adminPermissions.add(Permission.fromId("hudson.model.Item.Configure"))
    adminPermissions.add(Permission.fromId("hudson.model.Item.Build"))
    adminPermissions.add(Permission.fromId("hudson.model.Item.Cancel"))
    adminPermissions.add(Permission.fromId("hudson.model.Item.Delete"))
    adminPermissions.add(Permission.fromId("hudson.model.Computer.Create"))
    adminPermissions.add(Permission.fromId("hudson.model.Computer.Connect"))
    adminPermissions.add(Permission.fromId("hudson.model.Computer.Configure"))
    adminPermissions.add(Permission.fromId("hudson.model.Computer.Build"))
    adminPermissions.add(Permission.fromId("hudson.model.Computer.Provision"))
    adminPermissions.add(Permission.fromId("hudson.model.Computer.Disconnect"))
    adminPermissions.add(Permission.fromId("hudson.model.Computer.Delete"))
    adminPermissions.add(Permission.fromId("hudson.model.Run.Update"))
    adminPermissions.add(Permission.fromId("hudson.model.Run.Replay"))
    adminPermissions.add(Permission.fromId("hudson.model.Run.Delete"))
    adminPermissions.add(Permission.fromId("hudson.scm.SCM.Tag"))

    // Create permissions for authenticated users
    Set<Permission> authenticatedPermissions = new HashSet<Permission>()
    authenticatedPermissions.add(Permission.fromId("hudson.model.Hudson.Read"))
    authenticatedPermissions.add(Permission.fromId("hudson.model.View.Read"))
    authenticatedPermissions.add(Permission.fromId("hudson.model.Item.Read"))
    authenticatedPermissions.add(Permission.fromId("hudson.model.Item.Build"))
    authenticatedPermissions.add(Permission.fromId("hudson.model.Item.Cancel"))

    // Create the admin role
    Role adminRole = new Role(globalRoleAdmin, adminPermissions)
    roleBasedAuthenticationStrategy.addRole(RoleBasedAuthorizationStrategy.GLOBAL, adminRole)

    // Create the builder role
    Role builderRole = new Role(globalRoleRead, authenticatedPermissions)
    roleBasedAuthenticationStrategy.addRole(RoleBasedAuthorizationStrategy.GLOBAL, builderRole)

    // Assign the admin role
    access.admins.each { l ->
        println("Granting admin permissions to ${l}")
        roleBasedAuthenticationStrategy.assignRole(RoleBasedAuthorizationStrategy.GLOBAL, adminRole, l)
    }

    access.builders.each { l ->
        println("Granting builder permissions to ${l}")
        roleBasedAuthenticationStrategy.assignRole(RoleBasedAuthorizationStrategy.GLOBAL, builderRole, l)
    }

    // Save the state
    println "Saving changes."
    instance.save()
}




С помощью данного скрипта создаются две роли — builder и admin, которым устанавливаются разные наборы прав доступа. Далее всем пользователям, перечисленным в access.admins, назначается роль admin, а группе authenticated (находится в access.builders) назначается роль builder.




Источник: https://ealebed.github.io/posts/2018/jenkins-as-a-code-часть-4/



2023-01-03T00:16:15
DevOps

Jenkins as a code. Часть 3

В данной статье цикла “Jenkins as a code” рассмотрим самый интересный (и полезный) пример — автоматическое создание задач (job) при запуске сервиса. Давайте разберемся!




Создавать задания (jobs) в Jenkins можно несколькими способами — традиционно (через webUI), через REST API или “подкладывая” файлы .xml.import в ${JENKINS_HOME}.




Но традиционный способ настройки занимает слишком много времени, использование API невероятно неудобно (см. пример ниже), а создание файлов с расширением .xml.import для каждой задачи и рестарт сервиса Jenkins после их добавления даже звучит несерьезно.




Чем плох REST API:




  • сначала необходимо получить описание уже существующей задачи в формате .xml. Например, это можно сделать используя API:




curl -X GET -u username:API_TOKEN 'http://JENKINS_HOST/job/MY_JOB_NAME/config.xml' -o config.xml




или




curl -u username:password "http://JENKINS_HOST/job/MY_JOB_NAME/config.xml" > config.xml




  • потом полученный xml-файл нужно отредактировать под свои нужды;



  • после редактирования можно создать новую задачу “запостив” этот файл через API, например:




curl -u username:password -s -XPOST 'http://JENKINS_HOST/createItem?name=newJobName' --data-binary config.xml -H "Content-Type:text/xml"




Ах да, если вдруг столкнетесь с ошибкой:




Error 403 No valid crumb was included in the request




то придется сделать на одно действие больше:




CRUMB=$(curl -s 'http://JENKINS_HOST/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,":",//crumb)' -u username:password)//crumb)' -u us




curl -u username:password -s -XPOST 'http://JENKINS_HOST/createItem?name=newJobName' --data-binary config.xml -H "$CRUMB" -H "Content-Type:text/xml"




Вообщем, так себе вариант, особенно если у вас 150-200 заданий…




Вспоминаем о возможностях настройки экземпляра Jenkins еще в момент старта с помощью хуков, которые мы рассматривали в первой  и второй статьях данного цикла. Идея — хранить в отдельном файле определенного формата (.json или .xml) минимально необходимый набор параметров (ссылка на git-репозиторий, название ветки, путь к Jenkinsfile и т. д.) для создания задачи. При запуске Jenkins groovy-скрипт берет из данного файла параметры и создает задачи, в качестве дополнительного бонуса — сразу распределяет эти задачи по представлениям (views).




Создаем файл job_list.yaml с набором параметров для задач, в моем случае выглядящий так:




dev:
  1st-project:
    jobDisabled:          false
    enableGithubPolling:  false
    daysBuildsKeep:       '1'
    numberBuildsKeep:     '5'
    githubRepoUrl:        'https://github.com/ealebed/hn1/'
    githubRepo:           'https://github.com/ealebed/hn1.git'
    githubBranch:         '*/master, */test'
    credentialId:         ''
    jenkinsfilePath:      'Jenkinsfile'
  2nd-project:
    jobDisabled:          false
    enableGithubPolling:  true
    daysBuildsKeep:       '2'
    numberBuildsKeep:     '10'
    githubRepoUrl:        'https://github.com/ealebed/hn1/'
    githubRepo:           'https://github.com/ealebed/hn1.git'
    githubBranch:         '*/master'
    credentialId:         ''
    jenkinsfilePath:      'Jenkinsfile'
stage:
  3rd-project:
    jobDisabled:          false
    enableGithubPolling:  true
    daysBuildsKeep:       '3'
    numberBuildsKeep:     '15'
    githubRepoUrl:        'https://github.com/ealebed/hn1/'
    githubRepo:           'https://github.com/ealebed/hn1.git'
    githubBranch:         '*/master'
    credentialId:         ''
    jenkinsfilePath:      'dir/Jenkinsfile'
test:
  4th-project:
    jobDisabled:          true
    enableGithubPolling:  true
    daysBuildsKeep:       '4'
    numberBuildsKeep:     '20'
    githubRepoUrl:        'https://github.com/ealebed/hn1/'
    githubRepo:           'https://github.com/ealebed/hn1.git'
    githubBranch:         '*/master'
    credentialId:         ''
    jenkinsfilePath:      'Jenkinsfile'




Примечание. Если планируется переезд со старого экземпляра Jenkins на новый, то можно легко написать скрипт, который возьмет нужные параметры на старом сервере и сформирует yaml-файл в нужном формате.




Скрипт, который должен “разобрать” данный файл и создать задачи у меня называется 05-create-jobs.groovy и выглядит следующим образом:




@Grab(group='org.yaml', module='snakeyaml', version='1.18')
import jenkins.model.*
import hudson.model.*
import hudson.tasks.LogRotator
import hudson.plugins.git.*
import org.jenkinsci.plugins.workflow.job.WorkflowJob
import org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition
import com.coravy.hudson.plugins.github.*
import com.cloudbees.jenkins.GitHubPushTrigger
import org.yaml.snakeyaml.Yaml

Jenkins jenkins = Jenkins.getInstance()

def pluginParameter = "workflow-aggregator github-pullrequest"
def plugins         = pluginParameter.split()
def pm              = jenkins.getPluginManager()
def installed       = false

plugins.each {
    if (!pm.getPlugin(it)) {
        println("Plugin ${it} not installed, skip creating jobs!")
    } else {
        installed = true
    }
}

if(installed) {
    def listExistingJob     = jenkins.items.collect { it.name }
    def listExistingViews   = jenkins.views.collect { it.name }
    def jobParameters       = new Yaml().load(new FileReader('/temp/job_list.yaml'))

    for (view in jobParameters) {
        if (jobParameters.any { listExistingViews.contains(view.key) }) {
            println("--- View ${view.key} already exist, skip")
        } else {
            println("--- Create new view ${view.key}")
            jenkins.addView(new ListView(view.key))
        }
        for (item in view.value) {
            if (view.value.any { listExistingJob.contains(item.key) }) {
                println("--- Job ${item.key} already exist, skip")
                continue
            } else {
                println("--- Create new job ${item.key}")

                def jobName             = item.key
                def jobDisabled         = view.value.get(item.key).getAt("jobDisabled")
                def enableGithubPolling = view.value.get(item.key).getAt("enableGithubPolling")
                def daysBuildsKeep      = view.value.get(item.key).getAt("daysBuildsKeep")
                def numberBuildsKeep    = view.value.get(item.key).getAt("numberBuildsKeep")
                def githubRepoUrl       = view.value.get(item.key).getAt("githubRepoUrl")
                def githubRepo          = view.value.get(item.key).getAt("githubRepo")
                def githubBranch        = view.value.get(item.key).getAt("githubBranch")
                def credentialId        = view.value.get(item.key).getAt("credentialId")
                def jenkinsfilePath     = view.value.get(item.key).getAt("jenkinsfilePath")

                def githubBranchList = githubBranch.split(', ')
                def branchConfig = new ArrayList<BranchSpec>()
                for (branch in githubBranchList) {
                    if( branch != null )
                    {
                        branchConfig.add(new BranchSpec(branch))
                    }
                    else
                    {
                        branchConfig.add(new BranchSpec("*/master"))
                    }
                }
                def userConfig          = [new UserRemoteConfig(githubRepo, null, null, credentialId)]
                def scm                 = new GitSCM(userConfig, branchConfig, false, [], null, null, null)
                def flowDefinition      = new CpsScmFlowDefinition(scm, jenkinsfilePath)

                flowDefinition.setLightweight(true)

                def job = new WorkflowJob(jenkins, jobName)
                job.definition = flowDefinition
                job.setConcurrentBuild(false)
                job.setDisabled(jobDisabled)
                job.addProperty(new BuildDiscarderProperty(new LogRotator(daysBuildsKeep, numberBuildsKeep, null, null)))
                job.addProperty(new GithubProjectProperty(githubRepoUrl))

                if (true == enableGithubPolling) {
                    job.addTrigger(new GitHubPushTrigger())
                }

                jenkins.save()
                jenkins.reload()
                hudson.model.Hudson.instance.getView(view.key).doAddJobToView(jobName)
            }
        }
    }
}




Сначала мы проверяем, установлены ли необходимые плагины (без них создать представление / задачу не получится). Далее получаем список уже существующих представлений и задач (может так случиться, что сервер с Jenkins будет перезагружен) — нам совершенно нет нужды пересоздавать задачи при каждом старте Jenkins. Только если нужного нам представления (view) нет — оно будет создано, после чего создаются добавляются в представление связанные с ним задачи.




Остается только позаботиться о том, чтобы файл с параметрами задач появился в /temp/job_list.yaml (или другом удобном для вас месте), а скрипт 05-create-jobs.groovy — в каталоге ${JENKINS_HOME}/init.groovy.d/.




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



2023-01-03T00:15:17
DevOps

Jenkins as a code. Часть 2

В первой статье цикла “Jenkins as a code” мы разобрали несколько примеров настройки экземпляра Jenkins под собственные нужды, в данной статье рассмотрим автоматическую настройку общих библиотек (Shared Libraries) — давайте разберемся!




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




Как и в предыдущей статье, нам на помощь приходят хуки (groovy-скрипты) из каталога ${JENKINS_HOME}/init.groovy.d/. Создадим новый файл 04-global-pipeline-library.groovy следующего содержания:




import jenkins.model.Jenkins
import jenkins.plugins.git.GitSCMSource
import jenkins.plugins.git.traits.BranchDiscoveryTrait
import org.jenkinsci.plugins.workflow.libs.GlobalLibraries
import org.jenkinsci.plugins.workflow.libs.LibraryConfiguration
import org.jenkinsci.plugins.workflow.libs.SCMSourceRetriever

List libraries = [] as ArrayList

def instance = Jenkins.getInstance()

println("--- Configuring global library getting")
def githubRepo      = 'https://github.com/ealebed/jenkins-shared-libs.git'
def libraryName     = 'jenkins-shared-libs'
def githubBranch    = 'master'
def credentialsId   = ''

def scm             = new GitSCMSource(githubRepo)
scm.credentialsId   = credentialsId
scm.traits          = [new BranchDiscoveryTrait()]
def retriever       = new SCMSourceRetriever(scm)

def library         = new LibraryConfiguration(libraryName, retriever)
library.defaultVersion          = githubBranch
library.implicit                = true
library.allowVersionOverride    = true
library.includeInChangesets     = true

libraries << library

def global_settings = instance.getExtensionList(GlobalLibraries.class)[0]
global_settings.libraries = libraries
global_settings.save()




Интересующие нас настройки задаются с помощью четырех параметров — githubRepolibraryNamegithubBranch и credentialsId.




Здесь:




  • githubRepo — ссылка на репозиторий с общими библиотеками в системе контроля версий (может быть как https, так и git);



  • libraryName — имя подключаемой общей библиотеки. Именно это имя будет использоваться в пайплайнах (Jenkinsfile) в формате @Library('jenkins-shared-libs@master') _;



  • githubBranch — ветка в git-репозитории с общими библиотеками (Она же “версия”). Также используется в пайплайнах (Jenkinsfile);



  • credentialsId — креденшелы для подключения к git-репозиторию. Значение может быть пустым — помните скрипт 03-user-service.groovy из первой части цикла, которым мы настраивали доступ к системе контроля версий?




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




Источник: https://ealebed.github.io/posts/2018/jenkins-as-a-code-часть-2/



2023-01-03T00:14:09
DevOps

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.




Источник: https://ealebed.github.io/posts/2018/jenkins-as-a-code-часть-1/



2023-01-03T00:12:36
DevOps

Jenkins: использование shared libraries

При использовании Jenkins в компании с большим количеством проектов, рано или поздно вы заметите, что ваши описания пайплайнов (Pipeline) имеют много общего. И, возможно, вам захочется избавиться от избыточности и следовать принципу DRY (Don’t Repeat Yourself) — давайте разберемся!




Помимо принципа DRY, важна также возможность внести изменения в код пайплайна один раз и автоматически использовать обновленный пайплайн в 50-100 других проектах.




Для этой цели как нельзя лучше подходят Shared Libraries — общие библиотеки, которые могут быть определены в отдельном репозитории системы управления версиями и загружены в описании пайплайна.




Структура каталогов в репозитории общих библиотек должна выглядеть следующим образом:




+- src                     # Source files
|   +- org
|       +- foo
|           +- Bar.groovy  # for org.foo.Bar class
+- vars
|   +- foo.groovy          # for global 'foo' variable
|   +- foo.txt             # help for 'foo' variable
+- resources               # resource files (external libraries only)
|   +- org
|       +- foo
|           +- bar.json    # static helper data for org.foo.Bar




Примечание. В данной статье мы будем использовать только каталоги vars и resources.




Для нас наибольший интерес представляет директория vars — в ней можно разместить глобальные функции и переменные, доступные в пайплайнах. Согласно документации, имена файлов должны быть в camelCase формате (без дефисов/подчеркиваний и т.д. — это важно), и иметь расширение .groovy (именно эти файлы нас и интересуют) или .txt (для документации).




В каталоге resources можно разместить любые другие (не Java) файлы (например, .yaml или .json), которые будут загружаться в описании пайплайна с помощью шага libraryResource.




Итак, рассмотрим несколько примеров. Допустим, у нас есть простенький проект, в котором описание пайплайна (файл Jenkinsfile) выглядит так:




pipeline {
  agent any

  stages {
    stage('Checkout') {
      steps {
        checkout scm
      }
    }
    stage('Build') {
      steps {
        script {
          sh '''
            sudo docker version
            sudo docker build -t ealebed/hellonode:latest .
            sudo docker image ls
          '''
        }
      }
    }
  }
}




Кроме описания пайплайна в репозитории находится еще два файла — Dockerfile:




FROM node:6.9
COPY server.js .
EXPOSE 8080
CMD node server.js




и файл server.js следующего содержания:




var http = require('http');
var handleRequest = function(request, response) {
  response.writeHead(200);
  response.end("Hello World!");
}
var www = http.createServer(handleRequest);
www.listen(8080);




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




  • создаем отдельный git-репозиторий для наших общих библиотек;



  • в репозитории создаем каталог vars;



  • в каталоге vars размещаем скрипт dockerCmd.groovy.




Содержимое dockerCmd.groovy:




def call(args) {
  assert args != null
  sh(script: "sudo docker ${args}")
}




Настраиваем использование Shared Libraries на Jenkins (пример с картинками). Теперь в нашем проекте пайплайн можно переписать так:




@Library('jenkins-shared-libs@master') _

pipeline {
  agent any

  stages {
    stage('Checkout') {
      steps {
        checkout scm
      }
    }
    stage('Build') {
      steps {
        dockerCmd 'version'
        dockerCmd 'build -t ealebed/hellonode:latest .'
        dockerCmd 'image ls'
      }
    }
  }
}




Продолжаем. Во второй итерации избавимся от необходимости хранить Dockerfile в репозитории проекта. Для этого в git-репозитории с общими библиотеками создаем каталог resources и переносим в него Dockerfile из основного проекта. Далее, в каталоге vars размещаем скрипт createDockerfile.groovy следующего содержания:




def call() {
  def file = libraryResource 'Dockerfile'
  writeFile file: 'Dockerfile', text: file
}




Описание пайплайна в основном проекте изменяем на следующее:




@Library('jenkins-shared-libs@master') _

pipeline {
  agent any

  stages {
    stage('Checkout') {
      steps {
        checkout scm
      }
    }
    stage('Get Dockerfile') {
      steps {
        createDockerfile()
      }
    }
    stage('Build') {
      steps {
        dockerCmd 'version'
        dockerCmd 'build -t ealebed/hellonode:latest .'
        dockerCmd 'image ls'
      }
    }
  }
}




Но что, если мы хотим использовать в других проектах не только отдельные функции, а весь пайплайн целиком? Нет ничего проще!




В git-репозитории с общими библиотеками в каталоге vars создаем скрипт allPipeline.groovy следующего содержания:




def call(body) {
  def pipelineParams= [:]
  body.resolveStrategy = Closure.DELEGATE_FIRST
  body.delegate = pipelineParams
  body()

  pipeline {
    agent any

    stages {
      stage('Checkout') {
        steps {
          checkout scm
        }
      }
      stage('Get Dockerfile') {
        steps {
          script {
            def tmpFile = libraryResource 'Dockerfile'
            writeFile file: 'Dockerfile', text: tmpFile
          }
        }
      }
      stage('Build') {
        steps {
          script {
            sh '''
              docker version
              docker build -t ealebed/hellonode:latest .
              docker image ls
            '''
          }
        }
      }
    }
  }
}




Теперь, в содержимое файла Jenkinsfile (описание пайплайна) в основном репозитории проекта невероятно упрощается:




@Library('jenkins-shared-libs@master') _

allPipeline {}




Больше интересных примеров можно найти в официальной документации, а также здесьздесь и здесь.




Don’t Repeat Yourself!




Источник: https://ealebed.github.io/posts/2018/jenkins-использование-shared-libraries/



2023-01-03T00:10:25
DevOps

Знакомство с Kubernetes. HorizontalPodAutoscaler

В этой статье рассмотрим использование HorizontalPodAutoscaler — объектов, предназначенных для автоматического масштабирования количества подов (Pods) в Replication ControllerReplica Set или Deployment, основываясь на использовании CPU (или, при поддержке custom metrics, на других метриках приложения). Давайте разберемся!




Сразу стоит отметить, что HorizontalPodAutoscaler не может быть применен к объектам, которые не предназначены для масштабирования, например DaemonSets. Horizontal Pod Autoscaler состоит из Kubernetes ресурса (объекта) и контроллера, поведение которого описывается ресурсом.




C периодичностью 15 секунд (можно изменить с помощью параметра --horizontal-pod-autoscaler-sync-period), контроллер собирает данные по использованию метрик, определенных в манифесте ресурса HorizontalPodAutoscaler. Метрики собираются или с resource metrics API (метрики использования ресурсов подами) или с custom metrics API (остальные метрики, например, метрики приложения).




Для каждого подконтрольного пода, контроллер собирает метрики (например, использования CPU) с resource metrics API (metrics.k8s.io, предоставляется metrics-server). Далее, происходит вычисление текущего значения использования CPU в процентах от запрошенных ресурсов (resource request) контейнерами каждого пода, после чего это значение сравнивается с “целевым” (target) значением — порогом, после которого количество подов должно быть увеличено.




Рассмотрим конкретный пример. Создадим файл test-hpa.yaml с описанием ресурса HorizontalPodAutoscaler такого содержания:




apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: test-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: test-api-deploy
  minReplicas: 10
  maxReplicas: 29
  metrics:
  - type: Resource
    resource:
      name: cpu
      targetAverageUtilization: 80




Создадим данный объект в кластере Kubernetes:




kubectl create -f test-hpa.yaml




Проверим наличие объекта:




kubectl get horizontalpodautoscaler
NAME          REFERENCE                        TARGETS         MINPODS   MAXPODS   REPLICAS   AGE
test-hpa      Deployment/test-api-deploy       <unknown>/80%   10        29        0          7s




Спустя некоторое время, вместо <unknown>, мы должны увидеть текущее использование CPU подами в деплойменте test-api-deploy, однако в моем случае этого не произошло. Начинаем разбираться — для начала, убедимся, что metrics.k8s.io доступно:




kubectl get --raw "/apis/metrics.k8s.io/" | jq
{
  "kind": "APIGroup",
  "apiVersion": "v1",
  "name": "metrics.k8s.io",
  "versions": [
    {
      "groupVersion": "metrics.k8s.io/v1beta1",
      "version": "v1beta1"
    }
  ],
  "preferredVersion": {
    "groupVersion": "metrics.k8s.io/v1beta1",
    "version": "v1beta1"
  }
}




Проверим, что метрики использования CPU доступны. Первый вариант:




kubectl top pod | grep test-api-deploy
test-api-deploy-5f77b79896-2t9x9                        738m         43931Mi
test-api-deploy-5f77b79896-fhr7b                        643m         43999Mi
test-api-deploy-5f77b79896-gcrlc                        700m         44028Mi
test-api-deploy-5f77b79896-lx24k                        666m         44201Mi
test-api-deploy-5f77b79896-mzlzb                        660m         44048Mi
test-api-deploy-5f77b79896-ndjwx                        651m         44136Mi
test-api-deploy-5f77b79896-q2nvw                        654m         44177Mi
test-api-deploy-5f77b79896-qmw4t                        692m         44051Mi
test-api-deploy-5f77b79896-rl4bb                        650m         43979Mi
test-api-deploy-5f77b79896-xhpbx                        752m         44116Mi




Второй вариант (метрики только одного конкретного пода):




kubectl get --raw /apis/metrics.k8s.io/v1beta1/namespaces/default/pods/test-api-deploy-5f77b79896-xhpbx | jq
{
  "kind": "PodMetrics",
  "apiVersion": "metrics.k8s.io/v1beta1",
  "metadata": {
    "name": "test-api-deploy-5f77b79896-xhpbx",
    "namespace": "default",
    "selfLink": "/apis/metrics.k8s.io/v1beta1/namespaces/default/pods/test-api-deploy-5f77b79896-xhpbx",
    "creationTimestamp": "2019-06-11T13:50:00Z"
  },
  "timestamp": "2019-06-11T13:49:41Z",
  "window": "30s",
  "containers": [
    {
      "name": "envoy",
      "usage": {
        "cpu": "489151208n",
        "memory": "45692Ki"
      }
    },
    {
      "name": "test",
      "usage": {
        "cpu": "7125240328n",
        "memory": "45515856Ki"
      }
    }
  ]
}




Как видим, метрики доступны. Получим детальное описание нашего HorizontalPodAutoscaler:




kubectl describe hpa test-hpa
Name:                                                  test-hpa
Namespace:                                             default
Labels:                                                app.kubernetes.io/managed-by=spinnaker
                                                       app.kubernetes.io/name=test
Annotations:                                           artifact.spinnaker.io/location: default
                                                       artifact.spinnaker.io/name: test-hpa
                                                       artifact.spinnaker.io/type: kubernetes/horizontalpodautoscaler
                                                       kubectl.kubernetes.io/last-applied-configuration:
                                                         {"apiVersion":"autoscaling/v2beta1","kind":"HorizontalPodAutoscaler","metadata":{"annotations":{"artifact.spinnaker.io/location":"default"...
                                                       moniker.spinnaker.io/application: test
                                                       moniker.spinnaker.io/cluster: horizontalpodautoscaler test-hpa
CreationTimestamp:                                     Tue, 11 Jun 2019 11:21:03 +0300
Reference:                                             Deployment/test-api-deploy
Metrics:                                               ( current / target )
  resource cpu on pods  (as a percentage of request):  <unknown> / 80%
Min replicas:                                          10
Max replicas:                                          29
Deployment pods:                                       10 current / 10 desired
Conditions:
  Type           Status  Reason                   Message
  ----           ------  ------                   -------
  AbleToScale    True    SucceededGetScale        the HPA controller was able to get the target's current scale
  ScalingActive  False   FailedGetResourceMetric  the HPA was unable to compute the replica count: missing request for cpu
Events:
  Type     Reason                        Age                    From                       Message
  ----     ------                        ----                   ----                       -------
  Normal   SuccessfulRescale             7m17s                  horizontal-pod-autoscaler  New size: 10; reason: Current number of replicas below Spec.MinReplicas
  Warning  FailedComputeMetricsReplicas  4m15s (x12 over 7m2s)  horizontal-pod-autoscaler  failed to get cpu utilization: missing request for cpu
  Warning  FailedGetResourceMetric       2m15s (x20 over 7m2s)  horizontal-pod-autoscaler  missing request for cpu




Здесь самое важное — сообщение the HPA was unable to compute the replica count: missing request for cpu. И действительно, в манифесте развертывания (Deployment) не указаны resource requests для одного из контейнеров (с именем envoy):




apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
# From https://www.spinnaker.io/reference/providers/kubernetes-v2/#strategy  
    strategy.spinnaker.io/use-source-capacity: "true"
  name: test-api-deploy
spec:
#  replicas: 15
  selector:
    matchLabels:
      deployment: test-api-deploy
  strategy:
    rollingUpdate:
      maxSurge: 0
    type: RollingUpdate
  template:
    metadata:
      labels:
        deployment: test-api-deploy
    spec:
      containers:
      - image: envoyproxy/envoy:v1.10.0
        name: envoy
        ports:
        - containerPort: 8080
          name: http
        volumeMounts:
        - mountPath: /etc/envoy
          name: envoy-config
      - env:
        - name: JAVA_OPTS
          value: -Xms40g -Xmx40g
        image: index.docker.io/ealebed/test:v1
        name: test
        resources:
          limits:
            memory: 55Gi
          requests:
            cpu: "10"
            memory: 55Gi
      volumes:
      - configMap:
          name: envoy-config
        name: envoy-config




Важно! Если не указаны resource request хотя бы для одного из контейнеров в Replication ControllerReplica Set или Deployment, то текущее значение использование CPU подами не может быть корректно определено, и, в результате, HorizontalPodAutoscaler не будет предпринимать никаких действий по масштабированию.




После исправления этой досадной ошибки, HorizontalPodAutoscaler, базируясь на полученных метриках, начинает масштабировать поды в развертывании:




kubectl get horizontalpodautoscaler                                                                                                              
NAME          REFERENCE                       TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
test-hpa      Deployment/test-api-deploy      86%/80%   10        29        29         9m10




Формула, по которой HorizontalPodAutoscaler вычисляет требуемое количество реплик выглядит так:




desiredReplicas = ceil[currentReplicas * ( currentMetricValue / desiredMetricValue )]




Например, если текущее значение метрики (currentMetricValue) равно 200m, а ожидаемое (desiredMetricValue) установлено в 100m, то количество реплик будет удвоено (200.0 / 100.0 == 2.0). Если же текущее значение метрики равно всего лишь 50m, то количество реплик должно быть уменьшено вдвое (50.0 / 100.0 == 0.5). Если соотношение текущего значения метрики к ожидаемому значению достаточно близко к 1, то никаких действий не будет предпринято.




Так как мы указали targetAverageUtilization при описании ресурса HorizontalPodAutoscaler, то текущее значение метрики (currentMetricValue) использования CPU рассчитывается как среднее значение этой метрики для всех подов, контролируемых данным автоскейлером.




После того, как текущее значение использования CPU снизилось и оставалось низким в течении 5 минут (устанавливается с помощью параметра --horizontal-pod-autoscaler-downscale-stabilization), количество реплик было автоматически уменьшено:




kubectl get horizontalpodautoscaler                                                                                                              
NAME          REFERENCE                       TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
test-hpa      Deployment/test-api-deploy      70%/80%   20        29        23         1h




На этом все, в одной из следующих статей рассмотрим более сложный вариант автоскейлинга, базирующийся на метриках приложения.




Источник: https://ealebed.github.io/posts/2019/знакомство-с-kubernetes-часть-19-horizontalpodautoscaler/



2023-01-03T00:09:28
DevOps