目前我厂 Jenkins CI 采用的是 Master-Slave 架构, Master 和 Slave 都是物理机搭建。主要用于跑单测,集成测试等。由于早期没有专人来管理 Jenkins ,随着业务的发展 Jenkins Job 越来越多,也带来了如下问题:
为了解决以上问题,减少 Jenkins 维护成本降低机器成本等。我们决定采用现下比较流行的 kubernetes Jenkins CI/CD 技术,将 Jenkins Master 和 Slave 交给 Kubernetes 动态调度。下图是基于 Kubernetes 搭建 Jenkins 集群的简单示意图:
从上图中可以看到 Jenkins Master 和 Jenkins Slave 以 Pod 形式运行在 Kubernetes 集群的 Node 上,Master 运行在其中一个节点,Slave 运行在各个节点上,Slave 的运行将按照需求去动态创建。
工作流程:当调用 Jenkins Master API 发起构建请求时,Jenkins Kubernetes Plugin 会根据 Job 配置的 Label 动态创建一个运行在 Pod 中的 Jenkins Slave 并注册到 Master 上,当 Job 结束后,这个 Slave 会被注销并且这个 Pod 也会自动删除,恢复到最初状态,这样集群资源得到充分的利用。
使用容器化和 Kubernetes 动态创建 Slave 优势:
由于我们采用 Kubernetes 集群部署,首先得制作 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,这里可以参考《 Kubernetes 在有赞 PaaS 测试环境中的实践 》里面有介绍,或在网上找资料。
CI/CD 中 Sonarqube 也是必不可少的,用于代码质量管理等。由于 Sonarqube 有一些规则等配置需要在启动时加载好,所以需要重新制作镜像。这里镜像制作分为 2 部分:
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"]
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平台发起创建业务线容器,后台会调用 Kubernetes API 创建 Jenkins 、 Sonarqube 容器,并返回访问地址。如下图:
这里我们使用的 Kubernetes 客户端是 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 又有点浪费和麻烦,并且需要 Kubernetes 之外的支持,而 Ingress 则只需要一个 NodePort 或者一个 LB 就可以满足所有 Service 对外服务的需求。
注意点:
Kubernetes 集群中,将图片或是文件上传到文件服务器上,如文件大于 1 M会报错所以 Ingress 的 Annotations 需要配置下"nginx.ingress.kubernetes.io/proxy-body-size", "600m"。
由于 Jenkins Master 容器起来 Kubernetes 插件配置信息都需要初始化好,就需要知道起来后 Pod Node Ip,这里可以通过 Env 来获取 Pod Node IP:
env: - name: MY_POD_IP valueFrom: fieldRef: fieldPath: status.podIP status.podIP :pod IP
前端访问 Jenkins Master 时会存在跨域问题,在 Ingress 中,跨域(CORS)的配置如下:
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
当需要进入容器内执行一些 shell 命令时,web terminal 可以让我们更方便的访问 container,执行 shell 命令,提高工作效率。如下图:
实现:
前端用 xterm.js 库,它是模拟一个 terminal 在浏览器中,此时并没有通讯能力。需要在后端搭建 k8s-websocket 服务。前端建立 websocket,连到后台搭建的 k8s-websocket 服务端。服务端会基于 Kubernetes 的 remotecommand 包,建立与 container 的 SSH 长连接。我们将输入输出写入到 Websocket 流中即可,当浏览器中 terminal 大小改变了,前端应该把最新的 terminal 大小发给服务端,服务端模拟终端也要相应的 resize。
遇到的问题:
由于我们使用的 kubernetes-client 当时只提供了 Pod 启动时,初始化 terminal 大小的功能,未实现 resize 功能。当浏览器中的 terminal 的大小改变时,由于与初始化时传递的行列数不同,导致显示不全或显示区域过小的问题。在查阅资料的过程中发现 Kubernetes 的 remotecommand 实际上是提供了该功能的(详情可见remotecommand.go)。此文件中,定义 resizeChannel 为 4,即将发送的命令 byte 数组的第一位修改为 4,就可发送 resize 的相关命令。
测试的时候发现 Kubernetes Slave 调度速度比较慢,尤其是多个同类型的 Slave 并行需要等待比较长的时间,上网查询发下默认情况下 Jenkins 保守地生成代理。如果队列中有 2 个构建,不会立即生成 2 个执行程序。会产生一个执行器并等待一段时间看第一个执行器有没有被释放,然后再决定产生第二个执行器。以确保产生的每个执行者都得到最大限度的利用。如果要覆盖此行为并立即为队列中的每个构建生成执行程序,可以在 Jenkins Mater 启动时参加一下参数:
总而言之 Kubernetes 博大精深,在 CI/CD 容器化的道路上还有很多知识点需要去学习。
原文链接: https://mp.weixin.qq.com/s/awPRvs_Vkj9cYSh4lsyEtQ