小毛

V1

2022/06/19阅读:17主题:自定义主题1

Jenkins流水线的另类实践

本文来自于Jenkins的实践总结。

CI/CD与Jenkins

CI/CD
CI/CD
  • CI continuous integration 持续集成
  • CD continuous delivery/deployment 持续交付/部署

CI/CD概念还是比较简单的,市面上有一些CI/CD产品:

  • CruiseControl ThoughtWorks开源的早期的CI工具,纯xml配置,很多人可能没听过。
  • Jenkins 最流行的CI/CD平台,从Hudson发展过来
  • Github Action/Gitlab CI 代码托管平台的内置CI
  • Travis CI/Circle CI 基于云的SAAS平台,方便集成开源代码托管平台

目前的主要发展方向有:

  • 插件化、云原生支持、Pipeline as Code
  • DevOps、DevSecOps

Jenkins介绍(扩展性与局限性)

开源、支持多种平台、安装部署简单、极其丰富的插件生态

Jenkins的扩展性

举例一些重要的插件,这是自己实践中比较有用且重要的插件

Jenkins几乎所有的功能都是通过插件实现的,需要在上千个插件中选择。另外,像Blue Ocean之类的插件,在展示上效果是不错的,但作为流水线编辑太傻瓜化,对于较复杂灵活的场景并不是很实用

Jenkins的局限性

当你需要管理数以百计的任务时,可能就会发现Jenkins也有一些不方便的地方。

  • Jenkins支持master-slave工作模式,但master是单点。
  • Jenkins的slave节点闲时空闲,资源利用率不高。
  • Jenkins配置是文件存储的,相对数据库比较脆弱。
  • Jenkins的界面化配置比较繁琐。

对应解决方案

  • 采用Kubernetes进行master的故障恢复。目前看看单master支持数百任务是没有问题,如果数量更多,建议就要进行master拆分。
  • 采用Kubernetes按需动态生成Jenkins的slave节点,提升资源利用率。
  • 采用定期备份配置。
  • 采用Jenkinsfile进行代码化配置。

利用Kubernetes插件让Jenkins master/slave跑到kubernetes

采用Jenkinsfile进行代码化配置 Pipeline as Code

Jenkinsfile (Declarative Pipeline)
pipeline {
    agent any

    stages {
        stage('Build') {
            steps {
                echo 'Building..'
            }
        }
        stage('Test') {
            steps {
                echo 'Testing..'
            }
        }
        stage('Deploy') {
            steps {
                echo 'Deploying....'
            }
        }
    }
}

流水线的集中化管理

新的问题

当我们陆续把Ci都搬到Jenkins之后,由于有几百个代码工程,发现有几个比较麻烦的的地方:配置起来比较麻烦,经常需要在Jenkins和代码仓库上同步配置;另外,像对所有代码工程做一个共性调整,需要逐个处理,比较费劲容易遗漏。

总结一下,就2个点:解决关联配置,实现集中化管理Jenkinsfile。

解决关联配置

思路也比较简单,基于Jenkins API和Gitlab API是可以通过程序来维护配置的。

实现集中化管理Jenkinsfile

借助Pipeline: Multibranch with defaults插件实现统一的初始化入口。

目前的Jenkinsfile管理方案

上图其实把两个流程合在一起的了,下面这个图应该更好理解一些。

完整的流程分为两块:

  • 对于管理员来说,所有项目的配置是放在一个单独工程里边的,它本身的CI也是托管在jenkins中,相应的CI动作就是把所有工程的Jenkins/Gitlab配置进行同步。
  • 对于开发人员来说,提交代码后触发Jenkins,相应的CI动作就是把配置工程拉下来,从中取出对应代码工程的Jenkinsfile配置,然后load进来执行。

除了Jenkins/Gitlab API参考官网,其他还有一些细节。

如何维护相应项目的Jenkins Job和Gitlab Hooks

根据上面描述,主流程的伪代码是这样的:

for dir in Jenkinsfile目录
    判断目录下是否有config.properties和Jenkinsfile.groovy文件,有则继续
    读取config.properties得到相关配置信息
    进行Jenkins Job配置同步:
        生成Job配置,用于判断是否发生变更
        获取目前的Job配置,如果发生变更或没有配置,则进行同步(增加或更新)
    进行Gitlab Hooks配置同步:
        生成Hooks配置,用于判断是否发生变更
        获取目前的Hooks配置(列表),判断Hook是否在配置中(通过url)或者发生变更或者没配置,则进行同步(增加或更新)
        对于MR Hook配置,如果不需要MR则要删除Hooks配置中关于MR的配置
        对于选择周期构建的配置,不创建Hook,并删除相关Hooks配置
end

这里主要会涉及到Jenkins和Gitalb api的调用,可以参考

  • Jenkins API https://wiki.jenkins.io/display/JENKINS/Remote+access+API
  • Gitlab API https://gitlab.huawei.com/help/api/README.md

应该设置哪些WebHook?

当前基于Pipeline: Multibranch来实现多分支流水线,还设置了一个专门的任务用于所有工程的MR操作。

