Kubernetes CICD工具Jenkins Pipeline

持续构建与发布是我们日常工作中必不可少的一个步骤,目前大多公司都采用 Jenkins 集群来搭建符合需求的 CI/CD 流程,然而传统的 Jenkins Slave 一主多从方式会存在一些痛点,比如:

  • 主 Master 发生单点故障时,整个流程都不可用了
  • 每个 Slave 的配置环境不一样,来完成不同语言的编译打包等操作,但是这些差异化的配置导致管理起来非常不方便,维护起来也是比较费劲
  • 资源分配不均衡,有的 Slave 要运行的 job 出现排队等待,而有的 Slave 处于空闲状态
  • 资源有浪费,每台 Slave 可能是物理机或者虚拟机,当 Slave 处于空闲状态时,也不会完全释放掉资源。

正因为上面的这些种种痛点,我们渴望一种更高效更可靠的方式来完成这个 CI/CD 流程,而 Docker 虚拟化容器技术能很好的解决这个痛点,又特别是在 Kubernetes 集群环境下面能够更好来解决上面的问题,下图是基于 Kubernetes 搭建 Jenkins 集群的简单示意图:

从图上可以看到 Jenkins Master 和 Jenkins Slave 以 Pod 形式运行在 Kubernetes 集群的 Node 上,Master 运行在其中一个节点,并且将其配置数据存储到一个 Volume 上去,Slave 运行在各个节点上,并且它不是一直处于运行状态,它会按照需求动态的创建并自动删除。

这种方式的工作流程大致为:当 Jenkins Master 接受到 Build 请求时,会根据配置的 Label 动态创建一个运行在 Pod 中的 Jenkins Slave 并注册到 Master 上,当运行完 Job 后,这个 Slave 会被注销并且这个 Pod 也会自动删除,恢复到最初状态。

那么我们使用这种方式带来了哪些好处呢?

  • 服务高可用,当 Jenkins Master 出现故障时,Kubernetes 会自动创建一个新的 Jenkins Master 容器,并且将 Volume 分配给新创建的容器,保证数据不丢失,从而达到集群服务高可用。
  • 动态伸缩,合理使用资源,每次运行 Job 时,会自动创建一个 Jenkins Slave,Job 完成后,Slave 自动注销并删除容器,资源自动释放,而且 Kubernetes 会根据每个资源的使用情况,动态分配 Slave 到空闲的节点上创建,降低出现因某节点资源利用率高,还排队等待在该节点的情况。
  • 扩展性好,当 Kubernetes 集群的资源严重不足而导致 Job 排队等待时,可以很容易的添加一个 Kubernetes Node 到集群中,从而实现扩展。 当然这也是 Kubernetes 集群本来的便捷性。

Jenkins Pipeline

Jenkins流水线是一套插件,它支持实现和集成持续交付流水线到Jenkins中。

Jenkinsfile一般有几个配置管理方式:

  1. 在Jenkins WebUI中配置管理
  2. 检入到源码管理系统中配置管理(推荐方式)

Jenkinsfile: 创建一个检入到源码管理系统中的Jenkinsfile带来了一些直接的好处:

  1. 流水线上的代码评审/迭代
  2. 对流水线进行审计跟踪
  3. 流水线的单一可信数据源,能够被项目的多个成员查看和编辑

流水线支持 两种语法:声明式(在 Pipeline 2.5 引入)和脚本式流水线。官方文档

Jenkins安装

1
2
3
4
5
6
helm fetch stable/jenkins -version 2.1.2
tar -zxf jenkins-2.1.2.tgz
vim jenkins/values.yaml
helm install --name jenkins -f values.yaml . --namespace devops
helm upgrade jenkins -f values.yaml . --namespace devops
helm delete --purge jenkins

