开个脑洞,带你写一个自己的极狐 GitLab CI Runner
极狐 GitLab Runner 是极狐 GitLab CI/CD 执行的利器,能够帮助完成 CI/CD Pipeline Job 的执行。
目前极狐 GitLab Runner 是一个开源项目,以 Golang 编写。
极狐 Gitlab 有个不错的特性,就是你可以使用自己的极狐 Gitlab CI Runner。可是,如果你没有自己的 CI Runner 该怎么办呢?别担心,我们可以自己写一个。`[]~( ̄▽ ̄)~*`
在这篇文章里,我们会:
阐述极狐 GitLab Runner 的核心任务;
分析 Runner 工作时和极狐 GitLab 的交互内容;
设计和实施一个我们自己的 Runner;
让我们的 Runner 运行自己的 CI 工作;
埋一个彩蛋!
当然,如果你习惯直接看代码,欢迎访问极狐GitLab仓库。如果喜欢,欢迎留个 star。
Here we go!
明确核心任务
打蛇打七寸,极狐 GitLab Runner 最核心的任务是这些:
从极狐 GitLab 拉取工作;
获取工作后,准备一个独立隔离可重复的环境;
在环境中运行工作,上传运行日志;
在工作完成/异常退出后上报执行结果(成功/失败)。我们 DIY 的 Runner 同样要完成这些任务。
接下来我们按顺序捋一捋各个核心任务,同时观察 Runner 是怎么和极狐 GitLab 交互的。为了行文简明,下文的 API 请求和返回的内容有所精简。
注册
如果你用过自托管的极狐 GitLab Runner,你应该熟悉这个页面:
用户在这个页面获取注册 token,然后通过gitlab-runner register
命令把 Runner 实例注册到极狐 GitLab。这个注册过程本质上是在调用接口POST /api/v4/runners
,其 body 形如:
如果注册 token 无效,极狐 GitLab 会返回403 Forbidden
。在成功注册时会返回:
Runner 只关心其中的 token,它代表了 runner 的身份,同时作为共享密钥参与后面的 API 调用的鉴权。这个 token 会连同其他设置被保存到文件~/.gitlab-runner/config.toml
中。
拉取工作
Runner 在设定中有个最大并行工作数,在目前执行的工作数目小于设定值时,它会轮询POST /api/v4/jobs/request
以获取工作,传入的 body 很像注册时的 body,形如:
如果没有要执行的工作,极狐 GitLab 会返回状态码204 No Content
,Header 中会有游标,形如X-Gitlab-Last-Update: 2794e577289a38db0df0e93e3215f597
,供下次请求传入。
游标其实是个随机字符串,请求进入极狐 GitLab 的前置代理(名为 Workhorse)时,代理会检查 Runner 提交的游标是否和 Redis 中的游标一致,如果一致就让 Runner 等着(long poll),不一致就把请求原样代理到极狐 GitLab 后端。Redis 中的游标的更新由后端维护,在变更时会通过Redis Pub/Sub通知到 Workhorse. 工作的选取在后端实现为一个复杂的 SQL 查询。
在有新工作需要执行时,极狐 GitLab 会返回201 Created
,其 body 形如:
准备环境和克隆仓库
为了让 CI 的执行稳定、可重复,Runner 执行的环境需要一定程度的隔离,执行环境的准备、脚本的执行由Executor负责,聊几个常见的:
Shell:
好处:调试容易,易于理解。
坏处:隔离级别很低,只提供基于文件目录的隔离,项目依赖、可用端口会在 CI job 之间相互影响。
Docker 或 k8s:
好处:除了操作系统内核,其他资源都隔离了;镜像生态丰富,CI job 可重复性高。
坏处:不适用于不可信的工作负载。
VirtualBox 或 Docker Machine:
好处:操作系统级别的隔离,安全性高。
坏处:挺重的,拖累 CI 执行效率。
所有的 Executor 都提供必须的 API 供极狐 GitLab Runner 调用:
准备环境;
执行 Runner 提供的脚本,获取执行时的输出,返回执行结果(这个 API 会被调用多次);
清理环境。
克隆仓库其实就是在环境中执行一个git clone
,所需参数在上一步“拉取工作”中获得:
执行工作和上传日志
所有要执行的工作都会被 Runner 编排成几个脚本文本,发给 Executor 执行,编排时会考虑 Executor 里的脚本执行环境是哪一个(bash/Powershell)。环境变量会放在编排的脚本最前面,例如对于 bash 环境,环境变量在脚本中使用export
声明。
说个有趣的,你在 CI log 里看到的,标识为绿色的接下来要执行的语句是 Runner 在编排脚本时用echo
命令+终端颜色控制符输出的,类似这样:
执行器的标准输出和标准错误会被 Runner 捕获,存放在/tmp
临时文件中。job 执行结束前,Runner 会周期性地调用接口PATCH /api/v4/jobs/{job_id}/trace
增量上传日志,请求的 header 形如:
body 里就是这批增量上传的日志,本例形如:
下一次上传日志时,新请求的Content-Range
和Content-Length
的内容同样会对应请求 body 的信息。
极狐 GitLab 在成功接受请求后会返回202 Accepted
,返回的 header 中有一些有意思的值:
这里有一个有意思的优化,当 CI log 页面有用户正在观看时,X-Gitlab-Trace-Update-Interval
的值会是 3,即 Runner 应该 3 秒就增量上报一次日志,这样用户才能更实时地看到最新进展。
上报执行结果
在用户定义的脚本执行成功或失败后,Runner 会做两件事:
如果还有没上传的日志,按前述方法将剩余日志全部上传;
调用
PUT /api/v4/jobs/{job_id}
更新 job 的状态。
一个成功的 job 对应的 HTTP body 形如:
一个失败的 job 对应的 HTTP body 形如:
极狐 GitLab 后端在成功接受状态更新请求后会返回200 OK
,Runner 的工作就结束了。
有时,服务端没准备好接受状态更新(日志的处理是异步的,还没落盘),此时会返回202 Accepted
,header 里的X-GitLab-Trace-Update-Interval
会告知 Runner 在下次尝试之前的等待时间(类似指数退避),Runner 会一直重发请求,直到服务端返回200 OK
或者超过最大重试次数。
整体来看,上述流程是这样子的:
构建专属 Runner
OK,我们已经把极狐 GitLab Runner 的核心任务捋了一遍了,现在该打开 IDE,写我们自己的 Runner 啦!
取个名字
我喜欢吃蛋挞,我们就叫我们的 DIY Runner “蛋挞” 吧,英文名Tart
.
画个 Logo,这样看上去比较像一个正经项目:
再打开编程祖师娘 Ada Lovelace 的画像拜一拜接受祝福,万事俱备,开工大吉!
规划功能
和极狐 GitLab Runner 一样,蛋挞也是个命令行程序,主要功能有:
注册(register):注册极狐 GitLab runner token,输出配置文件到标准输出,这样我们可以再把它重定向到文件里,还能避(丢)免(给)处(用)理(户)“什么时候该覆盖配置文件”这样的玄学问题。
尝试获取和运行一个工作(single):监听工作,运行工作,提交结果,退出。这个命令主要是为了调试方便。
运行多个工作(run):监听工作,运行工作,提交结果,重复。
用上spf13/cobra,我们可以很快把命令行本体捏出来:
构建隔离的执行环境
构建隔离执行环境可能是 Runner 的一个最重要的任务了,理想的执行环境应该有这些特征:
资源隔离,文件系统、端口、进程空间独享,包括:
不被上一个 job 影响;
不被同时运行的其他 job 影响;
不被宿主机的其他进程影响。
可重复性:同一个 commit 对应的 job 每次执行结果应该是一致的。
宿主安全:job 的执行不会影响到宿主机或其他 job。
缓存友好:用空间换时间。
分析现有的极狐 GitLab Runner 的 Executor 各自满足了上述哪些特征就作为留给读者的练习了。
既然蛋挞是我们自己的 Runner,我们有充分的自由,让我们选择 Firecracker 来构建执行环境吧。
Firecracker是亚马逊云服务(AWS)开发和开源的虚拟机管理器,特点是轻量,它依靠KVM实现,通过模拟尽可能少的硬件以及跳过 BIOS 启动,可以在不到一秒内启动一台具有终端输入输出的虚拟机,并且每台虚拟机的额外内存开销不大于 5MB,AWS 使用 Firecracker 来构建自己的函数计算服务 Lambda 和无服务器托管服务 Fargate。
启动一台能供 CI 使用的 MicroVM(Firecracker 对虚拟机的称呼)需要三个依赖:
Linux 内核镜像;
联通外部网络的 TAP 设备(一个虚拟的 layer-2 网络设备);
根文件系统(rootFS,以文件的形式存在,可以类比 docker image 来理解,里面有操作系统的根
/
及其下属内容)。
你可以查看蛋挞对它们的具体实现,其中,根文件系统值得说道一下。
还记得我们梳理的极狐 GitLab Runner Executor 的必备 API 吗?虽然蛋挞并不直接仿写极狐 GitLab Runner 的 Executor,但是这三个操作仍然是必要的:
准备环境:按样本复制一份根文件系统交给 Firecracker,启动虚拟机。
执行 Runner 提供的脚本,获取执行时的输出,返回执行结果(这个 API 会被调用多次):我们稍后讨论。
清理环境:关闭虚拟机,删掉根文件系统。
让每个虚拟机都在根文件系统的副本上操作可以提供资源隔离和可重复性。
Firecracker 提供的终端只有一个输入和输出,操作自由度不够,这意味着我们在虚拟机里需要一个 agent,脚本交给它去执行,输出和退出码由它转交给蛋挞。思来想去,我们最常用的 agent 恐怕是 ssh 了:
在根文件系统里安装好 sshd 和登录公钥;
每次虚拟机启动后,蛋挞使用 ssh 去连接虚拟机;
蛋挞经过 ssh 执行命令,获取执行时的输出和执行结果。
sshd 会调用虚拟机本地的 bash 运行蛋挞提供的脚本,这正是我们想要的。
脚本的生成和执行
这步不难,极狐 GitLab 提供的用户脚本是一个字符串数组,环境变量是一个对象数组:
脚本开头写一个
set -euo pipefail
,这样执行会在遇到错误的时候停下来;git clone
和cd
到仓库目录;export
环境变量,每个一行,其中环境变量的值需要 escape;写一个
set +x
,这样 bash 就会把接下来要执行的每个命令写到标准输出了;写入用户脚本,每个一行;
每行末尾记得写断行符
\n
.
脚本交给 sshd 后就可以执行了,标准输出和标准错误会被蛋挞实时收集写到本地临时文件中,另有一个进程会把它周期性地增量上传到极狐 GitLab。
脚本执行结束后,sshd 会返回退出码,蛋挞会视情况上报 job 成功或失败。
运行自己的 CI
既然蛋挞是用来运行 CI 任务的,我们就找点任务来让它运行,比如……它自己的 CI?
让我们为蛋挞写一个.gitlab-ci.yml
:
把蛋挞注册为仓库的 CI Runner 后,禁用 shared runner(确保任务调度到蛋挞上),触发一次 CI 执行,看上去效果还不错!
埋一个彩蛋
对了,我还埋了一个小彩蛋与大家分享,如果你在星期四使用蛋挞运行 CI job,将会有一个神秘惊喜!点击👉即可访问蛋挞代码仓库
一点历史
2014 年~2015 年,GitLab Runner 有很多活跃的第三方实现,其中Kamil Trzciński基于 Go 的GitLab CI Multi-purpose Runner实现被 GitLab 相中,替代了 GitLab 自己基于 Ruby 的实现,成为了我们今天看到的极狐 GitLab Runner. 那时 Kamil Trzciński 还在 Polidea 工作,因此极狐 GitLab CI Multi-purpose Runner 是一个社区贡献。开源真是奇妙。
参考资料
版权声明: 本文为 InfoQ 作者【极狐GitLab】的原创文章。
原文链接:【http://xie.infoq.cn/article/e8af2c4984dc3f2f67f75e487】。
本文遵守【CC BY-NC】协议,转载请保留原文出处及本版权声明。
评论