http://whereisyourjenkins/project/common_mr 触发MR构建
http://whereisyourjenkins/git/notifyCommit?url=ssh://xxxx/groupA/projA.git&delay=0sec 触发分支构建

初始化入口是怎样的?

通过在Manage Jenkins - Managed Files增加一个Jenkinfile的groovy脚本,大体逻辑如下,当然对于MR和分支是略有差异的,需要根据gitlab插件传递过来的环境变量的区分。

git url:"ssh://xxx/pipeline.git", branch: …
def check_groovy_file="Jenkinsfile/${job_name}/Jenkinsfile.groovy"
load "${check_groovy_file}"

这里有几点差异:

  • 对于MR任务,是手工配置,执行的Jenkinsfile可以指向配置工程中的某个Jenkinsfile,便于管理;对于分支任务,要统一配置只能采用Managed Files这种模式。
  • load实际上是由Jenkins执行groovy脚本的沙箱限制的(在设置中有个"Run default Jenkinsfile within Groovy sandbox"的选项),所以在load中有些方法是不能调用的。

集中配置的目录怎么组织?

这本身也是一个代码工程,都是托管在代码仓库上的,可以参考代码仓库路径进行组织。

- pipeline
  - Jenkinsfile
    - groupA
       - projA
         - config.properties
         - Jenkinsfile.groovy
       - projB
         - config.properties
         - Jenkinsfile.groovy

这里有两个关键文件。首先是config.properties作为项目的配置,用来设置仓库路径,分支,有没有MR,保存多久等等。看项目需要了,用于配置工程读取后同步修改Jenkins和Gitlab的配置。

另外一个文件是Jenkinsfile.groovy,其实就是原来的Jenkinsfile,它内部也是可以调用load方法,这样你可以把一些公共的东西抽取到一个统一的地方,简化配置的同时也能方便统一修改。

#!/usr/bin/env groovy
def common = load "Jenkinsfile/common.groovy"

common.buildKubernetes({
    stage('Checkout') {
        common.git_clone()
    }

    stage('Build') {
        sh "mvn -U clean ${common.is_mr() ? 'package' : 'deploy'}"
    }
})

可能的内存泄露问题

jenkins存在一些内存泄露问题的,特别是在复杂环境采用Jenkinsfile进行脚本构建的情况下。目前通过增加个空的POD来拉起实际的工程,实测可以稳定一些。

podTemplate(
        cloud: 'kubernetes',
        label: 'assistant',
        idleMinutes: 30,
) {
    timeout(time: 3600, unit: 'SECONDS') {
        node('assistant') {
            ...
            load ".../Jenkinsfile.groovy"
        }
    }
}

脚本的集中化管理

新的想法

代码和配置脚本还是有一些差异点:

  • 脚本没有和代码仓库一一对应,处理的内容是可以代码,也可以是其他。
  • 没有Hook触发的场景,更多是定时处理。
  • 通常情况,对资源的要求没有代码构建要求高。

希望提供更多的Jenkins Job定制要求,再统一管理的基础上实现灵活定制Job?

目前的Jenkins脚本管理方案

这里主要是通过Job DSL 采用代码方式来配置Job,然后在修改配置工程的时候,遍历某个目录把任务都生成一遍。这里也有一些细节可以讨论一下。

配置的目录怎么组织?

- pipeline
  - Watchdog
    - job1
       - job.groovy
       - Jenkinsfile.groovy
    - job2
       - job.groovy
       - Jenkinsfile.groovy

和上面的目录有点类似,对每个目录,job.groovy就是任务对应配置,同样调用Job DSL plugin用来生成任务,并把任务对应流水线设置成Jenkinsfile.groovy。而Jenkinsfile.groovy就是实际的任务操作内容了。

种子工程是怎样的?

init-dsl-job是用来设置种子工程的,这是基于Job DSL plugin的要求。和上面的mr工程类似,它也可以指定到配置工程中的某个具体Jenkinsfile,便于管理。

总体来说,这里坑比较多,首先需要用到一些Jenkins内部的API,还有就是一些安全限制和BUG规避等,可以参考下面这段模板。

/**
 * 1.创建freestyle任务init-dsl-job
 * 2.设置Source Code Management为git,设置url
 * 3.Build阶段增加Process Job DSLs,设置Look on Filesystem的路径是Watchdog/init.groovy
 * 4.设置Advance..选项,Context to use for relative job names选择"Seed Job"
 *
 * 备注: 需要在In-process Script Approval运行该脚本的执行(否则会提示ERROR: script not yet approved for use)
 */
import javaposse.jobdsl.dsl.DslScriptLoader
import javaposse.jobdsl.plugin.JenkinsJobManagement

