jenkins中基于Shared Libraries和Active Choices插件的容器化项目回滚方案
一、背景描述
- 对平台应用进行容器化改造
- ci/cd 流程基于容器化的改造 本次容器化改造过程也算是对容器化技术的积累进行一次集中的发挥,在三个星期内完成了方案可行性分析、成本预估、方案选型、实际落地等工作,并藉此对 Kustomize 和 Jenkins Shared Libraries 进行了一次最佳实践,这里以回滚方案为例进行简要的分享,其他场景,如基于 java、node、python、golang 语言,各类微服务应用组件、中间件的无状态、有状态服务的容器化部署方案有兴趣,可以联系我共同探讨,邮箱:im_alan@163.com,微信:Alan-pro;
二、案例场景:
- 镜像仓库使用阿里云 PaaS 服务
- 回滚作为一个 jenkins 任务,各服务共享;
- 提供最多 10 个可选版本以供回滚选择(通过镜像 tag 区分版本,tag 中已包含 Build Number、分支/tag、短 git commit id 等关键信息)
- jenkins 任务构建描述添加执行人、服务名、版本等信息
三、方案概述:
- 通过 jenkins pipeline 参数化构建的形式提供回滚功能,提供两个选项参数:1、服务名;2、镜像版本;服务名在 pipeline 中通过 Parameter 关键字以列表的形式传入,jenkins 页面提供下拉框选择(PT_SINGLE_SELECT);镜像版本通过 Active Choices 插件根据选择的服务名通过 script 获取可选的镜像列表并在 jenkins 前端展示一个 RADIO 按钮以便选择;
- 镜像列表获取方式是通过调用阿里云的 API 接口获取
- jenkins 任务调用共享库中的指定函数执行回滚动作;共享库使用得当可以极大地降低 jenkins job 的维护成本;
四、操作流程
4.1 准入条件
这些比较基础、网上样例也多,我们不展开详细说明;
- jenkins 共享库已添加(样例共享库名:demo-shared-lib)
- jenkins 已添加集群 api 操作认证的凭据(样例凭据 id:k8s_auth_docker);
- 已创建阿里云镜像仓库,并且创建了基于 RAM 认证的 AK 供镜像拉取使用
4.2 关键配置、代码展示
- jenkins 部署主机上添加/data/scripts/getImageTag.sh 脚本,作用是用来加载 golang 程序拉取的镜像版本列表(这里有优化空间,应该没必要串两层:golang->shell->groovy,经测试 grooxy 直接调用 golang 程序获取返回值在 Active Choices 插件不能正常显示,所以就这样写了),脚本内容:
/data/scripts/getImageTag -namespace=demo-service -name=$1
cat /tmp/rollback_tag && truncate -s 0 /tmp/rollback_tag
- getImageTag 为 golang 脚本编译生成的可执行程序,代码为:
package main
import (
"encoding/json"
"flag"
//"fmt"
"io/ioutil"
"strings"
"github.com/aliyun/alibaba-cloud-sdk-go/sdk"
"github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests"
//"github.com/emre/golist"
)
func main() {
type Tag struct {
TagName string `json:"tag"`
}
type ImageTag struct {
TagList []Tag `json:"tags"`
}
type ImageData struct {
TagData ImageTag `json:"data"`
}
repoNamespace := flag.String("namespace", "demo-service", "repo namespace")
repoName := flag.String("name", "demo-microservice001", "repo name")
flag.Parse()
client, err := sdk.NewClientWithAccessKey("cn-hangzhou", "<AccessKey ID>", "<AccessKey Secret>")
if err != nil {
panic(err)
}
request := requests.NewCommonRequest()
request.Method = "GET"
request.Scheme = "https" // https | http
request.Domain = "cr.cn-hangzhou.aliyuncs.com"
request.Version = "2016-06-07"
// request.PathPattern = "/repos/demo-service/demo-microservice001/tags"
request.PathPattern = strings.Join([]string{"/repos", *repoNamespace, *repoName, "tags"}, "/")
request.Headers["Content-Type"] = "application/json"
request.QueryParams["Page"] = "1"
request.QueryParams["PageSize"] = "10"
body := `{}`
request.Content = []byte(body)
response, err := client.ProcessCommonRequest(request)
if err != nil {
panic(err)
}
imagetags := &ImageData{}
json.Unmarshal(response.GetHttpContentBytes(), imagetags)
//my_list := golist.New()
var tagListStr string = ""
for _, v := range imagetags.TagData.TagList {
//my_list.Append(v.TagName)
tagListStr = strings.Join([]string{tagListStr, v.TagName}, ",")
//fmt.Println(v.TagName)
}
//fmt.Println(tagListStr[1:])
write_err := ioutil.WriteFile("/tmp/rollback_tag", []byte(tagListStr[1:]), 0644)
if write_err != nil {
panic(write_err)
}
}
- 共享库 demo-shared-lib 的 vars 目录中添加 javaRollback.groovy(主要内容为一个接收 map 类型参数的 call 函数)内容如下:
#!groovy
def call(Map map) {
properties([
parameters([
[$class: 'ChoiceParameter',
choiceType: 'PT_SINGLE_SELECT',
filterLength: 1,
filterable: true,
name: 'servername',
script: [
$class: 'GroovyScript',
fallbackScript: [
classpath: [],
sandbox: false,
script:''
],
script: [
classpath: [],
sandbox: false,
script:
'return["demo-aaa","demo-bbb","demo-ccc","demo-ddd"]'
]
]
],
[$class: 'CascadeChoiceParameter',
choiceType: 'PT_RADIO',
filterLength: 1,
filterable: true,
name: 'tagname',
referencedParameters: 'servername',
script: [
$class: 'GroovyScript',
fallbackScript: [
classpath: [],
sandbox: false,
script:''
],
script: [
classpath: [],
sandbox: false,
script:
'''
def sout = new StringBuffer()
def serr = new StringBuffer()
def proc = ["/data/scripts/getImageTag.sh", servername].execute()
proc.consumeProcessOutput(sout, serr)
proc.waitForOrKill(1000)
return sout.tokenize(",")
'''
]
]
]
])
])
// def call(Map map) {
pipeline {
agent {
label 'slave002'
}
environment {
// map变量传递
namespace="${map.namespace}"
// 自定义变量
IMAGE_HUB="registry-vpc.cn-hangzhou.aliyuncs.com/demo-service"
}
options {
timestamps()
disableConcurrentBuilds()
timeout(time: 10, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '20'))
ansiColor('xterm')
}
stages {
stage ("") {
steps {
// echo "${params.tagname}"
script {
if ("${tagname}") {
try {
configFileProvider([configFile(fileId: "k8s_auth_docker", targetLocation: "docker.kubeconfig")]){
sh '''
kubectl --kubeconfig docker.kubeconfig -n ${namespace} set image deployment/${servername} ${servername}=${IMAGE_HUB}/${servername}:${tagname}
'''
}
} catch(exc) {
config.err "应用回退出错!"
throw(exc)
}
} else {
config.err "镜像版本有误!回退失败"
}
}
}
}
}
post {
success {
script{
wrap([$class: 'BuildUser']){
buildName "#${BUILD_NUMBER}-${BUILD_USER}"
buildDescription "Service:${servername} - Version: ${tagname}"
}
}
}
}
}
}
- jenkins 控制台创建 pipeline 类型的 job,因为非生产环境使用了 namespace 来分环境,这里给 map 传一个 namespace 的参数,实现一个环境一个 job 回滚所有任务的功能(当然也可以把这个参数到构建选项中,这样隔离性较差,根据自己的需求可以自行调整),Pipeline script 填入代码如下:
#!groovy
library "demo-shared-lib"
def map = [:]
map.put('namespace','demo-qa')
javaRollback(map)
- 发布即可,选择服务后,下面会自动加载倒序排列的最新的 10 个可选版本,示例图: