云原生Jenkins实践
本文总结了我在工作中在云原生环境下对jenkins的使用,主要介绍了jenkins和kubernetes、docker、helm的结合,并使用一个angular项目和maven项目作为案例,分别展示了前后端的构建流程。
由于整理仓促,涉及到的内容时间跨度较长,难免有些细节不够严谨,仅供参考。
部署Jenkins
按照顺序依次创建以下k8s资源,在集群中快速部署一套比较简单的jenkins环境。
命名空间
apiVersion: v1
kind: Namespace
metadata:
name: jenkins
service account
apiVersion: v1
kind: ServiceAccount
metadata:
name: jenkins
namespace: jenkins
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: jenkins
rules:
- apiGroups: ["extensions", "apps"]
resources: ["deployments"]
verbs: ["create", "delete", "get", "list", "watch", "patch", "update"]
- apiGroups: [""]
resources: ["services"]
verbs: ["create", "delete", "get", "list", "watch", "patch", "update"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["create", "delete", "get", "list", "patch", "update", "watch"]
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create", "delete", "get", "list", "patch", "update", "watch"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: jenkins
namespace: jenkins
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: jenkins
subjects:
- kind: ServiceAccount
name: jenkins
namespace: jenkins
service
apiVersion: v1
kind: Service
metadata:
name: jenkins
namespace: jenkins
labels:
app: jenkins
spec:
selector:
app: jenkins
type: NodePort
ports:
- name: web
port: 8080
targetPort: web
# nodeport方式暴露访问地址
nodePort: 30020
- name: agent
port: 50000
targetPort: agent
pvc
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: jenkins
namespace: jenkins
spec:
# 使用cephfs作为pvc的动态提供者
storageClassName: cephfs
accessModes:
- ReadWriteMany
resources:
requests:
storage: 5G
deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: jenkins
namespace: jenkins
spec:
selector:
matchLabels:
app: jenkins
template:
metadata:
labels:
app: jenkins
spec:
terminationGracePeriodSeconds: 10
serviceAccount: jenkins
# 用initContainer修复pvc数据目录的权限,确保容器进程可写
initContainers:
- name: permission-fixer
image: alpine
command: ["sh", "-c", "chown -R 1000 /cephfs"]
volumeMounts:
- name: jenkinshome
mountPath: /cephfs
containers:
- name: jenkins
image: jenkins/jenkins:lts
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
name: web
protocol: TCP
- containerPort: 50000
name: agent
protocol: TCP
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 500m
memory: 512Mi
livenessProbe:
httpGet:
path: /login
port: 8080
initialDelaySeconds: 60
timeoutSeconds: 5
failureThreshold: 12
readinessProbe:
httpGet:
path: /login
port: 8080
initialDelaySeconds: 60
timeoutSeconds: 5
failureThreshold: 12
volumeMounts:
- name: jenkinshome
subPath: jenkins2
mountPath: /var/jenkins_home
volumes:
- name: jenkinshome
persistentVolumeClaim:
claimName: jenkins
确认jenkins pod状态为running,且探针正常后,访问node节点上的http://node-ip:30020,进入jenkins web控制台。
初始化Jenkins
从pod 标准输出中找到解锁码,输入。
选择默认插件配置即可,开始插件安装过程。
最后设置账号密码信息,完成基本配置登入jenkins控制台。
换源
如果集群所在内网没有配置路由器实现自由访问某些网络资源,可以考虑换源以加速插件下载速度
-
根据jenkins deployment持久化的情况,用shell进入pvc中的jenkins配置目录中,修改
hudson.model.UpdateCenter.xml
<?xml version='1.1' encoding='UTF-8'?> <sites> <site> <id>default</id> <url>http://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json</url> </site> </sites>
-
还需要替换两处
sed -i 's/https:\/\/updates.jenkins.io\/download/http:\/\/mirrors.tuna.tsinghua.edu.cn\/jenkins/g ./updates/default.json sed -i 's/http:\/\/www.google.com/https:\/\/www.baidu.com/g' ./updates/default.json
通用设置
集成k8s
- 目标:实现jenkins调用k8s接口,操作docker、k8s,在动态生成的pod中完成ci、cd流程,最后销毁动态生成的pod。
-
在jenkins中安装k8s插件。
-
选择可选插件,并输入kubernetes搜索,找到如下图叫做kubernetes的插件(安装后在已安装插件列表里名字是kubernetes plugin,可以用简介和版本号进行区分)。
-
等待安装完成。
-
增加slave的集群信息:系统配置拉到页面尾部,在cloud项目下点超链接进入新页面配置集群信息。新增cloud,选择kubernetes。
-
配置jenkins构建用的slave pod所在k8s集群的连接方式:kubernetes地址为集群自带的default命名空间的kubernetes service,直接按图填即可。最后测试连接,确保连接成功。
-
根据jenkins部署的命名空间和service名字填写jenkins地址。
-
增加一个pod template,标签列表需要记住,以后的ci/cd需要增加该标签才会调用该集群进行构建。其他按图填即可,该容器镜像是预配置好的镜像,包含了docker和kubectl客户端程序。
-
增加host path类型的卷,分别为host上的docker socket和kubeconfig
-
填入部署时的sa,使模板pod有权限操作相应的资源
验证动态pod构建
-
jenkins左侧新建item,选择free style,注意标签写入上一步里的标签,进行匹配,方能触发动态构建。
-
增加实际构建时的操作,此处执行shell命令,证明动态构建的pod可以操作docker和k8s。在构建中选择执行shell,输入以下shell命令
echo "测试 Kubernetes 动态生成 jenkins slave" echo "==============docker in docker===========" docker info echo "=============kubectl=============" kubectl get pods
-
保存后,进入该构建的主页面,点击左侧的立即构建,手动触发一次构建。此时观察pod的动态,可以看到新的pod被自动创建、运行、最终被销毁。
-
build完成后,左侧build history中会出现可点击的链接,左侧点击控制台输出可以看到本次构建的标准输出,可以看到动态构建pod成功运行了docker info获取了host上的docker信息,成功用kubectl获取了集群pod信息。
至此完成了jenkins和k8s集成的初步配置。接下来可以开始针对具体的工作任务进行详细的应用和进一步的优化配置。
实际应用
内网及代理配置
- 私有服务尽量使用ip,可避免集群中域名解析的困扰
- 私有服务如果需要用域名解析,可以通过hostAlias修改jenkins pod及jenkins slave pod的hosts
- 如果配置了上述的内部域名,同时配置了访问公网的代理,记得再no_proxy里加入内部域名
插件
- Config File Provider:管理配置文件,并在jenkinsfile中引用,实际构建时会映射到工程目录里
- Pipeline Utility Steps:读取yaml文件,在jenkinsfile中引用。使用该插件可以方便地整合helm,实现以helm chart的values.yaml为唯一的配置文件
构建项目配置
本文构建项目以流水线类型的项目为主,并可以建立文件夹,进行分类管理。
在流水线类型的项目中,主要需要关注的几个点如下:
-
源代码仓库相关:先在系统管理-安全-credentials里保存git用户名和密码
-
构建参数:勾选参数化构建,可以设定构建时的输入参数。例如:
-
分支:在代码仓库配置中使用
*/${branch}
引用该参数<img src="https://cdn.wubw.fun/typora/211201-101428-image-20211201101428440.png" alt="image-20211201101428440" style="zoom:50%;" />
- 命名空间:同1,可以设定namespace参数,使用户可以指定部署到哪个命名空间
-
jenkinsfile编写
经过众多配置,终于来到了核心的jenkinsfile的编写。本文展示我对jenkinsfile应用的几个案例,以供学习参考。
由于slave pod运行在k8s中,所以通过配置不同的pod template,可以实现对各种各样构建任务的支持。
在本文的集成k8s部分,配置了一个基础的pod template,基于该pod template生成的动态pod使用了cnych/jenkins:jnlp6
镜像,其中包含了kubect和docker客户端,因此在jenkinsfile中就可以使用kubectl和docker完成构建任务。
接下来分别展示前后端构建使用的pod template和jenkinsfile,注意二者需要配合使用。
后端
技术栈:spring->maven->docker image->k8s helm release
pod template
接下来对后端的pod template一些关键点做简要说明:
- 名称随意填,但是标签列表要记下来,随后的jenkinsfile中通过标签引用该pod template生成动态pod用于构建。
- container template会设置两个容器,一个镜像是maven:3.6.3-jdk-8,用于编译java代码生成jar包;另一个容器使用了我自制的镜像
ccr.ccs.tencentyun.com/lwabish/cloud-native-clients
,其中包含了kubect、docker、helm三个客户端,可以将编译好的jar包打包成docker镜像、部署到集群。 - 使用hostPath volume将node上的docker socket和kubeconfig映射给slave pod直接使用。
- 代理的空闲存活时间:如果构建任务比较频繁,默认每次动态pod都销毁,反复重建影响效率。设置合适的存活时间方便pod重用。
- pod retention配置为on failure,在构建出问题时保留slave pod方便查看日志。
- 如果需要对slave pod的yaml文件进行一些额外的配置,而表单里没有,那么可以在raw yaml for the pod中填写,并选择merge。注意yaml层级要写全,不是只写需要的部分。
- 记得给slave pod指定sa。
jenkinsfile
pipeline{
agent{
// 这里对应pod template中的标签列表
label "java"
}
environment {
// 整合helm chart,从chart的values.yaml以及构建参数获取输入,结合一些简单的逻辑确定构建任务的各项环境参数
config=readYaml (file: 'chart/values.yaml')
// 构建参数里的namespace override chart里的namespace
namespace="${namespace=='chart'?config.global.namespace:namespace}"
// harbor仓库直接在chart里写死,通常不变
harbor="${config.global.registry}"
// 构建镜像的tag,和jenkins全局变量绑定,方便排查问题
tag="${branch}-${BUILD_NUMBER}"
// 完整的image tag
image="${harbor}/${JOB_BASE_NAME}:${tag}"
}
stages{
stage('prompt env'){
steps{
container('maven'){
sh """
echo ${namespace}
echo ${harbor}
echo ${tag}
echo ${image}
"""
}
}
}
stage('maven compile'){
steps{
container('maven'){
// config file provider插件中配置的config file id
configFileProvider([configFile(fileId: "040a1234-40c0-466f-acf0-526de4aa8144", targetLocation: "settings.xml")]){
sh """
mvn clean package -U -Dmaven.test.skip=true --settings settings.xml
"""
}
}
}
}
stage('build docker image'){
steps{
withCredentials([[$class: 'UsernamePasswordMultiBinding',
credentialsId: '24d21234-3f88-4648-b354-31c53ca0d0e3',
usernameVariable: 'DOCKER_HUB_USER',
passwordVariable: 'DOCKER_HUB_PASSWORD']]) {
container('clients') {
sh """
echo ${DOCKER_HUB_PASSWORD} | docker login ${harbor} -u ${ DOCKER_HUB_USER } --password-stdin
docker build -t ${image} ./target/ --build-arg=branch=${branch} --build-arg=commit=\$(git rev-parse --short HEAD) --build-arg=build_time=\$(date +'%Y-%m-%dT%H:%M:%S%Z')
docker push ${image}
docker rmi ${image}
"""
}
}
}
}
stage('helm upgrade'){
steps{
container('clients') {
sh """
// helm 不允许release名称有/,替换成-
releaseName=`echo ${JOB_NAME} | sed 's/\\//-/'`
// helm chart中使用appVersion作为镜像tag,此处进行替换
sed -i 's/appVersion: \\S*/appVersion: ${tag}/' ./chart/Chart.yaml
helm upgrade -i -n ${namespace} --create-namespace --set global.registry=${harbor} \${releaseName} ./chart
"""
}
}
}
}
}
大部分和前端部分相同,唯一的区别是maven构建部分,同样使用的也是未采用多阶段构建的dockerfile,并使用config file provider预配置了maven的settings文件,注入到slave pod中,以使用私有的maven仓库。
前端
技术栈:angular->docker image->k8s helm release
pod template
pod templeate整体配置和后端一致,唯一的区别是把maven镜像换成了node镜像。
jenkinsfile
stage('获取依赖'){
steps{
container('node'){
sh """
// yarn 公网代理
yarn config set proxy http://x.x.x.x:3128
yarn config set https-proxy http://x.x.x.x:3128
yarn config set registry https://registry.npm.taobao.org/
yarn config set sass_binary_site https://npm.taobao.org/mirrors/node-sass/
yarn
"""
}
}
}
stage('编译打包'){
steps{
container('node'){
sh """
node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --prod --configuration=production
"""
}
}
}
上面jenkinsfile中仅展示了和后端不一样的几个stage。
多用户及权限管理
安装插件Matrix Authorization Strategy,在系统设置-安全-管理用户中新建用户,在全局安全配置-授权策略中选择安全矩阵,即可以按照用户分配权限。
多任务批量执行
-
安装插件Multijob plugin。
-
系统设置-Maven项目配置-执行者数量默认为0,必须设置成大于0的数量才能开启构建,且大于1时可以支持多pod并发。
-
新建任务,类型选择MultiJob
-
在构建中为增加phase,每个phase下都可以引用多个其他job
-
注意引用job name时要加文件夹前缀
-
参数传递:multi job选择参数化构建,然后在子job的高级按钮中增加predefined paramters,通过${}语法将父job的参数传递给子job,例如:
特别注意:${}里的变量名称不能出现中划线-,会导致变量不解析直接原样传递到子job中
-
并发度控制有多处配置需要考虑:
- 系统配置-maven项目配置-执行者数量
- 系统配置-cloud-Kubernetes集群-容器数量
- 系统配置-cloud-podTemplate-Concurrency Limit
排坑
- 项目配置,scm的轻量级检出要取消勾选
- 快速查询jenkins内置方法及变量:进入某个构建项目,左侧流水线语法
- jenkinsfile有两种书写格式:脚本化流水线和声明式流水线,两种语法有区别,需要统一,不可交叉使用。
- jenkinsfile调试效率不高,可通过以下几个方式尝试改进
- 失败的构建可以在build详情左侧点击回放,快速修改jenkinsfile后快速重试
- 我在使用idea和vscode两种编辑器编辑jenkinsfile时,发现其对groovy的lint等支持较少,可以在vscode中搜索jenkins相关插件,尝试remote lint或者remote debug的方式提高效率。这部分由于我司的jenkinsfile变更不算太多,就没有深入实践。
- 某次升级插件后,遇到了Kubernetes插件的集群配置丢失的问题。因此升级插件前建议做好配置备份。
- 插件更新如果受网络原因影响,也可以下载到本地,通过浏览器手动更新。插件出问题看jenkins pod日志,会提示插件依赖以及相关的插件url,本地下载很方便(前提是本地网络通畅)。
参考
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。