本文作者:倚天码农

持续集成和部署是 DevOps 的重要组成部分,Jenkins 是一款非常流行的持续集成和部署工具,最近试验了一下 Jenkins,发现它是我一段时间以来用过的工具中最复杂的。一个可能的原因是它需要与各种其它工具集成才能完成任务,而集成的方法又各不相同。在这些工具中,Docker 是最简单的,真的非常好用。K8s 比较复杂,开始要花些时间熟悉,但它的整体设计十分合理,一旦搞清核心概念,掌握脉络之后,就非常顺利。它的命令格式即规范又统一,使得有些命令自己都能猜出来,这就是好的设计带来的福利。。但 Jenkins 给人的感觉就是开始的时候没有设计得很好,后面在不断地打补丁,导致一件事情有好几种不同的做法,对不熟悉的人来讲无所适从。没有统一的风格,处处都是意外,使得整个系统看起来既庞杂又没有章法,当然这也跟它出来的时间比较长有关。虽然它可能不是最好的,但它是免费的,因此不能要求太高。

由于种种原因,我的 Jenkins 安装碰到了各种各样的问题,为此我查看了大量的资料。但遗憾的是每个人安装 Jenkins 的方法都有些不同,很难找到一篇文章能解决所有问题。在我看来,Jenkins 的安装有两三个关键之处,非常容易出错,一定要理解透彻才能成功。

本文分成两部分,第一部分讲正常安装步骤,如果一切顺利,就不需要看第二部分了。我只能说恭喜你,你的运气太好了。第二部分是讲各种问题及解决办法,这应该是本文最有价值的部分。

第一部分:在 k8s 上部署 Jenkins

1. 安装在什么地方?

容器化是大势所趋,它不但包括应用程序的容器化,还包括与之相关的工具的容器化。当把 Jenkins 部署在 K8s 上时,Jenkins 的主节点会根据情况自动生成子节点(新的容器)来完成任务,任务结束后会自动销毁子节点。

我先在 Windows 上部署了 VirtulBox 虚机,并用 Vagrant 来管理虚机,再在虚机上部署了 k8s。并通过 Vagrant 设置虚机和宿主机之间的网络共享,这样就可以在宿主机上用游览器直接访问 k8s 上的 Jenkins。另外还要把宿主机的硬盘挂载到 Jenkins 上,这样 Jenkins 的物理存储还是在宿主机上,即使虚机出了问题,所有的配置和数据都不会丢失。

2. 选择镜像文件

这个看起来不是问题,但是一不留神就容易出错。我就是因为选错了镜像,导致安装了很多遍,最后才成功,在本文的第二部分会详细说明。我最终用的镜像文件是“jenkinsci/jenkins:2.154-slim”,后来发现这个是比较旧的版本,新的镜像 是“jenkins/jenkins:lts”, 但因为已经安装成功了,就没有再换。Jenkins 真的很坑人,有三个镜像“Jenkins”,“jenkinsci/jenkins”, "jenkins/jenkins", 其中正确的是"jenkins/jenkins"。

选好镜像之后,可以先运行下面命令,下载 Jenkins 镜像文件到本地(虚机上)。vagrant@ubuntu-xenial:~$ docker pull jenkinsci/jenkins:2.154-slim

3. 安装 Jenkins 镜像:

在安装之前,需要先把宿主机的 Jenkins 安装目录挂载到虚机上,这样可以在本地直接操作 Jenkins。下面是 Vagrant 的配置文件(Vagrantfile)中的设置,它把宿主机的 app 目录挂载到虚机的"/home/vagrant/app"。Jenkins 就安装在 app 目录下。

下面就是在宿主机上安装好了的 Jenkins 目录




k8s jenkins指定容器 jenkins k8s 持续部署_ci


目录

安装 Jenkins 镜像分成四部分,创建服务账户,安装持久卷,安装部署和安装服务,需要按顺序进行。其中的关键是创建服务账户,这个是必须的,没有它不会成功。不知为什么网上的有些文章没有提到它。

服务账户配置文件(service-account.yaml):

这里创建了一个名为“service-reader”的“ClusterRole”,并把特定的权限(例如["get", "watch", "list"])赋给特定的资源(例如["services"])。