https://github.com/helm/charts/tree/master/stable/jenkins#200-configuration-as-code-now-default--container-does-not-run-as-root-anymore

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
# values.yaml
clusterZone: "cluster.local"
master:
numExecutors: 2 # 允许在master节点上同时执行2个任务
# https://github.com/helm/charts/tree/master/stable/jenkins#200-configuration-as-code-now-default--container-does-not-run-as-root-anymore
enableXmlConfig: true # 允许变更配置 -> <your-jenkins-ingress>/configureSecurity此url下设置`安全域`为`Jenkins专有用户数据库`
resources:
requests:
cpu: "50m"
memory: "256Mi"
limits:
cpu: "1000m"
memory: "1024Mi"
installPlugins:
- kubernetes:1.25.7
- workflow-job:2.39
- workflow-aggregator:2.6
- credentials-binding:1.23
- git:4.2.2
- configuration-as-code:1.41
- blueocean:1.23.2
- git-parameter:0.9.12
- localization-zh-cn:1.0.17
ingress:
enabled: true
hostName: jenkins.boer.xyz
agent:
enabled: false # 我们在pipeline中自定义Agent Pod
persistence:
enabled: true
storageClass: openebs-hostpath
accessMode: "ReadWriteOnce"
size: "2Gi"

必备Plugins:

  • kubernetes:1.25.7
  • workflow-job:2.39
  • workflow-aggregator:2.6
  • credentials-binding:1.23
  • git:4.2.2
  • configuration-as-code:1.41
  • blueocean:1.23.2
  • git-parameter:0.9.12
  • localization-zh-cn:1.0.17

Jenkins配置操作

全局安全配置

系统管理 -> 全局安全配置 -> Authentication -> 安全域 -> Jenkins专有用户数据库

auth

添加全局凭证

系统管理 -> Manage Credentials -> Stores scoped to Jenkins -> Jenkins -> 全局凭据 (unrestricted) -> 添加凭据

1. 添加代码仓库凭证

key-gitea

2. 添加Harbor Registry凭证
  • 方法同添加代码仓库凭证
  • 添加harbor统一镜像拉取账号 参考
3. 添加kubeconfig凭证

key-kubeconfig

KubernetesPod.yaml

划重点

  1. maven缓存.m2
  2. docker in docker
  3. jnlp容器必须有,command不能覆盖jenkins-slave
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
---
apiVersion: v1
kind: Pod
metadata:
labels:
jenkins-slave: true
spec:
volumes:
- name: maven-cache
hostPath:
path: /var/lib/cache/.m2
- name: docker-sock
hostPath:
path: /var/run/docker.sock
- name: docker-cache
hostPath:
path: /var/lib/docker
containers:
- name: jnlp
image: jenkins/jnlp-slave:3.27-1
tty: true
- name: maven
image: maven:3.6.3-jdk-8
command:
- cat
tty: true
volumeMounts:
- mountPath: /root/.m2
name: maven-cache
- name: docker
image: docker:19.03.8
volumeMounts:
- mountPath: /var/run/docker.sock
name: docker-sock
- mountPath: /var/lib/docker
name: docker-cache
tty: true
command:
- cat
- name: kubectl
image: boer0924/kubectl:1.18.3
tty: true
command:
- cat

Jenkinsfile

声明式Jenkinsfile

