[!jenkins文章分为三部分]

第一部分为docker安装Jenkins与常用插件使用,Jenkins安装

第二部分为运行pipeline并通过k8s pod打包 Jenkins-pipeline使用

第三部分为,pipeline高阶使用,如map,共享库,此文章

1 复杂情况 dockerfile不通用

如果buildDockerImage输入dockerfile,使用输入的dockerfile,输入使用,默认全局的DOCKER_DOCKERFILE_ID
同事,这里加了个map,根据不同应用名配置不通jar包路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
pipeline {
stages {
stage('镜像制作') {
steps {
container('docker') {
script {
// 定义map,根据不同应用名配置不通jar包路径
def jarPath = [
'app1': 'path/to/app1.jar',
'app2': 'path/to/app2.jar',
]
jarPathbuildDockerImage("${jarPath[APP_NAME]}")
}
}
}
}
}
}

def buildDockerImage(dockerfileContent = null) {
if (dockerfileContent) {
writeFile file: 'Dockerfile', text: dockerfileContent
} else {
configFileProvider([configFile(fileId: "${DOCKER_DOCKERFILE_ID}", targetLocation: "dockerfile")]){
// 替换dockerfile文件中的变量
def docker = readFile encoding: "UTF-8", file: "dockerfile"
def dockerfile = docker.replaceAll("#APP_NAME","${APP_NAME}")
.replaceAll("#PROJECT","${PROJECT}")
.replaceAll("#SW_OAP","${SW_OAP}")
.replaceAll("#jarPath","${jarPath}")
// 生成新的 dockerfile 部署文件
writeFile encoding: 'UTF-8', file: './Dockerfile', text: "${dockerfile}"
}
}
sh "cat Dockerfile"
}

2 共享存储库

Done

正常情况有好多pipeline,有些步骤都是重复的,下面简单介绍下如何复用pipeline

2.1 pipeline简单拆分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pipeline {
stages {
stage('拉取代码') {
steps {
script {
checkoutCode()
}
}
}
}
}
// 迁移到共享库只需要checkoutCode为文件名,函数名换成call
def checkoutCode() {
git(credentialsId: "${GIT_ID}", url: "$GIT_URL", branch: "${BRANCH_NAME}", changelog: true, poll: false)
}

2.2 拆分部分放到共享存储库

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
jenkins-shared-libraries/
├── src/ # 公共函数方法,供其他地方调用。
│ └── global.groovy
├── vars/ # 构建流程主逻辑,这里主要用这个
│ └── checkoutCode.groovy
└── (其他如README.md等)

# ${xxx}引用的变量对应pipeline里变量
cat /vars/checkoutCode.groovy
def call() {
git(credentialsId: "${GIT_ID}", url: "$GIT_URL", branch: "${BRANCH_NAME}", changelog: true, poll: false)
}

2.3 使用共享存储库

image.png

pipeline里引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Library('jenkins-shared-libraries@main') _
pipeline {
agent any
parameters {
choice choices: ['app1', 'app2'], description: '选择发布程序', name: 'APP_NAME'
}
options { buildDiscarder(logRotator(numToKeepStr: '10', artifactNumToKeepStr: '5')) }
environment {
BRANCH_NAME = '环境名(分支名)'
GIT_ID = 'git凭据名称'
GIT_URL = "https://git.xxx.com/${APP_NAME}.git(git的url)"
}
stages {
stage('拉取代码') {
steps {
script {
checkoutCode()
}
}
}
}
}

3 多选

Success

有的公司会把所有微服务代码都放到一个仓库,如果每个都进行一次mvn打包时间非常长,这是一个支持一个大仓库根据应用名,打包不同服务的例子

大概内容就是应用名称为目录名,但使用docker部署,不同服务部署到对应节点