运行如下命令,创建一个名为“service-reader-pod”的集群角色绑定,它的“clusterrole”是“service-reader”,它的名字是“default:default”,其中第一个“default”是名空间(namespace),第二个“default”是服务账户名字,后面的部署配置文件会引用这个名字(default)。这里由于我没有给 Jenkins 创建单独的名空间,因此它用的默认名空间(“default”)。

关于服务账户的权限定义,请参阅“Kubernetes plugin for Jenkins”[1] .

持久卷配置文件(jenkins-volumn.yaml):

部署配置文件(jenkins-deployment.yaml):

注意,这里引用了服务账户“default”(serviceAccountName: default)。

服务配置文件(jenkins-service.yaml):

这里面的一个关键点是部署和服务都暴露了两个容器端口,一个是 8080,另一个是 50000. “8080”是外部访问 Jenkins 的端口,“50000”是 Jenkins 内部集群之间的互相通信端口。这里的 Jenkins 集群不需要你搭建,而是 Jenkins 根据需要自动生成的,因此这两个端口是必须配置的。这里的配置命令都是比较标准的 k8s 配置,因此没有详细解释。

如果你想了解 k8s 命令详情(包括 Vagrant 配置),请参阅“通过搭建 MySQL 掌握 k8s(Kubernetes)重要概念(上):网络与持久卷”.

运行下面命令创建 Jenkins:

验证安装:

获得 Jenkins 的 Pod 名

查看 Jenkins 日志

当看到“INFO: Jenkins is fully up and running”,就说明 Jenkins 已经运行好了,Jenkins 的第一次启动需要一定时间,要耐心等待。

4. 登录

前面已经讲过,你可以在 Vagrant 里设置宿主机和虚机之间的网络互访,我的虚机的地址是“192.168.50.4”,“30080”是 Jenkins 服务的 NodePort 的对外地址,因此可以用“http://192.168.50.4:30080/” 访问 Jenkins。

登录之前先要获得初始口令,你可以在 Jenkins 的“secretsinitialAdminPassword”目录里获得管理员用户初始口令,我挂载 Jenkins 的宿主机目录是“E:app2kubappjenkins”, 因此口令文件是“E:app2kubappjenkinssecretsinitialAdminPassword”。口令是“072d7157c090479195e0acaa97bc1049”。第一次登录之后,需要重新设置用户和口令。

5. 安装推荐插件

登录之后,先要安装必要的插件才能完成整个安装工程, 直接选“Install suggested plugins”就可以了。


k8s jenkins指定容器 jenkins k8s 持续部署_k8s jenkins指定容器_02


6 安装 Kubernetes Plugin

用管理员账户登录 Jenkins 主页面后,找到 Manage Jenkins-》Manage Plugins-》Available, 勾选安装“Kubernetes plugin”即可。

下图是安装之后的图:


k8s jenkins指定容器 jenkins k8s 持续部署_怎么查看一个项目部署之后的项目名_03


plugin

7. 配置 Kubernetes Plugin

用管理员账户登录 Jenkins Master 主页面后,找到 Manage Jenkins-》Configure System-》,然后配置 Kubernetes Plugin。如下图所示:


k8s jenkins指定容器 jenkins k8s 持续部署_k8s jenkins指定容器_04


配置

这是最重要的一个配置,决定整个安装的成败。默认的“name”是“Kubernetes“,这个不需要修改,但以后配置 Pipelines 时要用到。“Kubernetes URL”用 “https://kubernetes.default” 就可以了。设置之后点击“Test Connection”,见到“Connection test successful”就成功了。

“Jenkins URL”是从外部(从虚拟机而不是宿主机)访问 Jenkins 的地址。你可以用如下命令,找到 Kubernetes 的“Jenkins Url”:

另外一个参数是“Jenkins tunnel”,这个参数是 Jenkins Master 和 Jenkins Slave 之间通信必须配置的,但不知道为什么,网上的很多文章都没提这个参数,也许是 Jenkins 的版本不同,有些版本可能不需要。

查看容器名

查看容器地址:

根据上面信息,Jenkins 的地址是“tcp://10.100.3.79:8080”,把 8080 换成 50000 就可以了。最终结果是“10.100.3.79:50000”,注意不要添加“http”。

8. 测试 Jenkins:

