持续集成和部署是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目录下。
config.vm.synced_folder "app/", "/home/vagrant/app", id: "app"
下面就是在宿主机上安装好了的Jenkins目录
安装Jenkins镜像分成四部分,创建服务账户,安装持久卷,安装部署和安装服务,需要按顺序进行。其中的关键是创建服务账户,这个是必须的,没有它不会成功。不知为什么网上的有些文章没有提到它。
服务账户配置文件(service-account.yaml):
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
namespace: default
name: service-reader
rules:
- apiGroups: [""] # "" indicates the core API group
resources: ["services"]
verbs: ["get", "watch", "list"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["create","delete","get","list","patch","update","watch"]
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create","delete","get","list","patch","update","watch"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get","list","watch"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"]
这里创建了一个名为“service-reader”的“ClusterRole”,并把特定的权限(例如[“get”, “watch”, “list”])赋给特定的资源(例如[“services”])。
运行如下命令,创建一个名为“service-reader-pod”的集群角色绑定,它的“clusterrole”是“service-reader”,它的名字是“default:default”,其中第一个“default”是名空间(namespace),第二个“default”是服务账户名字,后面的部署配置文件会引用这个名字(default)。这里由于我没有给Jenkins创建单独的名空间,因此它用的默认名空间(“default”)。
kubectl create clusterrolebinding service-reader-pod --clusterrole=service-reader --serviceaccount=default:default
关于服务账户的权限定义,请参阅“Kubernetes plugin for Jenkins” .
持久卷配置文件(jenkins-volumn.yaml):
apiVersion: v1
kind: PersistentVolume
metadata:
name: k8sdemo-jenkins-pv
labels:
app: k8sdemo-jenkins
spec:
capacity:
storage: 1Gi
# volumeMode field requires BlockVolume Alpha feature gate to be enabled.
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
storageClassName: standard
local:
path: /home/vagrant/app/jenkins
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- minikube
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: k8sdemo-jenkins-pvclaim
labels:
app: k8sdemo-jenkins
spec:
accessModes:
- ReadWriteOnce
# storageClassName: local-storage
resources:
requests:
storage: 1Gi #1 GB
部署配置文件(jenkins-deployment.yaml):
apiVersion: apps/v1
kind: Deployment
metadata:
name: k8sdemo-jenkins-deployment
labels:
app: k8sdemo-jenkins
spec:
selector:
matchLabels:
app: k8sdemo-jenkins
strategy:
type: Recreate
template:
metadata:
labels:
app: k8sdemo-jenkins
spec:
serviceAccountName: default # 服务账户的名字是default
containers:
- image: jenkinsci/jenkins:2.154-slim
name: k8sdemo-jenkins-container
imagePullPolicy: Never
ports:
- containerPort: 8080
- containerPort: 50000
volumeMounts:
- name: k8sdemo-jenkins-persistentstorage
mountPath: /var/jenkins_home
volumes:
- name: k8sdemo-jenkins-persistentstorage
persistentVolumeClaim:
claimName: k8sdemo-jenkins-pvclaim
注意,这里引用了服务账户“default”(serviceAccountName: default)。