Jenkins CI 插件:Kubernetes

Jenkins Kubernetes 插件实现了 当 Jenkins Master 接受到 Build 请求时,会根据配置的 Label 动态创建一个运行在 Pod 中的 Jenkins Slave 并注册到 Master 上,当运行完 Job 后,这个 Slave 会被注销并且这个 Pod 也会自动删除,恢复到最初状态。

一.Jenkins 安装插件:Kubernetes

Jenkins 选择-插件管理,搜索并安装 Kubernetes 插件

二.Kubernetes 插件配置:Configure Clouds

安装后添加和配置 Kubernetes Cloud 作为 Jenkins 配置的一部分,依次进入:

Manage Jenkins -> Manage Nodes and Clouds -> Configure Clouds


2.1 Configure Clouds: Configure Kubernetes Clouds

  • 配置情况一:本地集群

本地集群,即 Jenkins Master 托管在同一个 Kubernetes 集群上,那么只需要为本地集群提供 Kubernetes apiserver 的机器内部域名地址,如:https://kubernetes:6443

其他配置参考下图:

  • 配置情况二:远程集群

远程集群是指 Jenkins Master 位于 Kubernetes 集群外部,一般为自建的 Jenkins 服务。

我们先来填写下图红色标识部分的信息:

名称:任意添加

  • Kubernetes 地址:kube-apiserver 地址和端口
  • Kubernetes 命名空间:default 或者自定义的名称空间
  • Jenkins 地址:外部独立的 Jenkins Server 地址

接下来配置蓝色标识部分信息:

  • Kubernetes 服务证书 key:获取 cluster-admin 管理员用户 kubeconfig 文件 /root/.kube/config 下的 certificate-authority-data 字段的内容转换成 base64 编码的结果填入
1
$ echo 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JS78978UR2akNDQXFhZ0F3SUJBZ0lVVnVjRWJ1dkZQVTlGUldUQ3Fqb3c1VW02YWtrd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1pURUxNQWtHQTFVRUJoTUNRMDR4RURBT0JnTlZCQWdUQjBKbGFXcHBibWN4RURB112T0JnTlZCQWNUQjBKbAphV3BwYm1jwNVpoaVRXaSt3N0hSRnFvM3hjT2MwWldjSlRMbCtIUloxcjdGdjYKSXVzUG9rVDBPN2lMaHNrRE11UWZqR21SWGVwd2haUUZOMUZkcG5iSmhhUUlNZFVUakZpWThvT21JMWUxVTd78EUAo5ekU9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K' | base64 -d
  • 凭据:凭据类型选择 “Secret file”,cluster-admin 管理员用户 kubeconfig 文件内容保存为 kubeconfig 附件上传。 /root/.kube/config

  • 最后 K8S集群连接测试:出现 Kubernetes 集群版本号表示 Jenkins 连接外部 Kubernetes 集群成功!

  • 报错证书错误,看k8s大于2.3 会有这个错误, 低版本的不会。
1
Error testing connection https://XXX:6443: javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

解决禁用HTTPS证书检查

2.2 Configure Clouds: Pod Templates

接下来开始配置 Pod Templates


2.2.1 Pod Template 区块配置:

  • 名称:jnlp
  • 命名空间:这里我使用 jenkins
  • 标签列表:jnlp-slave, 这个标签非常重要,其作用是在 Jenkins Pipeline 指定 agent 完成 Job 时用到
  • 用法:选择 “尽可能使用这个节点”
  • 运行命令:对于Windows容器,powershell是一个很好的默认值。sleep,正常是设置为空,比较少用window容器
  • 命令参数:对于Windows容器,参数Start Sleep 999999是搭配powershell的合理选择。

2.2.2 Container Template:区块配置:

  • 名称:必须为 jnlp
  • Docker 镜像:指定临时 Pod 使用的镜像,默认会使用 jenkins/inbound-agent 作为镜像来完成。

配置私有镜像仓库

我们在Jenkins上配置了镜像地址,但这个地址必须是要公开的。否则我们拉去不到,那怎么样才可以设置私有仓库呢?

在高级下面有配置秘钥

这边配置的秘钥,是k8s 启动 jnlp pod 起来之后在里面pull拉群镜像的秘钥。

