云原生环境下CICD平台构建实践
(一)现状
公司目前推进基础架构的云原生化,其中一个重要工作方向CI/CD,现状是:
- 缺乏统一的CI/CD方法论
- 缺乏统一的CI/CD工具链
- 缺乏统一的CI/CD操作平台
(二)整体方案
结合公司现状,和我们过往的经验,为尽快推进CI/CD落地,目前设计的一个简单方案:
2.1 实现原理
基于下图中的一种CI/CD工作模型
工具链:Git + Kubernetes + Harbor + Helm + Jenkins + TAPD(或自研CI/CD平台);
- 基于GitFlow的代码管理流程;
- 基于GitOps的持续交付模型;
- 基于K8S的基础运行平台;
- 基于Helm 的K8S 部署交付物包管理方式;
- 基于Harbor的Docker Image仓库、Helm Chart仓库
- 基于Jenkins的CI/CD基础服务;
- 基于Jenkins Pipeline 的CI/CD核心流程;
- 基于TAPD(后续增加自研平台)的CI/CD前端操作界面;
2.2 逻辑架构
2.3 拓扑结构

(三)方案实现
3.1 代码管理
在研发流程的代码管理上采用GitFlow流程进行管理。

3.2 交付模型
3.3 Helm Chart管理
将应用部署到K8S的Deployment、StatefulSet等各种配置进行分解抽象,以Helm Chart模板的形式进行管理,新应用只需简单配置一些参数值,即可轻松部署到K8S中。
目前提供的Chart模板包括
- Deployment通用模板
- StatefulSet通用模板
3.4 流水线
将CI/CD的各个操作步骤进行分解与抽象,以流水线(Pipeline)的方式进行标准化,并针对不同开发语言提供对应的流水线模板,并可在该模板基础上进行定制;
可提供的流水线模板包括:
- Java Pipeline;
- PHP Pipeline(开发中);
- Go Pipeline(开发中);
(四)使用方式
4.1 创建Dockerfile及配置Helm Chart模板参数
首先需要在代码根目录下创建
docker目录如果项目为多模块,就需要在
docker目录下创建多个目录,每一个目录名为模块名,如果为单模块,则忽略如果为单模块,则在
docker目录下,创建四个目录,分别是:- dev
- prod
- stage
- test
如果为多模块,就需要在每一个模块下创建以上四个文件目录。以上四个目录,代表着四套不同的环境。
在每一个环境目录下,需要配置两个文件:
- Dockerfile
- values.yaml,用于配置Helm Chart模板参数。参数在最后的详细说明的Helm Charts模板参数章节。
多模块的结构如下:

单模块的结构如下:

4.2 在Jenkins上创建凭据
会创建两个凭据,如下:
- harbor的账号与密码,ID可以指定为
harbor-middleware或者其它特定有意义的ID,如果非harbor-middleware,则需要修改流水线中对应的ID - gitlab的账号与密码,ID不能自定义,会生成全局唯一ID,此ID在从gitlab上拉取代码时会用到。
添加完成后,如下图:

