当前位置:网站首页>Jenkins中pipeline对接CMDB接口获取主机列表的发布实践原创
Jenkins中pipeline对接CMDB接口获取主机列表的发布实践原创
2022-06-12 19:11:00 【二丫讲梵】
1,前言
发布平台统一项目中,二丫 (opens new window)将八百多个job全部基于Jenkins Pipeline实现,极大简化了项目配置与维护工作,自此之后再没有什么发布上面的难题。
当时宿主机发布的实现,全部基于参数化构建的特性处理集群里的IP列表(事实上在这之后,二丫 (opens new window)增减项目也就只是针对这个字段的调整了),结合ansible进行发布,现在随着各种基建的完善,打算改进一版,直接对接CMDB获取应用的接口拿到应用对应的主机列表来完成发布的实践,从而完成发布平台这最后一点工作量的覆盖,实现全通用,全自动,围绕以CMDB为中心的运维基建工作。
开始正题之前,先看一眼之前已经运行半年左右的实践:
这几个参数满足了二丫 (opens new window)在宿主机发布应用的需求,二丫 (opens new window)基于共享库抽取出Jenkinsfile引导文件,又从Jenkinsfile里抽取出这四个变量,几个都是常规的变量,作用也显而易见,因此不做过多介绍。实现它们的伪代码(实际应用中需要将如下代码中的部分变量进行提取)如下:
parameters {
string(name: 'BRANCH', defaultValue: 'master', description: '请输入将要构建的代码分支')
choice(name: 'REMOTE_HOST', choices: 'ALL\n10.3.7.7\n10.3.7.8', description: '请选择发布主机,默认ALL')
choice(name: 'MODE', choices: ['DEPLOY','ROLLBACK'], description: '请选择发布或者回滚?')
extendedChoice(description: '回滚版本选择,倒序排序,只保留最近十次版本;如果选择发布则忽略此项', multiSelectDelimiter: ',', name: 'ROLLBACK_VERSION', propertyFile: env.JOB_BASE_NAME, propertyKey: env.JOB_BASE_NAME, quoteValue: false, saveJSONParameterToFile: false, type: 'PT_SINGLE_SELECT', visibleItemCount: 10)
}单说REMOTE_HOST,当二丫 (opens new window)把choices: 'ALL\n10.3.7.7\n10.3.7.8'提取成一个参数,那么每个项目的不同主机就可以通过参数传递,然后利用如下代码渲染出ansible需要的主机发布列表:
stage('定义部署主机列表'){
steps{
script{
try{
sh '''
OLD=${IFS}
IFS='\n'
if [ $REMOTE_HOST == "ALL" ];then
echo "[remote]" > ${ANSIBLE_HOSTS}
for i in ${HOSTS};do echo "$i ansible_port=22" >> ${ANSIBLE_HOSTS};done
sed -i '/ALL/d' ${ANSIBLE_HOSTS}
else
echo "[remote]" > ${ANSIBLE_HOSTS}
echo "$REMOTE_HOST ansible_port=34222" >> ${ANSIBLE_HOSTS}
fi
IFS=${OLD}
'''
}catch(exc) {
env.Reason = "定义主机列表出错"
throw(exc)
}
}
}
}那么,现如今想要对接CMDB发布服务,则只需要解决两个事情即可:
- 参数化构建处通过CMDB接口展示主机列表。
- 定义部署主机列表步骤改造成通过接口获取的方式渲染。
2,参数
当二丫 (opens new window)有了明确的目标以及大概的思路之后,凭借着以往网上冲浪时大概看到过有人通过groovy实现通过接口渲染参数的模糊印象,开始了漫长地研究之路。
本文如下代码可能需要你根据报错情况添加插件,具体情况具体分析,这里不多赘述。目前简单记得至少需要如下插件:
- Active Choices Plugin (opens new window):支持灵活定义的参数化插件。
- http request plugin (opens new window):支持创建http请求的插件。
- Pipeline Utility Steps (opens new window):提供pipeline流水线中一些读写文件之类的操作。
找到以往模糊印象中的文章,确定了这里需要使用Active Choices Plugin,通过一些检索,很快确定了基本的发起http request的代码,后边一直困住自己的,是在这个步骤获取项目名字的需求,二丫 (opens new window)需要与CMDB一起约定一个东西作为项目的唯一key,不用说,直接用项目名一定是最好的,这个项目名从gitlab仓库中的命名,到Jenkins中job的命名,到CMDB中应用的命名,乃至整个生命周期中,都应该是一致的,唯一的。
接下来直接上干货。
为了模仿CMDB接口返回应用对应的数据信息,二丫 (opens new window)这里直接在Nginx中添加如下配置:
location /user-api {
default_type application/json;
return 200 '{"name":"user-api","host":["ALL","10.0.0.1","10.0.0.2","10.0.0.3","10.0.0.4"]}';
}当二丫 (opens new window)请求服务对应的 /user-api 接口时,将会拿到应用对应的集群主机IP列表:
然后在Jenkins中,创建一个名叫 user-api 的流水线项目,流水线代码如下:
properties([
parameters([
[$class: 'ChoiceParameter', filterLength: 1, filterable: true, randomName: 'choice-parameter-18463792817640626',
name: 'HOST', choiceType: 'PT_SINGLE_SELECT', description: '选择要部署的主机',
script:[
$class: 'GroovyScript',
fallbackScript: [classpath: [], sandbox: false, script: 'return[\'error\']'],
script: [ classpath: [], sandbox: false,
script:'''import hudson.model.*
import groovy.json.JsonSlurperClassic
def getHostList(jobName) {
def url = new URL("http://10.6.6.66:66/${jobName}")
def parsedJSON = parseJSON(url.getText())
return parsedJSON.host
}
def parseJSON(json) {
return new groovy.json.JsonSlurperClassic().parseText(json)
}
return getHostList(Thread.currentThread().toString().split('job')[-1].replace('/',''))
''']]]])])
pipeline{
agent any
stages{
stage('Example'){
steps{
script{
sh """echo ${env.HOST}"""
}
}
}
}
}关于active choice插件的流水线语法,这里不多介绍,可参考官方文档,也可在你的Jenkins流水线语法中自行生成理解。
说明:因为Jenkins官方对流水线支持的参数化定义仅支持:booleanParam,choice,text,password,file等几种常用参数,并不支持active choice这类复杂的选项定义,因此这里借助properties的方法来实例化任一需要的对象,同样,该用法可以在Jenkins中的流水线语法参考详细了解。示例如下:
使用代码定义active choice 参数时需要注意一个坑是:如果你在一个项目中定义多个active choice参数,那么请确保不同参数的 randomName是唯一的,否则可能会无法正常使用。 接着进入到groovy脚本区域,在写这里的代码时,参考网络上的写法各种各样,先说引包问题。 同一个方法,有人导包有人不导,后来查了下这块儿:Groovy提供一些默认的导入。Groovy默认导入的包有参考 (opens new window):
import java.lang.*
import java.util.*
import java.io.*
import java.net.*
import groovy.lang.*
import groovy.util.*
import java.math.BigInteger
import java.math.BigDecimal
接着是借助URL模块儿,传递唯一参数应用名,拿到数据,return出来,当然这里如果用于生产,还应该再好好设计一下,这里暂时按下不表,在后文再详述。 最后困难的是如果在这个阶段拿到项目名,成了困住二丫 (opens new window)的一个地方,二丫 (opens new window)找遍了网上的资料,试了又试,败了又败,人家说:文章不厌百回改,二丫 (opens new window)说,code不厌百回试。虽然在active choice官方文档处有说明提供了两个变量获取Jenkins构建的系统信息:
也见到有人使用这两个变量来获取的,如下:
import hudson.model.*
def jobName = this.binding.jenkinsProject.name
但经自己的测试,却始终都没拿到。最后还是在Stack Overflow中看到一个答案:
def jobName = (Thread.currentThread().toString() =~ /job\/(.*?)\//)[0][1]如果你所有项目都是单层目录的场景,则用上边的方案没有问题,只不过需要注意的是转义符会报语法错误,可多加一层转义处理:
def jobName = (Thread.currentThread().toString() =~ /job\\/(.*?)\\//)[0][1]不过因为二丫 (opens new window)测试场景是多目录的情况,发现如上方法拿多层级job的时候会有问题,因此个人改造此方法如下:
def jobName = Thread.currentThread().toString().split('job')[-1].replace('/','')现在来到Jenkins当中,在任意目录中创建个项目,命名为 user-api,然后将Jenkinsfile文件丢进去,运行一下,发现能够满足需求:
到这里,参数基于CMDB接口实时拿主机列表的需求已经满足,接下来就是实现通过接口将数据解析下来的功能了。
3,渲染列表
渲染列表也是这次改造工作的重中之重,一开始考虑用shell实现,但是觉得接口拿到的json经由shell处理实在不理想,于是打算用go做个脚本,但是丢个go的二进制放在流水线中,又违背了操作指令应当显式呈现在定义流程的code这一理念,于是暂时放弃go的实现方案,打算转用与流水线更亲和的groovy来实现这个需求,不必觉得难,事实上在实现如下需求之前,二丫 (opens new window)对groovy的编程经验也是零,因此如下的代码实现也会以运维视角来实现,而非纯编程方式,对于将发布流水线写的像编程一个项目那样,二丫 (opens new window)并不喜欢。
但在这里先说一个二丫 (opens new window)后来才知道的事情:在声明式的流水线中,一些场景中对原生的groovy代码会有各种各样的奇怪限制,比如二丫 (opens new window)在实验的过程中就遇到过不让用URL模块儿,不让用write模块儿的,大概报错日志如下:
Scripts not permitted to use staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods getText java.net.URL
如下:
Method definition not expected here. Please define the method at an appropriate place or perhaps try using a block/Closure instead后来才了解到,针对groovy原生语法的一些常见用法,事实上Jenkins pipeline语法中已经提供了相对应的方法进行支持,比如原生的url方法,在pipeline中,应该改用 httpRequest方法,原生的json解析方法,可以改用 readJSON方法,原生的 writer.writeLine方法,可以改用 writeFile方法,凡此种种,不一而足。
所以当二丫 (opens new window)用原生groovy在本地调试成功之后,屁颠屁颠跑到Jenkins上运行时,发现各个步骤都会卡自己一道,最后不得不用Jenkins给定的方法进行了重构改造,不过这里还是放一下groovy原生的处理方式,仅当留念自己奋斗的成果:
import groovy.json.JsonSlurperClassic
def getHostList(jobName) {
def url = new URL("http://10.6.6.66:66/${jobName}")
def parsedJSON = parseJSON(url.getText())
return parsedJSON.host
}
def parseJSON(json) {
return new groovy.json.JsonSlurperClassic().parseText(json)
}
def newFile = new File(${ANSIBLE_HOSTS})
if (!newFile.exists()) {
newFile.createNewFile()
} else {
newFile.withWriter('utf-8') { writer ->
writer.writeLine '[remote]'
}
host = getHostList(${env.JOB_BASE_NAME})
host -= "ALL"
host.each{
println(it)
newFile.append(it + " ansible_port=22\n")
}
}这段代码在本地直接通过groovy解析是没毛病的,放到Jenkins中就不行了。
于是再次借助Jenkins中流水线语法参考,对各个功能点逐个击破,最后完成脚本内容如下:
properties([
parameters([
[$class: 'ChoiceParameter', filterLength: 1, filterable: true, randomName: 'choice-parameter-18463792817640626',
name: 'HOST', choiceType: 'PT_SINGLE_SELECT', description: '选择要部署的主机',
script:[
$class: 'GroovyScript',
fallbackScript: [classpath: [], sandbox: false, script: 'return[\'error\']'],
script: [ classpath: [], sandbox: false,
script:'''import hudson.model.*
def getHostList(jobName) {
def url = new URL("http://10.6.6.66:66/${jobName}")
def parsedJSON = parseJSON(url.getText())
return parsedJSON.host
}
def parseJSON(json) {
return new groovy.json.JsonSlurperClassic().parseText(json)
}
return getHostList(Thread.currentThread().toString().split('job')[-1].replace('/',''))
''']]]])])
pipeline{
agent any
environment {
ANSIBLE_PORT="22" // 定义远程主机ssh端口,一般不需要更改
ANSIBLE_USER="root" // 定义远程主机ssh用户,一般不需要更改
// 定义主机hosts文件,一般不用更改
ANSIBLE_HOSTS="${WORKSPACE}/${env.JOB_BASE_NAME}_hosts"
}
stages{
stage('定义主机列表'){
steps{
script{
try {
if (HOST == 'ALL') {
def response = httpRequest \
httpMode: "GET",
ignoreSslErrors: true,
contentType: 'APPLICATION_JSON',
validResponseCodes: '200',
// requestBody: groovy.json.JsonOutput.toJson(["k1":"v1","k2":"v2"]),
url: "http://10.6.6.66:66/${env.JOB_BASE_NAME}"
println response.content
def props = readJSON text: response.content
props.host -= "ALL"
writeFile file: env.ANSIBLE_HOSTS, text: '[remote]\n'
props.host.each{
appendFile(env.ANSIBLE_HOSTS,it + " ansible_port=${env.ANSIBLE_PORT} ansible_user=${env.ANSIBLE_USER}")
}
} else {
writeFile file: env.ANSIBLE_HOSTS, text: '[remote]\n'
appendFile(env.ANSIBLE_HOSTS,HOST + " ansible_port=${env.ANSIBLE_PORT} ansible_user=${env.ANSIBLE_USER}")
}
}catch(exc) {
env.REASON = "定义主机列表出错"
throw(exc)
}
}
}
}
}
}
// 该方法实现了往文件内追加内容的功能
def appendFile(String fileFullPath, String line) {
if (fileExists(fileFullPath)) {
current = readFile fileFullPath
}
writeFile file: fileFullPath, text: current + line + "\n"
}
说明:这里不一一介绍代码了,捡一些干货聊聊,有话则长,无话则短。
httpRequest在流水线语法中有非常详细的设置项,感兴趣的同学可以去了解查看,这里特地预留了requestBody,是为了二丫 (opens new window)通过该参数与CMDB交互时,能够提供更加灵活的方式,从而让代码更具普适性。readJSON的用法非常简单,让二丫 (opens new window)能够直接解析json串。-=是groovy语法的一种,能够直接删除掉列表中二丫 (opens new window)不需要的元素。.each是一个遍历方法,它会返回一个it对象表示value。writeFile方法默认是覆盖式写文件,通过一个外挂方法,实现追加功能。
4,归纳
经过以上思路的实现之后,二丫 (opens new window)可以来做一些归纳,将一些规范化的东西约定出来,以便于通过同样的配置代码,完成不同的项目发布需求。
唯一key串联
CMDB平台建设期间,应用的模型至少应该有如下定义:
# http://10.6.6.66:66/project?project=test-user-api-runner&env=test
ProjectName 全局唯一
ProjectEnv 应用要有环境的属性
test
pre
prod
HostList 主机列表,其中主机列表应该通过请求参数进行区分通过一些处理从Jenkins项目名中拿到环境以及项目名的参数,实现一套代码,发布多套环境的需求。
保留冗余字段
除了上边提到的三种环境字段,还应该结合自己的实际保留一定的冗余字段,比如因为经常搞活动而需要扩缩容,这个时候就应该再多个场景expand-user-api,自动扩容过程中,通过运维平台购买下来的主机通过这个字段返回,然后复制一个项目将代码同步到扩容的主机上去。
提高CMDB稳定性
当CMDB的应用接口暴漏给发布接入之后,同时对CMDB所在服务的稳定性提高了要求,原来通过单台提供服务的,现在就需要配置成两台高可用起来。
以CMDB为中心
这一举措彻底将发布中心从原来的手动维护转到以CMDB为中心的方案中来,那么如果保障CMDB中的数据可持续性稳定,准确,就是需要从购买机器,扩缩容流程,等逐个变动的场景中来细细规划。
封装一层
如上策略全局推广之后,所有应用的信息全部维护在CMDB中,那么发布就只是一个透明的管道,只需要将应用的参数传递给管道即可触发构建,这个时候,就是适合在Jenkins上封装一层,完全集成到运维平台的时候了。有很多公司一上来就要给Jenkins封装,最后发现要么无法封装成熟,要么就封装出了个新的Jenkins。
本文从想法的萌发,到思路的梳理,到逐步的实现,都做了详细介绍以及过程中的心理描写,运维标准化规范化建设之路非常漫长,需要不断地深耕建设,更需要无数这样的点汇聚起来,从而才能形成一片堂堂汪洋。
本文研究完毕之后,我曾在朋友圈发表感慨一段,也摘录与此:
《技术的乐趣》
你在冲破一个新知识点的过程中,难道不充满曲折,困厄,迷惘,反复么!
你知道自己将要达到何方
你知道自己将要面对多少凶险
你几乎靠近最终答案
你经常与正确答案擦肩
你在被自己的怯弱劝退
你在被自己的勇气鼓励
你终于,会收获那最终的正确答案
而这个答案,足够你开心灿烂好久好久
同时摘录了南宋诗人杨万里的一首诗表意:
《桂源铺》- 杨万里
万山不许一溪奔,
拦得溪声日夜喧。
到得前头山脚尽,
堂堂溪水出前村。
5,参考
- How to get the job name on a groovy dynamic parameter? (opens new window)
- A real example of Jenkins active choices and reactive parameter (opens new window)
- 实战 Groovy: for each 剖析 (opens new window)
- 在Java / Groovy中将数组转换为字符串 (opens new window)
- 如何将文本追加到jenkinsfile中的文件 (opens new window)
- Jenkins Pipeline 实现 http 请求并解析响应 (opens new window)
- Groovy: Method definition not expected here (opens new window)
- 文件操作 (opens new window)
- Groovy 教程 (opens new window)
6,补充
1,参数优化
上边举例中,应用返回的主机列表里还有一个ALL字段,这是应用对应的主机列表所不存在的值,通常应用应该只返回主机列表,所以下边对此处做了简单的改造,利用列表的plus参数,将ALL字段添加上去。
properties([
parameters([
[$class: 'ChoiceParameter', filterLength: 1, filterable: true, randomName: 'choice-parameter-18463792817640626',
name: 'HOST', choiceType: 'PT_SINGLE_SELECT', description: '选择要部署的主机',
script:[
$class: 'GroovyScript',
fallbackScript: [classpath: [], sandbox: false, script: 'return[\'error\']'],
script: [ classpath: [], sandbox: false,
script:'''import hudson.model.*
def getHostList(jobName,jobEnv) {
def url = new URL("http://10.6.6.66:66/${jobName}/${jobEnv}?token=abcdefg")
def parsedJSON = parseJSON(url.getText())
return parsedJSON.host
}
def parseJSON(json) {
return new groovy.json.JsonSlurperClassic().parseText(json)
}
def getProjetName(name) {
def B = name.tokenize('-')
B.remove(0)
return B.join('-')
}
def jobName = Thread.currentThread().toString().split('job')[-1].replace('/','')
def htmp = ["ALL"]
return htmp.plus(getHostList(getProjetName(jobName),jobName.tokenize('-')[0]))
''']]]])])
pipeline{
agent any
environment {
// 定义主机hosts文件,一般不用更改
ANSIBLE_HOSTS="${WORKSPACE}/${env.JOB_BASE_NAME}_hosts"
}
stages{
stage('初始化主机列表'){
steps{
script{
println env.HOST
}
}
}
}
}
// 该方法实现了往文件内追加内容的功能
def appendFile(String fileFullPath, String line) {
if (fileExists(fileFullPath)) {
current = readFile fileFullPath
}
writeFile file: fileFullPath, text: current + line + "\n"
}
def getProjetName(String name) {
def B = name.tokenize('-')
B.remove(0)
return B.join('-')
}边栏推荐
- Design of smart home control system (onenet) based on stm32_ two thousand and twenty-two
- Istio 1.14 release
- [today in history] June 12: the United States entered the era of digital television; Mozilla's original developer was born; 3com merges with American Robotics
- Research Report on the overall scale, major manufacturers, major regions, products and applications of Electric Screwdrivers in the global market in 2022
- ISCC2022
- 3GPP RAN第一次F2F会议,都干了些啥?
- The solution of BiliBili video list name too long and incomplete display
- 基于FPGA的VGA协议实现
- leetcode:5289. 公平分发饼干【看数据范围 + dfs剪枝】
- CVPR 2022 oral Dalian Institute of technology proposed SCI: a fast and powerful low light image enhancement method
猜你喜欢

liunx部署Seata(Nacos版)

Research Report on the overall scale, major manufacturers, major regions, products and application segments of lifeboats in the global market in 2022

Meituan won the first place in fewclue in the small sample learning list! Prompt learning+ self training practice

Redis中的事务

WinCC7.5 SP1调整画面尺寸以适应显示分辨率的方法

leetcode:6097. 替换字符后匹配【set记录 + 相同长度逐一查询】

Leetcodesql: count the number of students in each major

leetcodeSQL:602. Friend application II: who has the most friends

Go package import mode member visibility

5G R17标准冻结,主要讲了些啥?
随机推荐
Research Report on global and Chinese cosmetics industry market sales scale forecast and investment opportunities 2022-2028
[matrix theory & graph theory] final exam review mind map
Leetcode topic [string] - Sword pointing offer 05- replace spaces
Global and Chinese smart government industry market research and investment risk outlook report 2022-2028
六石认知学:大脑的显速与潜速
Go init initialization function
no available service ‘null‘ found, please make sure registry config correct
In 2021, the global fire pump drive power revenue is about $381million, and it is expected to reach $489.3 million in 2028
从应无所住说起
Operational research optimization of meituan intelligent distribution system - Notes
Attack and defense world (WEB) -- supersqli
Analysis report on market demand and investment strategy of China's re guarantee industry 2022-2028
Order allocation strategy for instant delivery: from modeling and Optimization - Notes
uniapp使用阿里图标
RHCA回忆录---CL280介绍
Super heavy! Apache Hudi multimode index optimizes queries up to 30 times
DACOM G150 dual-mode earphones make sound for love and protect the healthy growth of children's hearing
Daily blog - micro service permission 12 matters
What are the third-party software testing organizations in Shanghai that share knowledge about software validation testing?
Wangxuegang room+paging3