# Jenkins：乘着 Kubernetes 的翅膀

## 前言

Kubernetes + Docker 是一对有意思的组合，为微服务架构的落地，扫清了最后一公里的障碍，在符合企业 IT 治理需求的前提之下，为传统企业应用的平滑过渡提供了有效条件和方法。

作为软件生产环节中重要组成部分的持续构建和发布过程，自然也要随势而动，这方面的老将 Jenkins 不但提供了用于构建、推送 Docker 镜像的插件，更提供了利用 Kubernetes 运行构建集群的能力。本文将利用一个简单的 Hello world 项目，来展示 Jenkins 的这一特性。

## 准备工作

### Kubernetes 集群安装

要在 Kubernetes 集群完成下面所有的工作，因此首先进行集群的安装和配置，这方面可以参考 [官方入门文档](http://kubernetes.io/docs/hellonode/)，如果英语不灵但动手排错能力强的话，也可以参考[已经过时的拙作](http://blog.fleeto.us/content/centos-7-kubernetes-11x-docker-19x-zhuang-zhi-nan)。

集群安装后，应该具有以下能力：

1. 集群具有受 DNS 支持的服务寻址能力；
2. 能够利用 Kubectl 或者其他方法发布容器应用的能力；
3. 私有镜像库，或者到 Docker Hub 的网络连接；
4. Pause 镜像也自然是 Kubernetes 中运行应用的必须条件。

### Jenkins 镜像

本文例子采用自制的一个集成镜像为基础工具，该镜像集成了众多常用的 CI/CD 工具，另外同时还包含了 Jenkins 的 Master 和 Slave 两种模式，镜像托管在 [Docker Hub](https://hub.docker.com/r/dustise/jenkins/)，源码可在 [Github](https://github.com/fleeto/docker-jenkins) 浏览和下载。

如果具有直接连接 Docker Hub 的网络连接，则可无需理会；否则就需要下载镜像，并上传至私库。为行文方便，这里假设采用私库方式，镜像地址为 `10.211.55.5:5000/jenkins:2.7.4.5`

### 共享存储

因容器在集群中的运行状况未知，一般来说是需要为容器化应用提供共享存储服务的，本文中采用最简单的 NFS 方式，当然也可以使用官方支持的其他方式，例如 GlusterFS、Flocker 以及 Ceph 等等，具体支持能力可以参看[官方说明](http://kubernetes.io/docs/user-guide/volumes/)，当然，如果只是学习测试，使用缺省的 Empty 格式亦可，或者偷懒使用 HostPath 方式结合限定 Node 运行的方式也能够完成任务。

> 本文中的卷挂载方式很是粗糙，目前推荐的共享存储方式是[持久卷（PV & PVC）](http://kubernetes.io/docs/user-guide/persistent-volumes/)方式，以提供更好的管控能力，中文可参考：[Kubernetes 中的 Persistent Volumes](http://blog.fleeto.us/translation/persistent-volumes)。

## Jenkins Master

### YAML

首先要为我们的 Jenkins 镜像编写一个 Yaml ，用于提交到集群中运行：

~~~yaml
kind: ReplicationController
apiVersion: v1
metadata:
  name: jenkins
  labels:
    name: jenkins
spec:
  replicas: 1
  selector:
    name: jenkins
  template:
    metadata:
      labels:
        name: jenkins
    spec:
      containers:
      - name: jenkins
        image: 10.211.55.5:5000/jenkins:2.7.4.5
        ports:
        - containerPort: 8080
          protocol: TCP
        - containerPort: 8081
          protocol: TCP
        volumeMounts:
          - name: jenkins
            mountPath: /data/jenkins
      volumes:
      - name: jenkins
        nfs:
          server: 10.211.55.5
          path: /var/data/nfs/ci/jenkins
          readOnly: false
---
kind: Service
apiVersion: v1
metadata:
  name: jenkins
  labels:
    name: jenkins
spec:
  type: NodePort
  ports:
  - protocol: TCP
    nodePort: 32502
    targetPort: 8080
    port: 8080
    name: web
  - protocol: TCP
    targetPort: 8081
    port: 8081
    name: service
  selector:
    name: jenkins
~~~

上面的 YML 文件有几点需要注意

1. 挂载：Jenkins 的所有插件、配置和工作文件都处于环境变量 `JENKINS_HOME` 所示的路径中，因此我们这里利用 NFS 把位于 `10.211.55.5` 上的  `/var/data/nfs/ci/jenkins` 目录映射到容器的  `/data/jenkins` 之中，让 Jenkins 获得持久化存储。
2. 端口：Jenkins 缺省运行需要 8080 端口对外提供 Web 界面。这里我们另外声明了一个 8081 端口，为集群内新建的 Slave 提供通信能力，可以看到，下面的 Service 定义中，仅仅使用 NodePort 方式暴露了一个 Web 端口
3. 时区：镜像提供了 TIMEZONE 环境变量，这一变量将会在运行时影响容器操作系统以及 Jenkins 的 JVM 的时区设置。这一变量缺省使用 `Asia/Shanghai`。
4. `JENKINS_MODE`：用于指示 Jenkins 的运行模式，可选值为 `MASTER` 或者 `SLAVE`，缺省运行在 `MASTER` 模式下。

## 运行和配置

接下来就可以使用 `kubectl create -f jenkins.yaml` 来运行镜像了。执行之后可以刷 `kubectl get pods` 来查看启动情况。

这一 RC 的启动需要加载 NFS 卷，因此如果启动时间过长，可以使用 `kubectl get events` 命令查看是否加载出了问题。

Pod 变为 Running 状态之后，就可以尝试采用上面 YAML 中定义的 Node Port 来访问 Jenkins 界面了，经过一段时间的 `"Jenkins正在启动，请稍后....."`，系统要求输入 `/data/jenkins/secrets/initialAdminPassword` 文件中保存的初始密码，假设前面 get pods 命令得到的 Pod 名称是 `jenkins-7nmka`，这里就可以使用 `kubectl exec jenkins-7nmka cat /data/jenkins/secrets/initialAdminPassword` 来查看，获取结果后即可复制黏贴到录入框中继续安装。

接下来的插件安装，为节省时间直接 Select None。接下来设置管理员用户名密码，保存后，安装结束。

## 安装和配置 Kubernetes 插件

### 安装

接下来就是进入 `pluginManager/available` 页面安装插件。

> 如果网络需要代理，可以进入网址 `/pluginManager/advanced`，设置代理服务器。
> 如果可选插件页面为空，可以在 `/pluginManager/advanced` 页面里面点击 "立即获取" 按钮进行刷新。

过滤框中输入 "kubernetes" 会看到列表中的 "Kubernetes plugin"，选择安装。

### 配置 Jenkins

安装成功后，进入配置页面（`/configure`）。

首先为了测试方便，我们把 “执行者数量” 设置为 0，也就是说只使用 Jenkins Slave 进行构建。

### 配置 Kubernetes

可以看到，页面下方有一个按钮 “新增一个云”，点击后出现 Kubernetes 配置项目。

这里我们用无认证的 http 方式进行连接，Kubernetes URL 中填入 API Server 的 http 地址，例如：`http://10.211.55.5:8080`。

Jenkins URL 中，这里要注意我们不应该使用浏览器地址栏中的 Node 地址，而是应该使用集群内部的服务地址，根据上文中 Service 的定义，这里使用 `http://jenkins:8080`。

最后是 ”Add Pod Template“ 按钮，来定义 Slave 的 Pod 模板：

- 这里的 "Docker Image" 项目跟上面使用的是同一个地址，也就是 `10.211.55.5:5000/jenkins:2.7.4.5`；
- 根据前文说道的环境变量，这里我们新增一个环境变量：JENKINS_MODE=SLAVE，
- "Jenkins slave root directory" 这一项填写 `/data/jenkins`，
- "Command to run slave agent	" 这里填写镜像中的启动命令 `/usr/local/bin/run.sh`

填写完成后，保存。

### 配置端口

最后需要到安全配置页面（`/configureSecurity`），`TCP port for JNLP agents` 一项填写固定端口 8081。

## Hello world

### 创建

新建一个 Free Style 项目。构建步骤中新增 "Execute shell script" 环节，并保存，内容为

~~~bash
echo "Hello World"
~~~

### 构建

点击该任务的 “立即构建” 按钮，即可触发构建动作。

因为前面我们设置 Master 不执行构建工作，所以在构建启动之后，会在构建执行状态中看到有节点被动态新建，来执行我们的构建过程。这一过程中如果使用 `kubectl get events` 或者 `kubectl get pods` 命令，能看到 Jenkins Slave Pod 的创建、执行和销毁过程。

## 尾声

上面只是一个最为基础的构建过程，甚至都无法称为是一个完整过程，Kubernetes 插件还提供了很多其他选项，结合自定义的 Slave 镜像，能够完成更多更复杂的任务，用于配合实际的生产过程。
