教程(四)pipeline与k8s集成
传统的Jenkins Slave方式存在的问题
传统的 Jenkins Slave 一主多从式会存在一些痛点。比如:
发生单点故障时,整个流程都不可用了
每个 Slave
的配置环境不一样,来完成不同语言的编译打包等操作,但是这些差异化的配置导致管理起来非常不方便,维护起来也是比较费劲;
资源分配不均衡,有的 Slave 要运行的 job 出现排队等待,而有的 Slave 处于空闲状态;
资源有浪费,每台 Slave 可能是实体机或者 VM,当 Slave 处于空闲状态时,也不会完全释放掉资源。
每台slave节点都需要安装jdk,配置ssh服务等,即便我只是想运行一个python任务。
这样使我们的dockerfile中增加了额外的步骤造成额外的维护成本,并且镜像制作速度也会下降。
解决方案
基于上面的问题我们当前的改造方式是把salve做成镜像部署到k8s中, 比如我们的UI自动化所需要的slave就是测试k8s集群中一个pod。
这样解决了我们上面说的第一个问题,就是如果节点出现故障,那么k8s会帮我们把这个pod迁移到其他可用节点,但是它无法解决其他4个问
题。 所以最终我们希望将jenkins和k8s进行整合。 达到如下的效果
上面是我再jenkins官网上下载的图。 抛开master节点的实现(我们的jenkins
mater没有部署在k8s中),在这里jenkins能调用k8s的接口,动态的在k8s中创建pod并作为slave运行我们的测试任务,
任务运行完毕后删除pod。 并且它充分利用了k8s的特性, 创建的pod中负责与jenkins
master连接的slave容器是jenkins团队发布的docker镜像 jenkins/jnlp-slave,
而且jenkins的k8s插件会自动帮助我们定义pod的配置,我们只需要定义一个基本的pod信息,jenkins会把我们的配置与它自己的配置进行m
erge。 比如, 在sage-sdk-test的pipeline中,我们是这么做的:
在agent部分跟以往不同的是我们提供了一个k8s的pod模板, 这样就是告诉jenkins我们这个job要跑在k8s上,
需要在k8s上运行这个pod然后当做slave节点,运行我们的job。
其他的pipeline配置跟以前基本一样,这里只是把slave容器从以前的方式变成了现在的动态创建pod的方式。
1.
2.
3.
那么接下来我们看一下这里面的技术细节。 这个pod里面定义了两个容器, 一个是jnlp, 这里需要注意一下。
jenkins默认把名字叫jnlp的容器当做slave容器,所以如果你想要更换这个镜像的话, 就像上面做的一样即可。
也就是我们自己写一个dockerfile然后继承 对它进行扩展。 这样我们既保留了slave的能力又扩展了自己的运行依赖。 jenkins/jnlp-slave
比如下面我做的:
之所以要扩展jenkins/jnlp-slave主要有三个原因:
默认的jenkins/jnlp-slave镜像启动容器的时候是使用jenkins这个用户,而不是root用户,这样会导致我们后续与其他容器协作的时
候出现权限问题
默认的镜像不知道什么原因没办法解析我们公司的gitlab域名,
我到现在也不知道什么原因。只有我集成它并安装自己的镜像就可以。 很诡异的问题。
直接安装maven,配置未我们公司的私服。 这样我们就可以直接拿这个slave容器来执行了。
那么在sage sdk这个项目中, 我们测试的是python3.7的sdk, 所以这样还需要一个python3.7 的镜像来启动容器。 dockerfile如下:
很简单,
直接使用python3.7的官方镜像,我们的扩展只是在镜像里安装一些pytest的依赖,这样容器启动后不用实时下载,运行速度会更快。
这样我们通过python容器和jnlp容器组合成了我们跑pipeline需要的运行环境了。 我们看看运行起来后启动的pod是什么样子的:
1.
2.
3.
4.
1.
---
apiVersion: "v1"
kind: "Pod"
metadata:
annotations:
buildUrl: "http://m7-qa-test03:8081/job/sage-sdk-test/109/"
labels:
qa: "python3"
jenkins: "slave"
jenkins/label: "sage-sdk-test_109-hpf67"
name: "sage-sdk-test-109-hpf67-tr47k-95sch"
spec:
containers:
- command:
- "cat"
image: "registry.4paradigm.com/qa/python3"
name: "python3"
tty: true
volumeMounts:
- mountPath: "/home/jenkins/agent"
name: "workspace-volume"
readOnly: false
- env:
- name: "JENKINS_SECRET"
value: "********"
- name: "JENKINS_AGENT_NAME"
value: "sage-sdk-test-109-hpf67-tr47k-95sch"
- name: "JENKINS_NAME"
value: "sage-sdk-test-109-hpf67-tr47k-95sch"
- name: "JENKINS_AGENT_WORKDIR"
value: "/home/jenkins/agent"
- name: "JENKINS_URL"
value: "http://m7-qa-test03:8081/"
image: "registry.4paradigm.com/tester_jenkins_slave:v1"
name: "jnlp"
volumeMounts:
- mountPath: "/home/jenkins/agent"
name: "workspace-volume"
readOnly: false
imagePullSecrets:
- name: "docker4paradigm"
nodeSelector:
beta.kubernetes.io/os: "linux"
restartPolicy: "Never"
securityContext: {}
volumes:
- emptyDir:
medium: ""
name: "workspace-volume"
这个是从启动的pod的配置信息。 这里我们要关注以下几点:
这个pod的配置不全是我们定义的,我们定义的pod模板是比较简单的, 其他的pod配置都是jenkins帮我们加上的,
就像上面说的jenkins有自己的pod 模板,它会把我们的模板和它自己的进行merge。 这样才既能满足我们的需求,
也能满足它的需求
默认情况下jenkins会在pod中添加一个叫jnlp的容器作为slave容器,像我们刚才说的这个容器是使用jenkins/jnlp-slave
启动的,这是jenkins官方的slave镜像, jenkins在启动它的时候会自动加上对应的环境变量,比如jenkins master的URL,
工作目录,鉴权的JENKINS_SECRET等,而一旦我们自己的模板中有一个叫jnlp的容器,那么jenkins也会触发merge机制,
它会使用我们定义的这个镜像来替换它的jenkins/jnlp-slave 镜像来启动这个slave容器。 而这个机制就是我们用来替换默认镜像,
扩展自己想要的能力的方式
我们在自己定义的pod模板里可以指定多个容器, 而jenkins会自动的把所有的容器都挂载同样的目录。 这里的细节是:
我们可以看到jenkins为我们创建了一个emtpyDir, 这个emptydir的名字是worksapce-volume。
而所有的容器都挂载了这个volumes到/home/jenkins/agent下。 这样就达到了一个效果,那就是所有的容器都共享了这个jenkins
job的worksapce,也就是不论你在pipeline中切换到哪一个容器里,实际上你都可以对整个workspace做任何操作。
这是一个非常棒的设计, 这样可以让我们在各个容器间无缝切换获取运行依赖而不会影响任务运行。
python的容器启动时我制定了一个cat tty-=true 这么个命令, 这是为了能让容器持续运行,而不是一上来就运行完毕。
当然也可以通过制作镜像的时候在entrypoint里写一个死循环。 这里我们一定要注意,一定要保持容器一直处于运行状态。
否则jenkins就会发现容器运行状态不是running然后不停重启, 这个job也就一直是pending状态.
OK, 通过以上技术细节我们就可以总结出一些最佳实践。
不要在像以前做一个大而全的slave镜像,
1.
2.
不要在像以前做一个大而全的slave镜像,
这样镜像维护的成本太高,一旦有一点改动就要重新build好久,中间出一点错重新build的时间成本太高。
取而代之的是我们每个场景定义一个小而美的镜像。 比如要测试python sdk,那就用官方的python镜像就可以了,
如果需要go语言的依赖,就做一个golang的镜像就可以了。 我们只需要在运行时在不同的阶段切换到对应的镜像运行就可以了。
反正他们是共享worksapce目录的, 所以不论怎么切换都没问题的。
只有在具体运行测试任务的时候切换到对应语言的容器中去,其他的都在jnlp这个slave容器中运行。 这是因为jenkins
与其他容器通信的机制走的是k8s client的exec
。这个方式有很大的坑,就是这种方式运行的shell环境没有设置很多的环境变量,网络设置等等。
我在高可用测试工具中也使用的这个方式去与其他容器通信,经常遇到这个问题。 所以如果你在其他容器中执行git
这种拉取代码操作。 有可能会遇见诸如canot resolve hostname这种网络请求问题, 我也在执行make
build命令的时候出现过由于没有 预先设置 LANG=utf-8这种问题 而导致
java字符转义失败。而一个正常的系统是有这些预先设置这些变量的。
所以为了防止被坑,那就只在必要的时候切换到其他容器中运行,平时我们只在jnlp容器中操作。
在pipeline中切换运行容器
在这里我们依然用sage sdk项目为例子看一下, 其实非常简单。 只需要在pipeline中的steps里使用container指令就可以了。