划重点

  1. 定义agent label是为在k8s中调度job的pod名字
  2. 定义parameters来选择需要部署的环境。
  3. Jenkinsfile的两个全局变量:env/params。
  • 设置env变量: env.KEY = value
  • 使用env变量: ${KEY}
  1. username&password凭证的使用: registryCre = credentials('registry') [_USR/_PSW]
  • 获取username: ${registryCre_USR}
  • 获取passowrd: ${registryCre_PSW}
  1. 使用short commit_id作为image_tag 和 kubernetes.io/change-cause, 以保证镜像唯一,和可以回退到指定版本。
  2. sed动态修改k8s资源定义文件manifests/k8s.yaml:
  • : 便于指定版本回退
  • : 指定版本
  • : 不同环境不同域名
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
pipeline {
agent {
kubernetes {
label 'jenkins-worker'
defaultContainer 'jnlp'
yamlFile 'manifests/KubernetesPod.yaml'
}
}
parameters {
choice(name: 'ENV', choices: ['test', 'pre', 'prod'], description: '选择部署环境?')
}
environment {
AUTHOR = 'boer'
EMAIL = 'boer0924@gmail.com'
registryUrl = 'registry.boer.xyz'
image = "${registryUrl}/public/spring-consume"
imageTag = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim()
changeCause = sh(script: "git log --oneline -1 HEAD", returnStdout: true).trim()
}
stages {
stage('Test') {
steps {
echo "单元测试"
echo "TEST"
script {
if ("${params.ENV}" == 'test') {
env.NAMESPACE = 'boer-test'
env.INGRESS = 'test.consume.boer.xyz'
}
if ("${params.ENV}" == 'pre') {
env.NAMESPACE = 'boer-pre'
env.INGRESS = 'pre.consume.boer.xyz'
}
if ("${params.ENV}" == 'prod') {
env.NAMESPACE = 'boer-prod'
env.INGRESS = 'consume.boer.xyz'
}
}
}
}

stage('Maven') {
steps {
container('maven') {
echo "编译打包"
sh "mvn clean package -Dmaven.test.skip=true"
}
}
}

stage('Docker') {
environment {
registryCre = credentials('dockerhub')
registryUser = "${registryCre_USR}"
registryPass = "${registryCre_PSW}"
}
steps {
container('docker') {
echo "构建镜像"
sh '''
docker login ${registryUrl} -u ${registryUser} -p ${registryPass}
docker build -t ${image}:${imageTag} .
docker push ${image}:${imageTag}
'''
}
}
}

stage('K8S') {
environment {
kubeconfig = credentials('kubeconfig')
}
steps {
container('kubectl') {
echo "Kubernetes发布"
sh '''
sed -i "s|<CHANGE_CAUSE>|${changeCause}|g" manifests/k8s.yaml
sed -i "s|<IMAGE>|${image}|g" manifests/k8s.yaml
sed -i "s|<IMAGE_TAG>|${imageTag}|g" manifests/k8s.yaml
sed -i "s|<INGRESS>|${INGRESS}|g" manifests/k8s.yaml
kubectl --kubeconfig $kubeconfig apply -f manifests/k8s.yaml -n ${NAMESPACE}
'''
}
}
}

stage('RollOut') {
environment {
kubeconfig = credentials('kubeconfig')
}
input {
id 'ROLLOUT'
message "是否快速回滚?"
ok "确认"
submitter ""
parameters {
choice(name: 'UNDO', choices: ['NO', 'YES'], description: '是否快速回滚?')
}
}
steps {
container('kubectl') {
echo "Kubernetes快速回滚"
script {
if ("${UNDO}" == 'YES') {
sh '''
# 快速回滚 - 回滚到最近版本
kubectl --kubeconfig ${kubeconfig} rollout undo deployment consume-deployment -n ${NAMESPACE}
# 回滚到指定版本
# kubectl -n ${NAMESPACE} rollout undo deployment consume-deployment --to-revision=$(kubectl -n ${NAMESPACE} rollout history deployment consume-deployment | grep ${COMMIT_ID} | awk '{print $1}')
# kubectl -n ${NAMESPACE} rollout status deployment consume-deployment
'''
}
}
}
}
}
}
}

脚本式Jenkinsfile

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
def label = "jenkins-slave"

properties([
parameters([
choice(name: 'ENV', choices: ['test', 'pre', 'prod'], description: '选择部署环境?')
])
])

