极狐GitLab Runner 是极狐GitLab CI/CD 执行的利器,可能帮忙实现 CI/CD Pipeline Job 的执行。
目前极狐GitLab Runner 是一个开源我的项目(https://jihulab.com/gitlab-cn...),以 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形如:
{ "description": "一段用户本人提供的形容", "info": { "architecture": "amd64", # runner的架构 "features": { # runner具备的个性,极狐GitLab可能会回绝不具备某些个性的runner注册 "trace_checksum": true, # 是否反对计算上传日志的checksum "trace_reset": true, "trace_size": true }, "name": "gitlab-runner", "platform": "linux", "revision": "f98d0f26", "version": "15.2.0~beta.60.gf98d0f26" }, "locked": true, "maintenance_note": "用户提供的保护备注", "paused": false, "run_untagged": true, "token": "my-registration-token" #极狐GitLab提供的注册token}
如果注册token有效,极狐GitLab会返回403 Forbidden
。在胜利注册时会返回:
{ "id": 2, # Runner在极狐GitLab这边的全局编号 "token": "bKzi84WitiHSN4N4TYU6", # runner的鉴权token "token_expires_at": null # 据我察看,这个字段对应的性能没有做}
Runner只关怀其中的token,它代表了runner的身份,同时作为共享密钥参加前面的API调用的鉴权。这个token会连同其余设置被保留到文件~/.gitlab-runner/config.toml
中。
二、拉取工作
Runner在设定中有个最大并行工作数,在目前执行的工作数目小于设定值时,它会轮询POST /api/v4/jobs/request
以获取工作,传入的body很像注册时的body,形如:
{ "info": { "architecture": "amd64", # runner的架构 "executor": "docker", # runner应用的执行器 "features": { # runner具备的个性,例如,如果一个runner不反对上传产物,那么须要上传产物的工作就不会调度到它身上。 "artifacts": true, "artifacts_exclude": true, "cache": true, "cancelable": true, "image": true }, "name": "gitlab-runner", "platform": "linux", "revision": "f98d0f26", "shell": "bash", "version": "15.2.0~beta.60.gf98d0f26" }, "last_update": "d8a43f53bb125ec6599d778b9969a601", # 游标 "token": "bKzi84WitiHSN4N4TYU6" # 后面注册时拿到的token}
如果没有要执行的工作,极狐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形如:
{ "allow_git_fetch": true, "artifacts": null, # 要上传的产物 "cache": [], # 要应用的缓存 "credentials": [ { "password": "jTruJD4xwEtAZo1hwtAp", # 用来拉取代码、上传日志、上报执行后果的通用密钥 "type": "registry", "url": "gitlab.example.com", "username": "gitlab-ci-token" # 用户名是固定的 } ], "dependencies": [], "features": { "failure_reasons": [ # 服务端可承受的工作谬误起因 "unknown_failure", "script_failure" ] }, "git_info": { "before_sha": "6b55b6ffd17b57a2ec0cf8e7d7c66ff709343528", "depth": 20, # 克隆深度 "ref": "master", # 指标分支/tag "ref_type": "branch", "refspecs": [ "+refs/pipelines/52:refs/pipelines/52", "+refs/heads/master:refs/remotes/origin/master" ], "repo_url": "http://gitlab-ci-token:jTruJD4xwEtAZo1hwtAp@gitlab.example.com/flightjs/Flight.git", "sha": "cb4717728e8f885558a4e0bb28c58288b8bf4746" # commit hash }, "id": 823, # 工作id,是前面很多API调用的重要参数 "image": null, "job_info": { "id": 823, "name": "build-job", "project_id": 6, "project_name": "Flight", "stage": "build" }, "services": [], "steps": [ # 要执行的脚本 { "allow_failure": false, "name": "script", "script": [ # 脚本内容,每项对应 .gitlab-ci.yml 中的一个数组元素 "echo \"sleeping 1\"", "sleep 5", "echo \"sleeping 2\"", "sleep 5" ], "timeout": 3600, # 脚本最大执行超时 "when": "on_success" } ], "token": "jTruJD4xwEtAZo1hwtAp", # job凭据,用来鉴权前面的API调用 "variables": [ # job执行时的环境变量,用户本人定义的环境变量也会放在这里 { "key": "CI_JOB_ID", "masked": false, "public": true, "value": "823" }, { "key": "CI_JOB_URL", "masked": false, "public": true, "value": "http://gitlab.example.com/flightjs/Flight/-/jobs/823" }, { "key": "CI_JOB_TOKEN", "masked": true, "public": false, "value": "jTruJD4xwEtAZo1hwtAp" } ]}
三、筹备环境和克隆仓库
为了让CI的执行稳固、可反复,Runner执行的环境须要肯定水平的隔离,执行环境的筹备、脚本的执行由Executor负责,聊几个常见的:
Shell:
- 益处:调试容易,易于了解。
- 害处:隔离级别很低,只提供基于文件目录的隔离,我的项目依赖、可用端口会在CI job之间相互影响。
Docker或k8s:
- 益处:除了操作系统内核,其余资源都隔离了;镜像生态丰盛,CI job可重复性高。
- 害处:不适用于不可信的工作负载。
VirtualBox或Docker Machine:
- 益处:操作系统级别的隔离,安全性高。
- 害处:挺重的,连累CI执行效率。
所有的Executor都提供必须的API供极狐GitLab Runner调用:
- 筹备环境;
- 执行Runner提供的脚本,获取执行时的输入,返回执行后果(这个API会被调用屡次);
- 清理环境。
克隆仓库其实就是在环境中执行一个git clone
,所需参数在上一步“拉取工作”中取得:
git clone -b [分支/tag名] --single-branch --depth [克隆深度] https://gitlab-ci-token:job-token@gitlab.example.com/user/repo.git [克隆目的地文件夹名称]
四、执行工作和上传日志
所有要执行的工作都会被Runner编排成几个脚本文本,发给Executor执行,编排时会思考Executor里的脚本执行环境是哪一个(bash/Powershell)。环境变量会放在编排的脚本最后面,例如对于bash环境,环境变量在脚本中应用export
申明。
说个乏味的,你在CI log里看到的,标识为绿色的接下来要执行的语句是Runner在编排脚本时用echo
命令+终端色彩控制符输入的,相似这样:
echo -e $'\x1b[32;1m$ date\x1b[0;m' # 打印出绿色的 $ datedate # 真正执行 date 命令
执行器的规范输入和规范谬误会被Runner捕捉,寄存在/tmp
临时文件中。job执行完结前,Runner会周期性地调用接口PATCH /api/v4/jobs/{job_id}/trace
增量上传日志,申请的header形如:
Host: gitlab.example.comUser-Agent: gitlab-runner 15.2.0~beta.60.gf98d0f26 (main; go1.18.3; linux/amd64)Content-Length: 314 # 这个增量的长度Content-Range: 0-313 # 这个增量在全副日志中的地位Content-Type: text/plainJob-Token: jTruJD4xwEtAZo1hwtApAccept-Encoding: gzip
body里就是这批增量上传的日志,本例形如:
\x1b[0KRunning with gitlab-runner 15.2.0~beta.60.gf98d0f26 (f98d0f26)\x1b[0;m\x1b[0K on rockgiant-1 bKzi84Wi\x1b[0;msection_start:1663398416:prepare_executor\x1b[0K\x1b[0K\x1b[36;1mPreparing the "docker" executor\x1b[0;m\x1b[0;m\x1b[0KUsing Docker executor with image ubuntu:bionic ...\x1b[0;m\x1b[0KPulling docker image ubuntu:bionic ...\x1b[0;m
下一次上传日志时,新申请的Content-Range
和Content-Length
的内容同样会对应申请body的信息。
极狐GitLab在胜利承受申请后会返回202 Accepted
,返回的header中有一些有意思的值:
Job-Status: running # job运行状态Range: 0-1899 # 以后收到的字节范畴,每次都是0-n这个模式X-Gitlab-Trace-Update-Interval: 60 # runner最低上报距离,单位秒
这里有一个有意思的优化,当CI log页面有用户正在观看时,X-Gitlab-Trace-Update-Interval
的值会是3,即Runner应该3秒就增量上报一次日志,这样用户能力更实时地看到最新进展。
五、上报执行后果
在用户定义的脚本执行胜利或失败后,Runner会做两件事:
- 如果还有没上传的日志,按前述办法将残余日志全副上传;
- 调用
PUT /api/v4/jobs/{job_id}
更新job的状态。
一个胜利的job对应的HTTP body形如:
{ "checksum": "crc32:4a182676", # 所有日志的CRC32校验,用来让服务端确定所有日志都曾经胜利上传 "info": { ... }, # 这个字段曾经在后面见过很屡次了,内容从略 "output": { "bytesize": 1899, # 日志的总字节数 "checksum": "crc32:4a182676" # 同checksum }, "state": "success", # job执行后果 "token": "jTruJD4xwEtAZo1hwtAp" # job凭据}
一个失败的job对应的HTTP body形如:
{ "checksum": "crc32:f67200bc", "exit_code": 42, # 用户脚本的退出码 "failure_reason": "script_failure", # 谬误起因,从一个与服务端约定的列表里选 "info": { ... }, # 这个字段曾经在后面见过很屡次了,内容从略 "output": { "bytesize": 1723, "checksum": "crc32:f67200bc" }, "state": "failed", # job执行后果 "token": "Lx1oBNfw2e9xhZvNKsdX"}
极狐GitLab后端在胜利承受状态更新申请后会返回200 OK
,Runner的工作就完结了。
有时,服务端没筹备好承受状态更新(日志的解决是异步的,还败落盘),此时会返回202 Accepted
,header里的X-GitLab-Trace-Update-Interval
会告知Runner在下次尝试之前的等待时间(相似指数退却),Runner会始终重发申请,直到服务端返回200 OK
或者超过最大重试次数。
整体来看,上述流程是这样子的:
构建专属Runner
一、取个名字
我喜爱吃蛋挞,咱们就叫咱们的DIY Runner “蛋挞” 吧,英文名Tart
.
画个Logo,这样看上去比拟像一个正经我的项目:
再关上编程祖师娘Ada Lovelace的画像拜一拜承受祝愿,万事俱备,动工大吉!
二、布局性能
和极狐GitLab Runner一样,蛋挞也是个命令行程序,次要性能有:
- 注册(register):注册极狐GitLab runner token,输入配置文件到规范输入,这样咱们能够再把它重定向到文件里,还能避(丢)免(给)处(用)理(户)“什么时候该笼罩配置文件”这样的玄学问题。
- 尝试获取和运行一个工作(single):监听工作,运行工作,提交后果,退出。这个命令次要是为了调试不便。
- 运行多个工作(run):监听工作,运行工作,提交后果,反复。
用上spf13/cobra,咱们能够很快把命令行本体捏进去:
$ tartAn educational purpose, unofficial Gitlab Runner. Usage: tart [command] Available Commands: completion Generate the autocompletion script for the specified shell help Help about any command register Register self to Gitlab and print TOML config into stdout run Listen and run CI jobs single Listen, wait and run a single CI job, then exit version Print version and exit Flags: --config string Path to the config file (default "tart.toml") -h, --help help for tart Use "tart [command] --help" for more information about a command.
三、构建隔离的执行环境
构建隔离执行环境可能是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
:
variables: # speed up go dependency downloading GOPROXY: "https://goproxy.cn,direct" # we have go and build-essential pre-installedour-exceiting-job: script: - echo "run test" - go test ./... - echo "build tart" - make - echo "run tart" - cd bin - ./tart - ./tart version
把蛋挞注册为仓库的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是一个社区奉献。开源真是微妙。
参考资料
- Gitlab Runner
- Gitlab Rails
- mitmproxy