现在 Jenkins 已经全部安装好了, 下面进行测试。在 Jenkins 主页面点击“New Item”创建新项目,如下图所示,输入项目名,然后选择“Pipeline”。


k8s jenkins指定容器 jenkins k8s 持续部署_k8s jenkins指定容器_05


Jenkins

进入项目配置页面,如下图所示,脚本文件是 jenkinsfile-test:


k8s jenkins指定容器 jenkins k8s 持续部署_k8s jenkins指定容器_06


test

这是最简单的测试,它直接使用 Jenkins 主节点(主节点名是 master),不需要启动子节点,因此基本上都不会有什么问题。在 Jenkins 主页面选项目“test”,然后选“Build Now”运行项目,再到“Console Output”中查看结果如下:

9. 测试子节点:

这是复杂一点的测试,需要启动子节点,这个才能真正检测出安装的成败。先创建一个新的项目“slave-test”。

上面是脚本(jenkins-salve-test)。其中“POD_LABEL”取任何名字都可以(在 Kubernetes-plugin 1.17.0 版本之后,系统会自动命名,但以前需要自己取名),“cloud: 'kubernetes'”要与前面定义的“Kubernetes Plugin” 相匹配。它有两个 stage,一个是“build”,另一个是“run”。在“podTemplate”里定义了每一个 stage 的镜像(这样后面的 stage 脚本里就可以引用),这里为了简便把两个镜像设成是一样的。因为是测试,第一个 stage 只是输出“echo hello”, 第二个运行镜像“jfeng45/k8sdemo-backend:1.0”里的 main.exe 程序。

在 Jenkins 主页面选项目“slave-test”,然后选“Build Now”运行项目,再到“Console Output”中查看结果如下:

运行成功,测试阶段就完成了。

用脚本来写 Pipeline 有两种方法,“Scripted Pipleline”和“Declarative Pipleline”,这里用的是第一种方法。详情请见“Using a Jenkinsfile[2]”. “Declarative Pipleline”是新的方法,我在以后的文章里会讲到。这里因为是测试,只要通过了就行。

不必须的安装步骤:

还有些安装步骤在某些文章中提到了,但它们只是锦上添花,不是必须的。如果你的配置出现了问题,不要怀疑是这些步骤没执行造成的。

  1. 配置名空间(namespace):

有些安装步骤为 Jenkins 配置了单独的名空间,这样当然更好,但你即使没有配置也不会出现问题。

  1. Kubernetes server certificate key:

有些安装步骤提到要配置“Kubernetes server certificate key ”,但我并没有设置它,也没有影响运行。

第二部分:常见问题

1. Jenkins 版本不对:

最开始用的是 jenkins:2.60.3-alpine(这个已经是 Jenkins 镜像的最高版本了),这个版本太低,在安装插件时基本上都不成功,如下图


k8s jenkins指定容器 jenkins k8s 持续部署_ci_07


后来换成 jenkins:latest,这个应该是最新的吧,结果 版本还是一样的,只不过 Linux 不是 Apline 的。

后来终于明白了是镜像错了(而不是版本的问题),是要用 Jenkinsci, 而不是 Jenkins。我用了当时排在第一位的 jenkinsci/jenkins:2.150.1-slim,安装之后,上面的插件错误全部消失了,真不容易。

2. 不支持 Kubernetes Plugin

但当安装 Kubernetes Plugin 插件时,提示需要 2.150.3(我的是 2.150.1),这也太坑了。只好再次重装,这次用的是 jenkinsci/jenkins:2.154-slim,还好终于成功了。不过这个其实还是以前的镜像,最新的在“jenkins/jenkins”。

3. 不能访问 Kubernetes

错误信息如下:

详情请参见Kubernetes log, User “system:serviceaccount:default:default” cannot get services in the namespace[3]

错误原因是没有建立 service account。解决办法是先创建“service-account.yaml”文件,然后运行如下命令:

再次运行,错误消失。

4. Jenkins URL 地址不对

在 Jenkins 主页面,进入 Manage Jenkins-》System Log-》All Jenkins Logs, 错误信息如下。

这个错误主要是和 Kubernetes-plugin 配置有关。在 Jenkins 主页面,进入 Manage Jenkins-》Configure System》,在“http://192.168.50.4:30080/configure” 里有两个“Jenkins URL”,不要弄混了。第一个是“Jenkins Location”下的“Jenkins URL”, 它是宿主机访问 Jenkins 的地址。