// 增加一些帮助方法用来规避job dsl支持不好的特性,例如script(readFileFromWorkspace(..))的实现
def helper = '''
def readFile(String filePath) {
    File file = new File("/where is Watchdog dir/init-dsl-job/${filePath}")
    return file.getText("UTF-8")
}
def getAbsoluteFile(String filePath){
    return "/where is Watchdog dir/init-dsl-job/Watchdog/GroovyScripts/${filePath}"
}
def common_definition(String theScriptPath){
    return {
        cpsScm {
            scm {
                git {
                    remote {
                        url("ssh://whereisyourpipeline.git")
                        credentials("hellokey")
                    }
                }
            }
            scriptPath(theScriptPath)
        }
    }
}
def config_logRotator(Map prop) {
    return {
        it / '
properties' / 'jenkins.model.BuildDiscarderProperty' {
            strategy {
                '
daysToKeep'(prop.daysToKeep ?: '-1')
                '
numToKeep'(prop.numToKeep ?: '-1')
                '
artifactDaysToKeep'(prop.artifactDaysToKeep ?: '-1')
                '
artifactNumToKeep'(prop.artifactNumToKeep ?: '-1')
            }
        }
    }
}
'
''

def rootDir = "/where is Watchdog dir/init-dsl-job/Watchdog"
def fileList = "find ${rootDir} -type d".execute()
fileList.text.eachLine { fulldir ->
    // 过滤特殊脚本,必须是目录且带有Jenkinsfile.groovy/job.groovy文件的才生成任务
    // Jenkinsfile.groovy 任务执行对应的流水线脚本
    // job.groovy 任务对应配置,通过Job DSL用来生成任务
    def exists = new File("${fulldir}/Jenkinsfile.groovy").exists() &&
            new File("${fulldir}/job.groovy").exists()
    if (!exists) {
        return
    }

    // 生成任务
    println "Load job dsl '${fulldir}/job.groovy'"

    def jobManagement = new JenkinsJobManagement(System.out, [:], ('.' as File))
    def loader = new DslScriptLoader(jobManagement)

    def theJobName = fulldir.substring("/where is Watchdog dir/init-dsl-job".length())
    def theFolderName = theJobName.substring(0, theJobName.lastIndexOf('/'))
    def theScriptPath = "${theJobName.substring(1)}/Jenkinsfile.groovy"

    def scriptText = ("${fulldir}/job.groovy" as File).text

    loader.runScript("""
def theJobName = "
${theJobName}"
def theFolderName = "
${theFolderName}"
def theScriptPath = "
${theScriptPath}"

${helper}

${scriptText}
    "
"")
}

应用情况及经验

其他一些经验,可以供参考。

  • 构建是在k8s容器环境里边的,目前设置是用完直接销毁的,出问题的情况只能看构建日志。一般是可以的,对于特殊情况,才需要进容器调测,可以在Jenkinsfile中增加类似sleep 3600的命令,然后进容器手工执行。完成调测后,再通过界面终止该任务。如果只是简单的MR构建失败,需要重新触发的话,优先使用代码仓库上有流水线重跑的功能。
  • 容器的资源是有限制的,如果对部分大工程来不够的话,例如发现明显变慢或者有失败的情况,得需要调整;对于Java工程来说,失败的情况主要是内存不足导致VM失败,考虑的做法是增加对应的Pod资源。对于前端工程来说,失败的情况主要是内存不足导致Pod被杀,这主要是因为Node没有内存比例设置,只能固定值,这个值也考虑调大。对于一些大工程,需要持续不断的进行优化构建,规模大了之后需要考虑拆包或优化构建的方式。
  • 由于是容器环境,如果内容有做共享。例如maven本地仓库,对于MR场景就不要使用install,并且在出现in epilog non whitespace content is not allowed报错情况需要删除对应的仓库文件。对于node_modules,如果需要排除环境干扰因素时,就需要删除对应缓存文件。如果网络足够快,尽量减少node_modules或者maven本地仓库的共享。
  • 写Jenkinsfile配置需要一些辅助工具,可以通过http://whereisyourjenkins/pipeline-syntax/帮你自动生成;如果对应的写法没有参考,一般可以在Jenkins的pipeline syntex中找到(除非插件没有实现流水线调用)。对于一些直接调用Jenkins编程对象的脚本,调通是非常麻烦的,可以在Jenkins管理页面的console中进行调试。并且要注意有没有ScriptApproval的安全限制。
  • 写Job DSL配置也可以考虑辅助工具,可以通过http://whereisyourjenkins/xmltodsl/帮你自动生成,感觉目前对一些复杂的情况支持不好;
  • 目前有实现静态检查增量门禁、构建缓存等;实现的基础得有共享存储,然后就是根据情况下将结果写入特定位置实现,具体就不详述了。

目前通过这种机制已经比较顺手了,维护的代码工程数百个,月构建数万次。对比一些偏向于界面操作的构建平台,虽然底层的构建可能是通过jenkins来做的,但很多还是传统的页面配置方式。虽然这些配置也是自动生成的,但是对项目来说,定制化能力比较弱,缺乏脚本控制手段,如果只是维护少量项目还行,多了就非常多人工干预。

由于没有特别的诉求,当前我也很久不捣腾这套东西了,如果团队规模不大不小,也没有太多SRE研发力量,这套经验是可以参考参考的。

分类:

后端

标签:

运维部署

作者介绍

小毛
V1