这个是秘钥不是 Jenkins 的连接镜像仓库的凭据, 而是k8s里面配置的秘钥,我们这边只需要在相应的namespace

1
$kubectl create secret docker-registry <secret name>  --namespace=jenkins --docker-server=<your-registry-server> --docker-username=<your-name> --docker-password=<your-pword> --docker-email=<your-email>

或者 kubesphere 上操作

2.2.3 工作目录:根据你实际使用的 docker镜像来写,或者保持默认

挂载卷:把二进制的 docker 命令和 .sock 文件挂载进 JOB POD,使得其能够在Pod内完成 docker镜像的构建。

1.23版本之前是docker 之后是 containerd

三.授权 docker 二进制命令权限

因为我们 Jenkins JOB 分发到 k8s-node 主要用于 docker image build 构建业务镜像,所以我们需要把二进制命令 /usr/bin/docker 与 /var/run/docker.sock 挂载进 Pod 供其能够使用。

相同的,如果需要构建 nodejs, Java, go 等需要先构建再发布的项目,思路也是如此:把构建过程要到的命令挂载进 Pod

3.1 授权方式1

Jenkins Agent 默认会以 jenkins 1000:1000 这个用户随机在某个 k8s-node 节点上创建临时 Pod 来完成 JOB的构建。

如果需要把宿主机的二进制命令 /usr/bin/docker 挂载进 Pod 来执行构建 docker image build,那么需要对二进制的 docker 命令和 /var/run/docker.sock 文件授权 1000:1000 用户的权限。

1
2
3
4
5
6
7
8
9
# 所有k8s-node执行
## k8s-node 上可能没有 uid,gid 为 1000:1000 的用户,那么我们可以手工创建一个
$ groupadd -g 1000 jenkins
$ useradd -u 1000 -r -g jenkins -s /sbin/nologin -MN jenkins
$ gpasswd -M jenkins root

## 所有 k8s-node 授权 docker 命令,挂载进 Jenkins Slave 容器中用于构建
$ chown jenkins.root /usr/bin/docker
$ chown jenkins.root /var/run/docker.sock

3.2 授权方式2

直接为宿主机的 docker systemd脚本配置 ExecStartPost

1
2
3
4
5
$ vim /etc/systemd/system/docker.service
....此处省略

[Service]
ExecStartPost=/usr/bin/chmod 0777 /var/run/docker.sock

….此处省略
重启 dockerd 后验证

1
2
$ ll /var/run/docker.sock
srwxrwxrwx 1 root root 0 Jan 4 19:58 /var/run/docker.sock

3.3 授权方式3(推荐)

这个功能在新版本的Jenkins 才会有,旧版本推荐2的方式(具体版本没去查,使用的时候注意下就好)

Jenkins 在配置k8s集群中 - Pod Template - Pod Template details - Container Template - 高级中,运行权限设置成root 使用命令 id root 查看root 用户的id gid 填入进去

四、k8s 运行空间

创建Kubernetes Namespace与Service Account

4.1 创建Kubernetes Namespace jenkins

在Kubenates的上创建jenkins命名空间,用于Jenkins使用

1
kubectl create namespace jenkins

4.2 创建Service Account (可选)

这个是需要在Jenkins 命名空间上调用部署其他业务空间的业务时用到,如果我们只是调用k8s 进行编译,部署k8s 还是Jenkins master 上就不需要这个权限。(没去验证)

在Kubernetes上为Jenkins构建创建有Cluster Admin权限的Service Account jenkins:

1
kubectl create clusterrolebinding jenkins --clusterrole cluster-admin --serviceaccount=jenkins:jenkins

五、Jenkins Pipeline

Jenkins On Kubernetes—Jenkins上Kubernetes Plugin的使用

我们给 node 添加了一个 jnlp-slave Label 标签,指定这个 Pipeline 的 4个 stage 步骤都使用这个 Label 标签下的 Pod 来构建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
node('jnlp-slave') {
stage('GitCheckout') {
echo "1.Git Checkout stage"
}
stage('InitConfig') {
echo "2.InitConfig Stage"
}
stage('DockerBuild') {
echo "3.DockerBuild Stage"
}
stage('DeployK8S') {
echo "4. DeployK8S Stage"
sh "sleep 600"
}
}

验证