需要的插件:

  • Publish Over SSH
  • Extended Choice Parameter
  • Config File Provider
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
pipeline {
parameters {
// 配置多选
extendedChoice name: 'APP_NAME', type: 'PT_CHECKBOX', description: '请勾选所要发布的项目模块', quoteValue: false, saveJSONParameterToFile: false, value: 'schedule,web-township-background', visibleItemCount: 2, multiSelectDelimiter: ',', defaultValue: 'account-server'
}
options { buildDiscarder(logRotator(numToKeepStr: '10', artifactNumToKeepStr: '5')) }
environment {
PROJECT = '项目名'
BRANCH_NAME = '环境名(分支名)'
HARBOR_NAMESPACE = 'docker仓库凭据名称'
GIT_ID = 'git凭据名称'
GIT_URL = "https://git.xxx.com/${APP_NAME}.git(git的url)"
REGISTRY = '镜像仓库地址'
DOCKER_COMPOSE_ID = 'global-docker-compose-file' # docker-compose模板名称
DOCKER_DOCKERFILE_ID = 'global-dockerfile-file' # dockerfile模板
SW_OAP = 'skywalking OPA地址'
ROBOT_ID = '机器人id'
}
agent any
tools {
maven 'maven'
jdk 'jdk8'
}
......
这部分跟其他的一样,省略
......
stages {
stage('镜像制作') {
steps {
script {
// for 上面的多选变量,','分割
for (appName in APP_NAME.tokenize(',')) {
// 按应用名称区分目录,进入到对应的应用目录
dir("${appName}") {
// 进行打包
buildDockerImage(appName)
}
}
}
}
}
stage('更新版本') {
steps {
script {
// 生成字典,分别是应用名,部署节点,应用端口
def App = [
'app1': ['host': 'host-231', 'port': '8080'],
'app2': ['host': 'host-241', 'port': '8081'],
]
for (appName in APP_NAME.tokenize(',')) {
// 根据应用名称与暴露端口生成对应的docker-ompose.yaml
dockerCompose(appName, App[appName]['port'])
// 发送生成的docker-ompose.yaml文件到对应的节点,然后启动
sshDeploy(appName, App[appName]['host'])
}
}
}
}
}
}
// 为了省事,方便理解直接写到下面
// 制作镜像函数
def buildDockerImage(String appName, String port = '') {
// 设置 Docker 镜像名称
def dockerImageName = "${REGISTRY}/${appName}:1.${BUILD_NUMBER}"
if ("${REGISTRY}" == '') {
dockerImageName = "${REGISTRY}/${appName}:1.${BUILD_NUMBER}"
}
// 读取 docker-compose 部署文件
configFileProvider([configFile(fileId: "${DOCKER_COMPOSE_ID}", targetLocation: "docker-compose")]){
// 替换docker-compose文件中的变量
def dockercompose = readFile encoding: "UTF-8", file: "docker-compose"
def dockercomposefile = dockercompose.replaceAll("#APP_NAME","${appName}")
.replaceAll("#APP_IMAGE_NAME","${dockerImageName}")
.replaceAll("#APP_PORT","${port}")
// 生成新的 docker-compose 部署文件
writeFile encoding: 'UTF-8', file: 'docker-compose.yml', text: "${dockercomposefile}"
}
}
// ssh远程更新函数
def sshDeploy(String appName,String hostName) {
sshPublisher(publishers: [sshPublisherDesc(configName: "${hostName}", transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: """
docker-compose -f /data/app/${appName}/docker-compose.yml up -d
""", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: "${appName}/", remoteDirectorySDF: false, removePrefix: '', sourceFiles: 'docker-compose.yml')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
}

4 审批支持

4.1 最简单方式,点击才能继续执行pipeline

1
2
3
4
5
6
7
8
9
10
pipeline {
agent any
stages {
stage('拉取代码') {
steps {
input id: 'approve' , message: '是否同意更新生产环境', ok: 'Yes'
}
}
}
}

4.2 飞书接收验证号,验证码匹配个更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
pipeline {
options {
buildDiscarder(logRotator(numToKeepStr: '10', artifactNumToKeepStr: '5'))
timeout(time:12, unit:'HOURS')
skipDefaultCheckout()
}
environment {
ROBOT_ID = '机器人id'
// 生成随机数
max = 100000
randomToken = "${Math.abs(new Random().nextInt(max+1))}"
}
agent any
stages {
stage('审批') {
steps {
sh 'echo ${randomToken}'
script {
tokenApprove(randomToken)
}
}
}
}
}

def tokenApprove(String randomToken) {
larkrandomToken(env.randomToken)
def isAbort = false //取消按钮
timeout(time:12, unit:'HOURS'){ //等待审批人审批,并通过timeout设置任务过期时间,防止任务永远挂起
try {
def token = input(
id: 'inputap', message: "xx申请更新服务", ok:"同意", , parameters: [
[$class: 'StringParameterDefinition',defaultValue: "", name: 'token',description: '请输入发布的秘钥' ]
])
if ( "${token}" == env.randomToken) {
} else {
error '秘钥错误,任务已终止'
}
}catch(e) { // input false
throw e
}
}
}
def larkrandomToken(String randomToken) {
lark(
robot: "${ROBOT_ID}",
type: "CARD",
title: "📣 xx更新生产环境审批",
text: [
"🔑 **密钥是**: <font color='red'>${randomToken}</font>",
"<at id=all></at>"
],
buttons: [
[
title: "更改记录",
url: "${BUILD_URL}changes"
],
[
title: "审批地址",
type: "danger",
url: "${JOB_DISPLAY_URL}"
]
]
)
}

5 完整配置

可以查看共享库部分源码

pipeline配置,代码整洁多了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
@Library('jenkins-shared-libraries@main') _
//// 配置多选,与单选二选一
//properties([
// parameters([
// [
// $class: 'ChoiceParameter',
// choiceType: 'PT_CHECKBOX', // 复选
// name: 'APP_NAME',
// description: '选择应用',
// script: [
// $class: 'GroovyScript', // Groovy脚本
// script: [
// classpath: [],
// sandbox: true,
// // 要返回的结果,写在一行
// script: "return ['app1', 'app2']"
// ]
// ]
// ]
// ])
// ])
pipeline {
parameters {
choice choices: ['app1', 'app2'], description: '选择发布程序', name: 'APP_NAME'
}
environment {
PROJECT = '项目名'
BRANCH_NAME = '环境名(分支名)'
HARBOR_NAMESPACE = 'docker仓库凭据名称'
GIT_ID = 'git凭据名称'
GIT_URL = "https://git.xxx.com/${APP_NAME}.git(git的url)"
REGISTRY = '镜像仓库地址'
KUBECONFIG_ID = 'kubeconfig id'
KUBERNETES_APP_REPLICAS = 'deployment副本数'
KUBERNETES_DEPLOYMENT_ID = 'global-kubernetes-deployment' # deployment模板名称
DOCKER_DOCKERFILE_ID = 'global-dockerfile-file' # dockerfile模板
SW_OAP = 'skywalking OPA地址'
ROBOT_ID = '机器人id'
max = 100000
randomToken = "${Math.abs(new Random().nextInt(max+1))}"
}
agent {
kubernetes {
inheritFrom 'mypod'
}
}
options {
buildDiscarder(logRotator(numToKeepStr: '10', artifactNumToKeepStr: '5'))
timeout(time:12, unit:'HOURS')
skipDefaultCheckout()
}
stages {
stage('拉取代码') {
steps {
script {
checkoutCode()
}
}
post {
failure {
echo "❌ 代码拉取失败"
failedsendNotification("拉取代码", "❌失败")
}
}
}
stage('编译打包') {
steps {
script {
buildPackage()
}
}
post {
failure {
echo "❌ 打包失败"
failedsendNotification("maven打包", "❌失败")
}
}
}
stage('镜像制作') {
steps {
container('docker') {
script {
// 定义map,根据不同应用名配置不通jar包路径
def jarPath = [
'app1': 'path/to/app1.jar',
'app2': 'path/to/app2.jar',
]
jarPathbuildDockerImage("${jarPath[APP_NAME]}")
}
}
}
post {
failure {
echo " 镜像制作"
failedsendNotification("镜像制作", "❌失败")
}
}
}
stage('审批') {
steps {
sh 'echo ${randomToken}'
script {
tokenApprove(randomToken)
}
}
}
stage('更新版本') {
steps {
script {
deployToKubernetes()
}
}
post {
failure {
echo "❌ 更新版本失败"
failedsendNotification("更新版本", "❌失败")
}
}
}
}
post {
success {
successsendNotification()
}
}
}