关于cicd:开个脑洞带你写一个自己的极狐GitLab-CI-Runner

43次阅读

共计 9283 个字符,预计需要花费 24 分钟才能阅读完成。

极狐 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 最外围的工作是这些:

  1. 从极狐 GitLab 拉取工作;
  2. 获取工作后,筹备一个独立隔离可反复的环境;
  3. 在环境中运行工作,上传运行日志;
  4. 在工作实现 / 异样退出后上报执行后果(胜利 / 失败)。
    咱们 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' # 打印出绿色的 $ date
date # 真正执行 date 命令

执行器的规范输入和规范谬误会被 Runner 捕捉,寄存在 /tmp 临时文件中。job 执行完结前,Runner 会周期性地调用接口 PATCH /api/v4/jobs/{job_id}/trace 增量上传日志,申请的 header 形如:

Host: gitlab.example.com
User-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/plain
Job-Token: jTruJD4xwEtAZo1hwtAp
Accept-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;m
section_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-RangeContent-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,咱们能够很快把命令行本体捏进去:

$ tart
An 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 clonecd 到仓库目录;
  • 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-installed
our-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

正文完
 0