4.3 在Jenkins创建一个流水线
配置如下:
勾选参数化构建过程,添加指定参数。参数在最后的详细说明的Jenkins的流水线参数章节。
在流水线中的
Pipeline script,加入如下的代码,并需要根据每个业务团队需求进行相应调整: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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150def podLabel = "build-${JOB_NAME}-${UUID.randomUUID().toString()}"
pipeline {
agent {
kubernetes {
label podLabel
yaml """
apiVersion: v1
kind: Pod
metadata:
labels:
label: build-agent
spec:
serviceAccount: "jenkins-admin"
imagePullSecrets:
- name: kn-registry
hostAliases:
- ip: "118.25.63.51"
hostnames:
- "jcenter.kn.com"
tolerations:
- key: "node-role.kubernetes.io/devops"
operator: "Exists"
effect: "NoSchedule"
containers:
- name: maven
image: registry-prod.kn.com/middleware/maven-jdk1.8.131:3.6.1
command:
- cat
tty: true
volumeMounts:
- name: maven-cache
mountPath: /root/.m2
- name: docker
image: docker:18.06.3-ce
command:
- cat
tty: true
volumeMounts:
- name: docker-sock
mountPath: /var/run/docker.sock
- name: helm
image: registry-prod.kn.com/middleware/helm:2.10.0
command:
- cat
tty: true
volumes:
- name: docker-sock
hostPath:
path: /var/run/docker.sock
- name: maven-cache
nfs:
path: /
server: 10.10.2.2
"""
}
}
stages {
stage('一: 构建') {
steps {
echo "Pull代码"
git branch: '${branch}', credentialsId: '69082cb5-1578-4e99-bf49-bd8d4496c4f6', url: '${projectGitUrl}'
script {
build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
}
container(name:'maven') {
echo "编译打包"
sh 'mvn clean install -Dmaven.test.skip=true'
script {
projectVersion = sh(returnStdout: true, script: "cat pom.xml | grep '<version>' | head -n 1 | awk -F '/' '{print \$(NF-1)}' | awk -F '>' '{print \$(NF)}' | awk -F '<' '{print \$(1)}'").trim()
bizImage = "registry-prod.kn.com/${team}/${project}/${tier}:${projectVersion}_${branch}_${build_tag}"
}
}
}
}
stage('二: 单元测试') {
steps {
echo "TODO: 代码扫描"
echo "TODO: 单元测试"
echo "TODO: 覆盖率统计"
echo "TODO: 生成报告"
}
}
stage('三: 部署') {
steps {
echo "构建镜像 & Push镜像"
container(name:'docker') {
sh "echo tag = ${build_tag}"
withCredentials([usernamePassword(credentialsId: 'harbor-middleware', passwordVariable: 'harborPassword', usernameVariable: 'harborUsername')]) {
script {
sh "echo ${harborPassword} | docker login -u ${harborUsername} --password-stdin registry-prod.kn.com"
docker.build("${bizImage}", "-f ${k8sConfig}/${environment}/Dockerfile .").push()
}
}
}
echo "部署上线"
container(name:"helm") {
script {
echo "helm init"
sh "helm init --client-only --skip-refresh"
echo "helm repo remove local"
sh "helm repo remove local"
echo "helm repo remove stable"
sh "helm repo remove stable"
echo "helm repo add harbor"
withCredentials([usernamePassword(credentialsId: 'harbor-middleware', passwordVariable: 'harborPassword', usernameVariable: 'harborUsername')]) {
sh "helm repo add --username ${harborUsername} --password ${harborPassword} harbor https://registry-prod.kn.com/chartrepo/${team}"
}
echo "helm repo update"
sh "helm repo update"
echo "执行上线操作"
def exitValue = sh(script: "helm upgrade --wait --timeout=600 --install --set team=${team} --set project=${project} --set tier=${tier} --set environment=${environment} --set deployImage=${bizImage} -f ${k8sConfig}/${environment}/values.yaml --namespace=${team}-${environment} ${tier}-${environment} --version=${helmChartVersion} harbor/${helmChartName}", returnStatus: true)
if (exitValue != 0) {
echo "上线失败,回滚"
sh "helm rollback ${tier}-${environment} 0"
}
}
}
}
}
stage('四: 集成测试') {
steps {
echo "自动化测试"
}
}
}
post {
always {
echo '清理临时文件'
}
success {
echo '系统上线成功!'
}
failure {
echo '系统上线失败!'
}
}
}
该流水线主要包含两个部分:
- 在k8s环境中,创建用于执行CI/CD的Pod。针对Java,在该Pod中现阶段主要包含三个容器: maven、docker、helm。maven用于编译,docker用于镜像打包及推送,helm用于执行上线操作
- 执行CICD操作,主要包含四个阶段:
- 构建,主要有三个部分,第一个部分用于从git仓库中拉取代码;第二部分用于从代码中解析出版本号;第三部分则是编译项目,各个项目需要自行调整编译参数
- 单元测试
- 部署,包含两个部分:镜像推送和通过helm chart模板在k8s中创建应用
- 集成测试
需要调整的部分如下:
- gitlab的访问凭证ID,用于拉取代码
- harbor的访问凭证ID,用于镜像的推送
- 镜像仓库地址
- 编译参数
- 针对非java环境,需要调整Pod的创建,增加新的容器
4.4 TAPD与Jenkins进行关联
- 在TAPD获取项目的项目ID,如下图:

- 将TAPD与Jenkins进行关联,如下图:

到此,所有配置完成。完成配置后,即在TAPD上的流水线执行CI/CD操作。如下图:

运行完成后,如下图:

