# Ci/cd 工具链的分分合合

## 作案动机

> 一种对 ci/cd 工具的轻量化和解耦的尝试？

Jenkins 的传统集群方式，是使用不同环境的服务器构成不同能力的 Jenkins 节点，由主节点根据任务
环节的需要，调度不同能力的子节点来完成构建或部署任务。

进入容器云时代，情况发生了变化，我们可以使用不同能力的 Jenkins 镜像，使用 Kubernetes 插件来
完成这种任务的拆分和调度，为此，我构建了一个包含所有我们平时用到的工具的 Jenkins 镜像，简化了
节点的扩展和选择过程。

然而随着学习和应用的深入，我意识到这种做法有几个问题：

- DevOps 中隐含着发挥个人能力的愿景，工具链的所谓大而全，只不过是在画一个比较大的圈，使用这样的一套 Jenkins，还是要被其中所包含的仅有的工具中进行选择，对身陷其中的技术人员绝不能说是友好，也绝不是鼓励各展所长的态度。

- 现有的功能测试、接口测试、压力测试等工具，越来越专业化，往往会有各自的工作集群调度甚至是托管方案，例如 selenium grid、JMeter 集群等。

- 同样的测试工作，可能有多种工具都可以完成，例如一个 Restful 的接口测试，不管是 JMeter 还是Postman，或者 SoapUI 以及五花八门的自有工具，都可以完成这样的工作。

- 各种 DevOps 以及微服务管控和治理平台会有各自的工具链构成以及扩展方案。

- 让各种工具自成镜像，无疑对镜像尺寸和更新速度都会有更好的支持。

这样就让我产生一个新的思路：能不能让 Jenkins 回归到一个原始状态，只负责问题的定义、任务的分发和结果的归集呢？于是就有了这样的一点尝试。

这一尝试的思路是，Jenkins 镜像/容器只使用插件和一些 Shell 脚本，同外部的调度能力（例如
Docker 的容器或者 Kubernetes 的任务等）进行交互，利用网络和共享存储，来实现任务的分发和协调
以及最后的结果汇聚。

下面以 JMeter 为例，进行一个简单的压力测试，测试环境为了节省起见，使用的是 Docker 加本地目录
共享的方式，这种方式也可以很方便的扩展为 Kubernetes 的 Job + PV/PVC 方式。

## 镜像

- Jenkins: dustise/jenkins

- JMeter: hauptmedia/jmeter

> 注意此处随意的选择了一个 2.x 版本的 JMeter。

## Jenkins 插件

- `docker_build_step`：用于执行 Docker 指令，类似功能的 Docker Common 插件，其参数不支持环境变量，因此淘汰。

- Performance Plugin：一个用于搜集和展示多种测试结果报告的插件。

## 存储

这里我们需要为 Jenkins 和各种工具（这里是 JMeter）提供一个可以共享访问的文件交换区域：

- 单机 Docker 下，可以是共享的主机目录。

- Kubernetes 环境可以使用共享的分布式存储卷。

例如我们创建名为`/var/cicd/exchange/jmeter`的目录，进行文件交换。

## 输入

使用 JMeter GUI 录制 jmx 文件，为任务生成汇总日志（保存到/root/summary.log）。
将 jmx 上传到上面所说的目录之中，这样就保证了 Docker 和 Jenkins 都能通过 `-v` 来进行加载。

这里我们启动 Jenkins 的时候，使用 `-v /var/cicd/exchange/jmeter:/exchange/jmeter`
参数让 Jenkins 加载这一目录。

## Jenkins 任务

### 准备工作

首先的环节是，为后面要开工的 JMeter 准备工作环境。

~~~bash
# 创建当前 Build 目录
mkdir -p /exchange/jmeter/$BUID_TAG
# 复制 jmx 到当前 Build 目录
cp /exchange/jmeter/jd.jmx /exchange/jmeter/$BUID_TAG
~~~

### 设置容器

准备好文件之后，我们需要添加下一个环节就是设置一个容器：

这里添加的环节是`docker_build_step`的`Execute Docker Command`环节。

- Image name: hauptmedia/jmeter

- Command：bin/jmeter -n -t /root/jd2.jmx -l /root/result.log -j /root/process.log

- Bind Mounts：`/var/cicd/kube/$BUILD_TAG /root`

> 注意绑定卷这里就使用了 $BUILD_TAG 变量

这样就完成了 Jenkins 和 JMeter 的文件共享：

Jenkins 的 `/exchange/jmeter/$BUILD_TAG`，对应的是新创建容器的`/root`目录。

### 启动容器

使用`Execute Docker Command`的`start container(s)`环节，`Container ID(s)`填写变量
`$DOCKER_CONTAINER_IDS`，代表启动我们刚才创建的容器。

### 等待任务

如果我们需要使用 Jenkins 进行结果的汇聚，那么这里就需要进行阻塞——等到 JMeter 执行完毕后，
才能进行下面的搜集结果、清理现场等操作。

~~~bash
RESULT="/exchange/jmeter/$BUILD_TAG/summary.log"
while [ ! -f $RESULT ]
do
  sleep 30
done


while [[ `lsof | grep summary.log` ]]
do
    sleep 10
done
~~~

上面的脚本利用 lsof 来检测 summary.log 的占用情况，一旦该文件关闭，说明压力测试已经结束。

### 清理容器

使用 `Execute Docker Command` 的 `remove container(s)` 环节，`Container ID(s)` 填写变量
`$DOCKER_CONTAINER_IDS`，代表清理我们刚才创建的容器。

### 结果展示

Performance Plugin 能够识别 JMeter、SoapUI 以及 Parrot 生成的报告文件，这里我们只设置一
个选项，就是 `Source data files (autodetects format):`，这里填写
`/exchange/jmeter/$BUILD_TAG/summary.log`。

插件会把这一文件拷贝到 Workspace，进行解析和显示。

## Kubernetes Job

上述过程可以很方便的改造成为 Kubernetes Job：

- Docker 相关内容，改为使用 kubectl 进行的 job 文件的生成和操作。

- 阻塞过程可以查询任务节点的状态

- 文件存储可以使用 PVC 来共享