podTemplate(label: label, containers: [
containerTemplate(name: 'maven', image: 'maven:3.6.3-jdk-8', command: 'cat', ttyEnabled: true),
containerTemplate(name: 'docker', image: 'docker:19.03.8', command: 'cat', ttyEnabled: true),
containerTemplate(name: 'kubectl', image: 'boer0924/kubectl:1.18.3', command: 'cat', ttyEnabled: true)], serviceAccount: 'jenkins', volumes: [
hostPathVolume(mountPath: '/root/.m2', hostPath: '/var/lib/cache/.m2'),
hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock'),
hostPathVolume(mountPath: '/var/lib/docker', hostPath: '/var/lib/docker')
]) {
node(label) {
if ("${params.ENV}" == 'test') {
env.NAMESPACE = 'devops'
env.INGRESS = 'test'
}
if ("${params.ENV}" == 'pre') {
env.NAMESPACE = 'pre'
env.INGRESS = 'pre'
}
if ("${params.ENV}" == 'prod') {
env.NAMESPACE = 'prod'
env.INGRESS = 'prod'
}
def myRepo = checkout scm
def gitCommit = myRepo.GIT_COMMIT
def gitBranch = myRepo.GIT_BRANCH
def imageTag = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim()
def changeCause = sh(script: "git log --oneline -1 HEAD", returnStdout: true).trim()
def dockerRegistryUrl = "registry.boer.xyz"
def imageEndpoint = "public/spring-produce"
def image = "${dockerRegistryUrl}/${imageEndpoint}"

stage('单元测试') {
echo "1.测试阶段"
}
stage('代码编译打包') {
try {
container('maven') {
echo "2. 代码编译打包阶段"
sh "mvn clean package -Dmaven.test.skip=true"
}
} catch (exc) {
println "构建失败 - ${currentBuild.fullDisplayName}"
throw(exc)
}
}
stage('构建 Docker 镜像') {
withCredentials([[$class: 'UsernamePasswordMultiBinding',
credentialsId: 'dockerhub',
usernameVariable: 'DOCKER_HUB_USER',
passwordVariable: 'DOCKER_HUB_PASSWORD']]) {
container('docker') {
echo "3. 构建 Docker 镜像阶段"
sh """
docker login ${dockerRegistryUrl} -u ${DOCKER_HUB_USER} -p ${DOCKER_HUB_PASSWORD}
docker build -t ${image}:${imageTag} .
docker push ${image}:${imageTag}
"""
}
}
}
stage('运行 Kubectl') {
withCredentials([file(credentialsId: 'kubeconfig', variable: 'KUBECONFIG')]) {
container('kubectl') {
sh "mkdir -p ~/.kube && cp ${KUBECONFIG} ~/.kube/config"
echo "查看当前目录"
sh """
sed -i "s|<CHANGE_CAUSE>|${changeCause}|g" manifests/k8s.yaml
sed -i "s|<IMAGE>|${image}|g" manifests/k8s.yaml
sed -i "s|<IMAGE_TAG>|${imageTag}|g" manifests/k8s.yaml
sed -i "s|<INGRESS>|${INGRESS}|g" manifests/k8s.yaml
kubectl apply -f manifests/k8s.yaml --namespace ${NAMESPACE}
"""
}
}
}

stage('快速回滚?') {
withCredentials([file(credentialsId: 'kubeconfig', variable: 'KUBECONFIG')]) {
container('kubectl') {
sh "mkdir -p ~/.kube && cp ${KUBECONFIG} ~/.kube/config"
def userInput = input(
id: 'userInput',
message: '是否需要快速回滚?',
parameters: [
[
$class: 'ChoiceParameterDefinition',
choices: "N\nY",
name: '回滚?'
]
]
)
if (userInput == "Y") {
sh "kubectl rollout undo deployment produce-deployment -n ${NAMESPACE}"
}
}
}
}

}
}

版本回退

划重点
1.根据${COMMIT_ID}找出REVISION (自动)
2.根据commit_msg找出REVISION (人工)

1
2
3
4
5
6
7
8
9
10
11
12
$ kubectl -n ${NAMESPACE} rollout history deployment consume-deployment 
deployment.apps/consume-deployment
REVISION CHANGE-CAUSE
1 a03d0eb optimize change-cause msg
2 73f5a5c resource quota
3 2772a88 resource quota up
4 254b592 plus probe time
5 87989d8 更新配置文件

# 1.根据${COMMIT_ID}找出REVISION(自动) 2.根据commit_msg找出REVISION(人工)
$ kubectl -n ${NAMESPACE} rollout undo deployment consume-deployment --to-revision=$(kubectl -n ${NAMESPACE} rollout history deployment consume-deployment | grep ${COMMIT_ID} | awk '{print $1}')
$ kubectl -n ${NAMESPACE} rollout status deployment consume-deployment

Ref


Kubernetes CICD工具Jenkins Pipeline
https://www.boer.xyz/2020/06/23/k8s-cicd-jenkins-pipeline/
作者
boer
发布于
2020年6月23日
许可协议