(五)详细说明
5.1 Jenkins的流水线参数
| 参数名 | 类型 | 类别 | 描述 |
|---|---|---|---|
| team | 字符参数 | 基本信息 | 团队名,英文 |
| project | 字符参数 | 基本信息 | 项目名,英文 |
| tier | 字符参数 | 基本信息 | 模板名,英文 |
| projectGitUrl | 字符参数 | 代码信息 | 项目的gitlab地址 |
| branch | 字符参数 | 代码信息 | gitlab分支 |
| helmChartName | 字符参数 | helm配置 | helm模板名称,现在支持deployment-common、statefulset-common |
| helmChartVersion | 字符参数 | helm 配置 | helm模板版本,现在的支持的版本号为1.0.0、1.0.1、1.0.2、1.0.3 |
| k8sConfig | 字符参数 | helm配置 | k8s的配置文件目录及Dockerfile文件 |
| environment | 选项参数 | 运行环境 | 环境列表,取值为prod、stage、dev、test |
5.2 提供的Helm Chart模板
现阶段中间件组提供两个Helm Chart 模板,分别是:
deployment-common
提供基于无状态服务的公共模板,支持ingress、service、deployment的创建
statefulset-common
提供基于有状态服务的公共模板,支持ingress、service、statefulset的创建
ChangeLog如下:
- 1.0.0
- 1.0.1,添加了针对环境变量的支持
- 1.0.2,修复了statefulset滚动更新的异常
- 1.0.3,添加了对于command和args的支持
- 1.0.4,修改了关于prometheus的BUG
5.3 Helm Charts模板参数
deployment-common和statefulset-common的模板参数是一致的。
| 字段Key | 是否必填 | 默认值 | 类别 | 描述 |
|---|---|---|---|---|
| team | 是 | 基本信息 | 团队名,英文名 | |
| project | 是 | 基本信息 | ||
| tier | 是 | 基本信息 | 项目的模板名,英文名 | |
| environment | 是 | 运行环境 | 环境,有四个选项,test、prod、stage(提供给其它组使用的环境)、dev | |
| harborHost | 是 | harbor配置 | harbor仓库的host,不需要带上http或者https。现在测试环境为registry.kn.com,线上为registry-prod.kn.com | |
| harborUsername | 是 | harbor仓库的用户名 | harbor配置 | |
| harborPassword | 是 | harbor仓库的密码 | harbor配置 | |
| deployReplicas | 是 | deployment配置 | ||
| deployImage | 是 | deployment配置 | ||
| deployCommand | 否 | deployment配置 | 运行命令,数组 | |
| deployArgs | 否 | deployment配置 | 运行参数,数组 | |
| deployContainerPort | 是 | deployment配置 | 项目的服务端口号 | |
| deployProbeEnable | 否 | false | deployment配置 | 是否开启探针,探针包含两个部分,read探针和live探针 |
| deployProbeURI | 否 | deployment配置 | 如果deployProbeEnable为true,则需要配置探针的uri | |
| deployLiveProbeInitialDelaySeconds | 否 | 120 | deployment配置 | 如果deployProbeEnable为true,用于配置live探针第一开始探测的延迟 |
| deployLiveProbeInterval | 否 | 5 | deployment配置 | 如果deployProbeEnable为true,用于配置live探针的探测间隔 |
| deployReadProbeInitialDelaySeconds | 否 | 120 | deployment配置 | 如果deployProbeEnable为true,用于配置read探针第一开始探测的延迟 |
| deployReadProbeInterval | 否 | 5 | deployment配置 | 如果deployProbeEnable为true,用于配置read探针的探测间隔 |
| deployResoucesLimits | 否 | requests: {cpu: 1,memory: 1G }, limits: { cpu: 2, memory: 2G } | deployment配置 | 设置每一个副本的资源限制 |
| logCollectorEnable | 否 | 是否开启sidecar的日志收集功能 | ||
| logCollectorImage | 否 | 当logCollectorEnable为true时,用于设置sidecar的镜像 | ||
| envs | 否 | 设置环境变量,例如: aa: 123 | ||
| logDirType | 否 | empty | 日志目录 | 挂载目录类型,有三种类型,empty,host,pvc |
| logDirSize | 否 | 日志目录 | 仅仅针对logDirType的类型为pvc时有效,设置pvc大小 | |
| logDirTargetPath | 否 | /data/logs | 日志目录 | 挂载到容器中的目录位置 |
| prometheusCollectEnable | 否 | false | prometheus采集 | 是否开启允许prometheus采集数据 |
| prometheusCollectMetricPort | 否 | prometheus采集 | 当prometheusCollectEnable为true时生效,用于设置prometheus采集数据时,容器的数据端口 | |
| promethuesCollectMetricUri | 否 | prometheus采集 | 当prometheusCollectEnable为true时生效,用于设置prometheus采集数据时,获取数据的uri | |
| tolerationEnable | 否 | false | k8s的容忍与污点 | 是否开启容忍机制,仅仅在线上环境需要开启,在测试环境不用开启 |
| tolerationKey | 否 | k8s的容忍与污点 | 当tolerationEnable为true时生效,用于设置容忍的key | |
| serviceEnable | 否 | true | k8s的service配置 | 是否需要创建k8s中的service |
| serviceSessionAffinityEnable | 否 | false | k8s的service配置 | 当serviceEnable为true时生效,用于设置是否开启session粘连功能,开启后,默认根据ClientIP进行粘连 |
| serviceLoadBalancerEnable | 否 | false | k8s的service配置 | 是否将service的类型设置为loadbalancer,否则为clusterIp |
| serviceSubnetid | 否 | k8s的service配置 | 针对serviceLoadBalancerEnable为true时,设置内网的id(找运维获取),线上和测试环境不是一样的 | |
| ingressEnabled | 否 | false | k8s的ingress配置 | 是否需要ingress |
| ingressType | 否 | ingress-nginx | k8s的ingress配置 | ingress的实现类型 |
| ingressHost | 否 | false | k8s的ingress配置 | 当ingressEnabled为true时生效,用于设置ingress匹配的host |
| ingressTlsEnable | 否 | false | k8s的ingress配置 | 当ingressEnabled为true时生效,用于设置ingress是否开启https |
| ingressSessionAffinityEnable | 否 | false | k8s的ingress配置 | 是否开启ingress的session亲近性 |
| ingressSessionAffinityMode | 否 | balanced | k8s的ingress配置 | ingress的session亲和性是根据cookie实现,此处定义session的生成方式,支持两种方式: balanced 和 persistent,balanced会根据pod的伸缩进行cookie调整,而persistent总会等到cookie过期后才会进行调整 |
| ingressSessionAffinityCookieName | 否 | INGRESSCOOKIE | k8s的ingress配置 | ingress的session亲和性是根据cookie实现,需要定义一个cookie名 |
| ingressSessionAffinityCookieTtl | 是 | k8s的ingress配置 | ingress的session亲和性是根据cookie实现,需要定义一个cookie的最大保存时间,单位秒 | |
| ingressTlsCrt | 否 | false | k8s的ingress配置 | 当ingressTlsEnalbe为ture时有效,用于设置https证书的crt数据,需要base64加密 |
| ingressTlsKey | 否 | false | k8s的ingress配置 | 当ingressTlsEnalbe为ture时有效,用于设置https证书的key数据,需要base64加密 |
| ingressAuthVerifyClientEnable | 否 | false | k8s的ingress配置 | 当ingressTlsEnalbe为ture时有效,是否开启客户端的认证(即只允许公司内部的员工才能访问,双向认证) |
| ingressAuthVerifyClientCaData | 否 | k8s的ingress配置 | 当ingressTlsEnalbe为ture,并且ingressAuthVerifyClient也为true时有效,用于配置根证书,数据为base64加密后的数据 |
5.4 Jenkins地址
https://k8s-test-jenkins.kn.com/,测试环境的k8s的jenkinshttp://jenkins-prod.kn.com/,线上k8s的jenkins
5.5 Harbor地址
线上的k8s集群不能访问测试环境的harbor仓库
5.6 注意事项
获取代码仓库的版本号
如果为Java项目的话,可以通过如下命令进行获取。
1
cat pom.xml | grep '<version>' | head -n 1 | awk -F '/' '{print \$(NF-1)}' | awk -F '>' '{print \$(NF)}' | awk -F '<' '{print \$(1)}'
容忍与污点
在线上环境中,需要开启容忍与污点机制,每一个小组的k8s节点是相互隔离,每个小组的资源只能负载到对应的节点上去。而在测试环境则无需要开启容忍与污点机制。
网络
在创建k8s的LoadBalance的Service时,默认会使用腾讯云的CLB,并生成一个外网IP。外网IP会收费。所以在创建内网的LoadBalance的Service时,需要添加一个
annotaion,如下:1
service.kubernetes.io/qcloud-loadbalancer-internal-subnetid:
这个key的现阶段取值有两个:
3a6sat1b,线上内网1lj7kqdt,测试环境内网
