ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

把GitHub Actions自托管Runner放到Modal Sandbox:按需临时容器实现CI弹性

把GitHub Actions自托管Runner放到Modal Sandbox:按需临时容器实现CI弹性 把 GitHub Actions 的自托管 Runner 跑在 Modal Sandbox 上相当于把 CI 构建机从一台常驻 VM 换成一个按需创建的临时容器环境。Runner 还是同一个官方二进制注册流程也没有变化变化的只是它的宿主一个按秒计费、用完即销毁的隔离沙箱。这个组合解决的是自托管 Runner 最让人头疼的维护成本问题——机器空闲要花钱、环境坏了要重新初始化、安全补丁要自己打。换成 Sandbox 之后Runner 销毁后不会留下任何状态下次需要再重新拉起来。下面从一个最小可运行案例开始先讲清楚 Runner 的注册、调度和运行链路再给出一套可以在 GitHub 仓库中实际验证的 Modal Sandbox 启动脚本最后补充排查方法和生产化建议。阅读本文需要你对 GitHub Actions 的 workflow 有基本了解不需要已经用过 Modal文中会逐步说明。1. 为什么要把自托管 Runner 放到 Modal Sandbox 里1.1 GitHub 托管 Runner 的局限GitHub 官方托管 Runner 的优势是零维护提交代码后GitHub 自动分配一个虚拟机执行 workflow执行完回收。但它的局限也很明显。一是规格固定。官方提供的 Linux Runner 通常是 2 核 7 GB 内存左右Windows Runner 也是固定规格。如果某个项目需要 16 核编译大型 C 项目或者需要 A100 跑深度学习训练官方托管 Runner 无法满足。二是排队不稳定。使用高峰时段官方 Runner 经常要排队。对发布流水线来说排队十分钟到半小时并不罕见。三是成本模型不透明。超出免费额度的部分按分钟计费但很多团队计算后发现与其支付官方 Runner 费用不如用已有的云资源自建 Runner。对于长期频繁跑 CI 的项目自托管 Runner 在成本和规格上更可控。1.2 自托管 Runner 的本质一个会主动拉任务的常驻进程很多人把自托管 Runner 理解成“一台装了 Git 的服务器”这个理解不够准确。准确地说自托管 Runner 是 GitHub 官方发布的一个代理程序它长期运行在一台机器上负责做三件事启动时通过 HTTPS 向 GitHub 注册自己。持续保持与 GitHub 的服务连接等待任务消息。收到任务后在本机创建临时目录、拉取代码、执行步骤、上传日志。这个模型对部署环境非常友好。Runner 不需要公网 IP不需要开放入站端口只要它能主动访问 GitHub 的几个域名就能正常工作。这正好匹配云上的临时容器环境。1.3 Modal Sandbox 为什么适合做 Runner 宿主Modal Sandbox 是 Modal 提供的一种临时隔离执行环境。它和普通容器的主要区别是“用完即走”的属性非常强可以指定 CPU、内存、GPU、超时时间可以挂载 Secret 和 Volume任务结束或超时后环境直接销毁。把这两者组合在一起技术主线就非常清晰了Runner 官方二进制不变。注册和拉任务机制不变。只把 Runner 的宿主从常驻 VM 换成 Modal Sandbox。每次创建 Sandbox 时Runner 都是全新的环境依赖、缓存、残留进程都不会积累。这种方式最适合的工作流是那些“需要自定义规格、但不想维护专用机器”的 CI 场景。对比项官方托管 Runner自托管 VMModal Sandbox Runner环境隔离每次全新常驻机器可能被污染每次全新天然隔离成本模型按分钟计费长期租用机器空闲也付费按运行秒数计费扩容方式不可直接扩容手动加机器或脚本扩 VM调用 API 创建更多 Sandbox持久化无有本地磁盘默认无可挂载 Volume网络暴露无机器可能暴露端口默认无入站端口当然它不是没有代价。因为 Sandbox 是临时的每次启动 Runner 都要重新注册工作目录、缓存和工具链默认不会保留所以需要额外规划缓存策略。后文会专门讲这个问题。2. 理解 Runner 的注册、调度和运行链路2.1 注册分两步先拿 registration token再执行 config.shRunner 第一次启动并不会自动注册。GitHub 要求你先通过 API 获取一个注册令牌 registration token然后运行官方脚本config.sh把 Runner 的基本信息写入本地配置文件。获取仓库级注册令牌的接口是这样的curl -s -X POST \ -H Authorization: Bearer ${GITHUB_TOKEN} \ -H Accept: application/vnd.githubjson \ https://api.github.com/repos/${GITHUB_REPO}/actions/runners/registration-token返回内容是一个 JSON{ token: AAAAAAAAAABBBBBBBBBBCCCCCCCCCC, expires_at: 2025-01-01T00:00:00Z }这里要注意registration token 的有效期是 1 小时。也就是说从拿到 token 到执行完config.sh动作要快不能把这段流程拆到两个隔了很久的阶段。拿到 token 后在 Runner 的安装目录执行配置./config.sh \ --url https://github.com/${GITHUB_REPO} \ --token ${REG_TOKEN} \ --name modal-runner-001 \ --labels self-hosted,Linux,X64,modal \ --unattended \ --replaceconfig.sh会生成.runner和.credentials两个文件。.credentials里保存的是 Runner 后续向 GitHub 鉴权用的凭据它和刚拿到的 registration token 不是同一个东西。2.2 调度靠长轮询不是被推送很多第一次接触自托管 Runner 的人会以为GitHub 向 Runner 的某个端口推送任务所以必须在防火墙上放行入站规则。这是错的。Runner 的Runner.Listener进程启动后会主动连接 GitHub 的分发服务保持一条长连接持续等待任务消息。这个过程是 Runner 主动出站拉取不是 GitHub 主动推入。因此在 Modal Sandbox 里运行 Runner完全不需要开放入站端口。Sandbox 默认的网络出口能力足够支撑这种工作模式。Runner 运行期间会访问这些域名github.com api.github.com pipelines.actions.githubusercontent.com *.actions.githubusercontent.com vstoken.actions.githubusercontent.com如果你所在的环境对出站网络做了白名单控制需要放行这些域名。如果 Modal 的 Sandbox 支持自定义网络策略也可以据这张列表做收敛。2.3 收到任务后在本机执行当 workflow 中有任务匹配到该 Runner 的标签时Runner 会在自己的工作目录下创建_work/{repo}/{run_id}这样的路径然后拉取代码通常是actions/checkout完成的。按顺序执行 workflow 中的步骤。实时上传日志。上传产物或缓存。任务结束后清理本次任务目录。这意味着Runner 执行过程中的所有文件都写在本地临时目录。Sandbox 销毁后这些目录也会一起消失不会污染下一次任务。对 CI 来说这正是期望的隔离行为。3. 环境准备和依赖3.1 需要的账号和权限开始编写代码前需要确认下面几个条件全部满足。资源用途需要准备的内容GitHub 仓库注册 Runner对该仓库有 admin 权限GitHub Token获取 registration token经典 PAT 或 fine-grained PATModal 账号创建 Sandbox在 Modal 官网注册并完成登录本地机器运行 Modal SDKPython 3.9 或更高版本GitHub Token 的权限要求需要特别确认。经典 PAT 在私有仓库场景下通常需要repo作用域在公共仓库场景下一般需要public_repo。如果使用 fine-grained PAT需要给对应仓库授予 Administration 的读写权限。具体权限项以 GitHub 当前文档为准不要只凭猜测。3.2 本地安装 Modal SDK 并登录Modal 的 Python SDK 可以直接用 pip 安装。pip install modal安装完成后执行登录命令。Modal CLI 会打开浏览器要求你登录账号并授权。modal token new登录成功后可以在本地执行modal profile current确认当前使用的 profile 是否正确。如果你同时维护多个云账号登录时要注意当前的 profile 指向避免把 Sandbox 创建到错误的账号下。3.3 把 GitHub Token 保存为 Modal Secret不要把 GitHub Token 直接写进代码。Modal 提供了 Secret 机制可以把敏感变量加密保存在 Sandbox 创建时注入为环境变量。用命令行创建 Secretmodal secret create github-runner \ GITHUB_TOKENghp_xxxxxxxxxxxxxxxxxxxx \ GITHUB_REPOoctoorg/octorepo如果仓库名或 Token 发生变化可以用重新创建的方式覆盖旧 Secret。这里只放示例数据实际使用时要替换成自己的值并且不要把 Token 提交到 Git 仓库。4. 最小实现在 Modal Sandbox 中启动一个自托管 Runner4.1 整体设计一个 Sandbox 对应一个 Runner为了让整个方案可以先跑通这里采用最简单的模型本地运行一个 Python 脚本。脚本调用 Modal SDK 创建一个 Sandbox。Sandbox 内运行entrypoint.sh完成下载 Runner、获取注册 token、配置、启动。本地脚本打印 Sandbox 的 stdout直到 Runner 退出。以下是文件结构modal-github-runner/ ├── run_sandbox.py └── entrypoint.shrun_sandbox.py负责创建 Sandboxentrypoint.sh负责在 Sandbox 内部初始化 Runner。4.2 编写 Sandbox 内部入口脚本 entrypoint.shentrypoint.sh的核心工作是下载 Runner 二进制可在构建镜像时完成、获取 registration token、执行config.sh、执行run.sh。#!/usr/bin/env bash set -euo pipefail RUNNER_VERSION2.323.0 RUNNER_NAMEmodal-runner-${RANDOM} if [ ! -x /opt/runner/run.sh ]; then echo downloading actions runner ${RUNNER_VERSION} curl -fsSL -o /tmp/runner.tar.gz \ https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz mkdir -p /opt/runner tar xzf /tmp/runner.tar.gz -C /opt/runner fi echo fetching registration token REG_TOKEN$(curl -fsSL -X POST \ -H Authorization: Bearer ${GITHUB_TOKEN} \ -H Accept: application/vnd.githubjson \ -H X-GitHub-Api-Version: 2022-11-28 \ https://api.github.com/repos/${GITHUB_REPO}/actions/runners/registration-token \ | python3 -c import sys, json; print(json.load(sys.stdin)[token])) echo configuring runner ${RUNNER_NAME} export RUNNER_ALLOW_RUNASROOT1 /opt/runner/config.sh \ --url https://github.com/${
返回列表