1
2
3
4
5
6
7
8
9
10
11
12
# Job 被下发给 dev-k8s-node1 节点
$ kubectl get pods -n jenkins
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
jnlp-fswft 1/1 Running 0 2m39s 10.244.23.245 dev-k8s-node1 <none> <none>

# 进入容器查看
$ kubectl exec -it jnlp-fswft /bin/bash -n jenkins
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.

# Job Pod Container内可以正常使用 docker命令
jenkins@jnlp-fswft:~/agent$ docker --version
Docker version 20.10.4, build d3cb89e

至此 Kubernetes + Jenkins 的 CI 阶段完成,我们可以在 Pipeline 脚本编写各式各样的构建任务,如拉取git代码、docker build、npm build 等等。

CI阶段的完成目标主要是:构建业务镜像 docker image push 推到到私有镜像仓库。

六.后记避坑

6.1 Q1.构建用的基础镜像

一开始使用的是 jenkins/inbound-agent:latest,才 360多M ,里面的工具命令很全能够满足基本的构建场景。jenkins/inbound-agent:latest 没有curl wget rsync,并且容器默认用户为 root

坚持镜像越小,效率越高,太重的东西可以安装在k8s node节点上,挂载进去使用。

根据这个镜像作为基础镜像,从新生成一个自己的镜像,用于以后业务,自己也根据自己的业务去制作这个镜像

具体的制作过程,看另外一篇文档jenkins-jnlp-镜像制作

6.2 Q2.二进制挂载构建过程需要使用的工具

我的业务镜像构建过程会用到 Python3,Nodejs,docker,kubectl,因此这些工具建议使用二进制部署于每一个 Node的宿主机,再采用 Host Path 的方式挂载进构建 Pod

注意配置 Pod 的环境变量,以供 Pod 内的 Container 能够正常使用二进制命令。不能使用$PATH 美元符号,需要填写完整 PATH

6.3 Q3.构建 Pod 的名称空间变更为 default

上文构建的名称空间开始为 jenkins,而在实际CD发布到 K8S上后发现,APP被部署到远端K8S的 jenkins 名称空间下了。而后改回 default 名称空间正常。

由于我这边并没有在yaml定义名称空间的字段,那如果在 yaml 内定义,可能可以指定名称空间发布。(猜测没有实践过)

上面也说到了如果我们发布不用k8s node 来部署业务,这个问题就不存在。

6.4 Q4 Jenkins 构建报错无法执行下去

排错进入容器

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
kubectl exec -it jnlp-2562q -- /bin/bash

cat /home/jenkins/agent/logs/remoting.log.0

LinkageError while performing UserRequest:hudson.slaves.SlaveComputer$SlaveVersion@57ca2813
java.lang.UnsupportedClassVersionError: Failed to load hudson.slaves.SlaveComputer$SlaveVersion
at hudson.remoting.RemoteClassLoader.loadClassFile(RemoteClassLoader.java:385)
at hudson.remoting.RemoteClassLoader.findClass(RemoteClassLoader.java:307)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:348)
at hudson.remoting.MultiClassLoaderSerializer$Input.resolveClass(MultiClassLoaderSerializer.java:134)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1868)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
at hudson.remoting.UserRequest.deserialize(UserRequest.java:291)
at hudson.remoting.UserRequest.perform(UserRequest.java:190)
at hudson.remoting.UserRequest.perform(UserRequest.java:54)
at hudson.remoting.Request$2.run(Request.java:369)
at hudson.remoting.InterceptingExecutorService$1.call(InterceptingExecutorService.java:72)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at hudson.remoting.Engine$1.lambda$newThread$0(Engine.java:93)
at java.lang.Thread.run(Thread.java:748)

java 版本不对,我们拉的镜像java 8的 但我们Jenkins 需要java 11
一开始用的镜像是 jenkins/inbound-agent,自己原先有做过修改的,所以里面java版本不对是java8 ,后面修改成 jenkins/inbound-agent:latest 从新做镜像。

解决换镜像

6.5 Q5 java.lang.NoSuchMethodError: No such DSL method ‘withKubeConfig’ found among steps [approveReceivedEvent,

测试发布,调研k8s cli插件部署业务的时候报错

看另外一篇文章 安装 Kubernetes CLI

参考资料1
参考资料2
参考资料3
参考资料4
参考资料5