k8s jenkins指定容器 jenkins k8s 持续部署_Jenkins_08


第二个是“Cloud”下的“Jenkins URL”, 它是从虚拟机访问 Jenkins 的地址。


k8s jenkins指定容器 jenkins k8s 持续部署_k8s里面的项目怎么暴露端口让用户访问_09


在上图中,我开始时用的是“http://192.168.50.4:30080/” ,但这个是从宿主机访问 Jenkins 的 Url,不是从虚机内部访问的 Url。你可以用如下命令,找到 Kubernetes 的“Jenkins Url”

键入如下命令测试 URL。

这就说明 URL 是好的。

5. 不能连接 slave

“Jenkins Url”改了之后,地址是对的,但还是不通。运行项目时,页面显示如下信息:


k8s jenkins指定容器 jenkins k8s 持续部署_怎么查看一个项目部署之后的项目名_10


“Console Output”(在 Jenkins->salve-test->#13 中,其中#13 是 build #)显示如下信息:

后来发现还有一个参数要填写,就是“Jenkins tunnel”。如下图所示。


k8s jenkins指定容器 jenkins k8s 持续部署_k8s里面的项目怎么暴露端口让用户访问_11


file

详情请见 Kubernetes Jenkins plugin - slaves always offline[4].

填写之后原来的信息没有了,而且出现了“Agent discovery successful”,这个信息是原来没有的。但又有新的错误。可用如下方法查看系统日志,在 Jenkins 主页面,选择 Manage Jenkins-》System Log-》All Jenkins Logs, 信息是这样的:

它的原因是“JenkinsTunnel”的地址还是不对,可用如下方法找到“Jenkins tunnel”地址:

根据上面信息,Jenkins 容器地址是“tcp://10.100.3.79:8080”,把 8080 换成 50000 就可以了。最终结果是“10.100.3.79:50000”,注意不要添加“http”。详情请见 What setting to use for jenkins tunnel?[5]

6. 镜像问题

当使用的镜像文件是“k8sdemo-backend:latest”或“k8sdemo-backend:1.0”时,“Console Output”显示错误如下:

查看 Pod, 错误是“ImagePullBackOff”:

查看镜像:

这里一共有三个“k8sdemo-backend”镜像,它们的“Image ID”都是一样的,之所以有三个是因为我用如下命令创建了 tag

但创建了之后,就只有“jfeng45/k8sdemo-backend:1.0”(最晚创建的)能够用在 Jenkins 的 Pipeline 脚本里,其他两个都会报错。修改了正确的镜像文件之后就运行成功了。

7. pv 和 pvc 删除慢

当用以下命令删除 pv 时,命令迟迟不能返回。

当你查看时,状态(status)显示一直是“Terminating”,但总是不能结束退出。pvc 也是一样。

这个主要原因是用到它们的服务和部署还在运行,先把服务和部署删除之后,pv 和 pvc 的删除操作就马上结束,顺利返回了。

源码:

完整源码的 github 链接[6]

注意,本文的程序在 0.1(tag)下,这个程序的主分支以后还会修改。

下面是程序的目录结构,黄色部分是与本文有关的配置文件。


k8s jenkins指定容器 jenkins k8s 持续部署_k8s jenkins指定容器_12



参考资料

[1]

“Kubernetes plugin for Jenkins”: https://github.com/jenkinsci/kubernetes-plugin/blob/master/src/main/kubernetes/service-account.yml

[2]

Using a Jenkinsfile: https://jenkins.io/doc/book/pipeline/jenkinsfile/

[3]

Kubernetes log, User “system:serviceaccount:default:default” cannot get services in the namespace: https://stackoverflow.com/questions/47973570/kubernetes-log-user-systemserviceaccountdefaultdefault-cannot-get-services

[4]

Kubernetes Jenkins plugin - slaves always offline: https://stackoverflow.com/a/38489054

[5]

What setting to use for jenkins tunnel?: https://devops.stackexchange.com/questions/6266/what-setting-to-use-for-jenkins-tunnel

[6]

完整源码的github链接: https://github.com/jfeng45/k8sdemo/tree/0.1