本文来自于Jenkins的实践总结。主要介绍一下工程应用上的实践,记录一下遇到的问题和相应的解决方案。
-
CI/CD与Jenkins
-
Jenkins介绍(扩展性与局限性)
-
Jenkins的扩展性
-
Jenkins的局限性
-
对应解决方案
-
流水线的集中化管理
-
新的问题
-
目前的Jenkinsfile管理方案
-
脚本的集中化管理
-
新的想法
-
目前的Jenkins脚本管理方案
-
应用情况及经验
CI/CD与Jenkins
-
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的扩展性
举例一些重要的插件,这是自己实践中比较有用且重要的插件。
-
Kubernetes 让Jenkins master/slave跑到kubernetes -
LDAP 很多公司是采用LDAP用户认证体系的 -
Warnings Next Generation 收集静态数据进行可视化 -
Publish Over SSH 收集发布件到SSH服务器 -
Email Extension 邮件通知 -
GitLab Plugin 集成Gitlab -
Job DSL 采用代码方式来配置Job -
Pipeline: Multibranch 多分支流水线,可以直接根据分支名生成多个任务 -
Pipeline: Multibranch with defaults 用于配置一个Jenkinsfile初始化入口 -
Hudson global-build-stats plugin 统计信息
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研发力量,这套经验是可以参考参考的。
原文始发于微信公众号(程序员的胡思乱想):Jenkins流水线的另类实践
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/22609.html