一、背景介绍
目前我厂 Jenkins CI 采用的是 Master-Slave 架构, Master 和 Slave 都是物理机搭建。主要用于跑单测,集成测试等。由于早期没有专人来管理 Jenkins ,随着业务的发展 Jenkins Job 越来越多,也带来了如下问题:
二、整体方案设计
为了解决以上问题,减少 Jenkins 维护成本降低机器成本等。我们决定采用现下比较流行的 kubernetes Jenkins CI/CD 技术,将 Jenkins Master 和 Slave 交给 k8s 动态调度。下图是基于 K8s 搭建 Jenkins 集群的简单示意图:
从上图中可以看到 Jenkins Master 和 Jenkins Slave 以 Pod 形式运行在 K8s 集群的 Node 上,Master 运行在其中一个节点,Slave 运行在各个节点上,Slave 的运行将按照需求去动态创建。
工作流程:当调用 Jenkins Master API 发起构建请求时,Jenkins k8s plugin 会根据 Job 配置的 Label 动态创建一个运行在 Pod 中的 Jenkins Slave 并注册到 Master 上,当 Job 结束后,这个 Slave 会被注销并且这个 Pod 也会自动删除,恢复到最初状态,这样集群资源得到充分的利用。
使用容器化和 K8s 动态创建 Slave 优势:
三、部署 Jenkins Master、Sonarqube
3.1 Jenkins Master 部署
由于我们采用 K8s 集群部署,首先得制作 Jenkins Master 镜像。当然也可以使用 Jenkins 官网的上镜像 jenkins/jenkins:lts,因为我们有一些需求,所以需要自己制作。下面是制作镜像中个人认为需要注意的地方:
需要 EXPOSE 2个端口,Jenkins Web 访问端口和 JNLP 代理协议的 TCP 端口( jnlp-slave 连接 Master 使用的端口)。
JNLP 代理协议的 TCP 端口: 由于 Jenkins-Master 是在容器中启动的,所以一定要将这个端口暴露到外部,不然 Jenkins-Master 不知道 Slave 是否已经启动,会反复去创建 Pod 直到超过重试次数。
Jenkins Master 若要动态创建 Slave 需要安装配置 Kubernetes Plugin,这里可以参考 K8S 在有赞 PaaS 测试环境中的实践 里面有介绍,或在网上找资料。
CI/CD 中 Sonarqube 也是必不可少的,用于代码质量管理等。由于 Sonarqube 有一些规则等配置需要在启动时加载好,所以需要重新制作镜像。这里镜像制作分为 2 部分:
这里我们是把 Mysql 和 Sonarqube 集成在一个镜像里,当然也可以分开。下面是制作 Mysql 镜像的部分 Dockerfile:
FROM mysql:5.7
#设置免密登录
ENV MYSQL_ALLOW_EMPTY_PASSWORD yes
#将所需文件放到容器中
COPY mysqld.cnf /etc/mysql/mysql.conf.d/mysqld.cnf
COPY setup.sh /mysql/setup.sh
COPY privileges.sql /mysql/privileges.sql
COPY sonar.sql /mysql/sonar.sql
COPY init_sonar.sql /mysql/init_sonar.sql
COPY run-entrypoint.sh /mysql/run-entrypoint.sh
COPY start.sh /mysql/start.sh
........
#设置容器启动时执行的命令
ENTRYPOINT ["/mysql/run-entrypoint.sh", "/mysql/setup.sh"]
3.3 Jenkins Slave 制作
Jenkins Java Slave 我们参考的官网制作并添加了一些我们自己包(官方提供的 jenkins/ssh-slave,官方文档中有说明,这个镜像安装了 JDK 和 sshd,有兴趣的同学也可以自己制作),其中 Nodejs 、Python Slave 制作和 Java Slave 类似,网上也有资料这里就不详细介绍了。
制作完的镜像需推送到镜像仓库中保存, 下面是构建和推送镜像的命令:
docker build -t [IMAGE:TAG] .
docker tag SOURCE_IMAGE[:TAG] harbor.xxx.com/xxx/IMAGE[:TAG]
docker push harbor.xxx.com/xxx/IMAGE[:TAG]
如上图所示,有需求的同学可以在有赞QA平台发起创建业务线容器,后台会调用 k8s api 创建 Jenkins 、 Sonarqube 容器,并返回访问地址。如下图:
这里我们使用的k8s客户端是fabric8io/kubernetes-client项目,需要在项目的pom 文件中加入kubernetes-client依赖:
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>kubernetes-client</artifactId>
<version>4.1.0</version>
</dependency>
Deployment 为 Pod 和 Replica Set(下一代Replication Controller)提供声明式更新。只需要在 Deployment 中描述你想要的目标状态是什么,Deployment controller 就会帮你将 Pod 和 ReplicaSet 的实际状态改变到您的目标状态。
Service 通过 Label Selector 跟服务中的 Pod 绑定,为 Pod 中的服务类应用提供了一个稳定的访问入口。通过使用 Service,我们就可以不用关心这个服务下面的 Pod 的增加和减少、故障重启等,只需通过 Service 就能够访问到对应服务的容器。
Service 虽然可以 LB, NodePort 对外提供服务,但是当集群服务很多的时候,NodePort 方式最大的缺点是会占用很多集群机器的端口,LB 方式最大的缺点则是每个Service一个 LB 又有点浪费和麻烦,并且需要 K8s 之外的支持, 而 Ingress 则只需要一个 NodePort 或者一个 LB 就可以满足所有 service 对外服务的需求。
注意点:
env:
- name: MY_POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
status.podIP :pod IP
nginx.ingress.kubernetes.io/cors-allow-headers: DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization
nginx.ingress.kubernetes.io/cors-allow-methods: PUT, GET, POST, OPTIONS
nginx.ingress.kubernetes.io/cors-allow-origin: '*'
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/proxy-body-size: 600m
五、K8s Web Terminal
当需要进入容器内执行一些 shell 命令时,web terminal 可以让我们更方便的访问 container,执行 shell 命令,提高工作效率。如下图:
实现:
前端用 xterm.js 库,它是模拟一个 terminal 在浏览器中,此时并没有通讯能力。需要在后端搭建 k8s-websocket 服务。前端建立 websocket,连到后台搭建的 k8s-websocket 服务端。服务端会基于 k8s 的 remotecommand 包,建立与 container 的ssh长连接。我们将输入输出写入到 Websocket 流中即可,当浏览器中 terminal 大小改变了,前端应该把最新的 terminal 大小发给服务端,服务端模拟终端也要相应的 resize。
遇到的问题:
由于我们使用的 kubernetes-client 当时只提供了 pod 启动时,初始化 terminal 大小的功能,未实现 resize 功能。当浏览器中的 terminal 的大小改变时,由于与初始化时传递的行列数不同,导致显示不全或显示区域过小的问题。在查阅资料的过程中发现 k8s 的 remotecommand 实际上是提供了该功能的(详情可见remotecommand.go)。此文件中,定义 resizeChannel 为4,即将发送的命令 byte 数组的第一位修改为4,就可发送 resize 的相关命令。
测试的时候发现 K8s Slave 调度速度比较慢,尤其是多个同类型的 Slave 并行需要等待比较长的时间,上网查询发下默认情况下 Jenkins 保守地生成代理。如果队列中有2个构建,不会立即生成2个执行程序。会产生一个执行器并等待一段时间看第一个执行器有没有被释放,然后再决定产生第二个执行器。以确保产生的每个执行者都得到最大限度的利用。如果要覆盖此行为并立即为队列中的每个构建生成执行程序,可以在 Jenkins Mater 启动时参加一下参数:
总而言之 K8s 博大精深,在 CI/CD 容器化的道路上还有很多知识点需要去学习。