ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

在Modal Sandboxes上运行GitHub Actions自托管Runner实践指南

在Modal Sandboxes上运行GitHub Actions自托管Runner实践指南 大家在日常开发中多多少少都遇到过 GitHub Actions 构建变慢、免费额度不够用或者需要更高配置的构建环境。之前我在团队里负责 CI 基础设施优化时就反复被“公共 runner 排队”“VCPU 时长超额”这类问题卡住网上关于 self-hosted runner 的教程虽然不少但要么是“在笔记本上跑一个常驻程序”的入门玩法要么就直接进入复杂的 Kubernetes 自建集群很难找到一套同时兼顾弹性、成本和易维护的方案。这篇文章想基于一个比较新的思路来展开将 GitHub Actions 的自托管 runnerself-hosted runners运行在 Modal Sandboxes 上。文章会先从核心概念讲起再带大家从零跑通一个最小示例在 Modal Sandbox 中启动自托管 runner让仓库里的 workflow 使用这个 runner 完成任务最后整理几个我实践过程中遇到的问题和工程建议。无论你是想省 CI 费用还是想给构建任务引入 GPU或者单纯对“按秒计费的临时 Runner”感兴趣这篇教程都能给你一条可以照着走完的路径。1. 背景与核心概念1.1 GitHub Actions 的 Runner 到底是什么先解释一下 Runner 在 GitHub Actions 里的角色。GitHub Actions 的完整执行链路大致如下workflow 由仓库中的 YAML 文件定义。当触发条件满足时GitHub 会创建一个运行任务job。这个 job 需要到一个具体环境中执行这个执行环境就是 Runner。Runner 可以理解为一台“能接收 GitHub 下发的任务指令并执行”的机器。官方文档把 Runner 氛围两类GitHub 托管的 RunnerGitHub-hosted runner由 GitHub 提供现成的虚拟机环境。特点是开箱即用、维护简单但有免费额度限制而且 CPU、内存、磁盘规格相对固定。自托管 Runnerself-hosted runner由用户自己提供机器在机器上安装一个名为actions-runner的客户端程序然后让 GitHub 把 job 派发给这台机器。自托管 Runner 最大的优势是“可控”和“可定制”你可以用高配机器跑大型编译任务也可以把 Runner 放在内网环境以访问私有资源。但它的维护成本也相当明显——机器要长期在线、系统要打补丁、Runner 程序要更新、还有容量规划问题。1.2 Modal Sandboxes 是什么Modal 是一个面向 AI 和数据处理场景的云平台我最开始认识它是因为它可以把 Python 函数直接变成云端任务。不过在 Modal 的产品体系里还有一个更灵活的能力叫做 Sandboxes。Sandboxes 可以理解成一个“临时可用的容器环境”支持自定义镜像。可以执行任意命令不局限于 Python 函数。按秒计费任务结束或者沙箱被关闭资源和费用就停止。秒级启动不需要像传统虚拟机那样等待分钟级启动时间。可以选择 CPU 实例也可以选择带 GPU 的实例。这意味着 Sandboxes 非常适合“临时跑一段代码”“开一个交互式开发环境”“跑一批短时任务”这类场景。把 GitHub Actions 的 self-hosted runner 放进 Sandbox本质上是让 CI 任务运行在一个“用完即走”的临时容器里。1.3 为什么要把 Runner 放在 Modal Sandboxes 上传统自托管 Runner 的痛点是“常驻”即使没有任务Runner 程序也占着机器资源机器费用照付。而 Modal Sandboxes 的特点是“按需”每个 job 到来时启动一个 Sandboxjob 结束就销毁充分利用了“按秒计费”的弹性优势。这个组合的实际价值有几点不用维护常驻服务器。Runner 的构建环境可以通过 Modal 镜像统一管理。如果想要 GPU可以直接在 Sandbox 配置里指定 GPU 类型解决本地没有 GPU 或 GPU 机器成本高的问题。每次 job 都是全新的环境构建产物更可预期不容易出现“跑到一半依赖状态残留”这类问题。当然这个方案也不是银弹。它更适合任务并行度较高、单次任务不需要内网固定 IP、对网络策略不敏感的团队。如果任务需要长期占用固定端口或者必须通过 IP 白名单访问公司内网这个方案就需要额外设计网络层方案不能直接照搬。2. 环境准备与版本说明2.1 需要的账号与工具在开始操作之前需要先准备好以下账号和工具项目说明GitHub 账号用于创建仓库和注册 runnerModal 账号用于创建 Sandbox需要注册并开通Modal CLI通过pip install modal安装Python 环境建议 Python 3.10 及以上Modal SDK 依赖现代 Python 特性本地终端用于执行 modal 命令和 git 操作具体版本以你当前安装为准本文示例代码基于常见的 Modal SDK 写法重点是演示配置思路。如果后续 Modal 官方更新了 SDK 接口请以官方文档为准。2.2 安装 Modal CLI 并验证在终端执行pip install modal安装完成后验证 CLI 是否可用modal --version接着登录 Modal 账号modal setup执行后终端会引导你打开浏览器完成认证认证成功后会生成一个 API Token保存在本地配置文件中。这里有一个小细节Modal 的登录凭证和设备绑定有关系。如果在服务器或者 Docker 容器里使用需要确认本地能读取到~/.modal.toml或对应的凭据文件。2.3 GitHub 仓库和 Token 准备为了注册 self-hosted runner我们需要一个 GitHub 仓库可以是公开仓库或私有仓库测试用公开仓库更简单。一个用于注册 runner 的 token。关于 Token 有两种选择Personal Access TokenPAT快速测试时使用但权限范围大建议仅在个人项目测试时使用。GitHub App Token权限被限定到指定仓库更安全适合团队使用。本文先以 PAT 作为快速开始的方式在第 6 节工程建议中会说明为什么生产环境更推荐 GitHub App。3. 核心原理拆解3.1 自托管 Runner 的工作原理一个自托管 Runner 的核心是actions-runner这个程序。它的工作流程如下Runner 程序从 GitHub 服务器获取任务。程序根据 workflow 配置在本地启动一个工作目录执行任务步骤。执行过程中上传日志、Artifact 等数据。任务结束后Runner 回到空闲状态等待下一个任务。自托管 Runner 和 GitHub 之间的通信基于轮询机制Runner 会定期向 GitHub API 请求任务。因此Runner 所在的机器需要能够访问 GitHub 的 API 地址也需要能够下载依赖包和代码。注册一个 Runner 的核心命令是./config.sh --url https://github.com/owner/repo --token token这里的--url指定仓库地址--token是临时注册凭证。注册完成后会生成一个.runner文件里面记录了 Runner 的身份信息。需要注意的是--token是一次性的通常有效期为 1 小时。过期后需要重新生成。这个细节在自动化脚本中经常踩坑后面会单独说明。3.2 Modal Sandbox 的启动模式Modal Sandbox 的核心 API 是modal.Sandbox.create。它以 Python 代码的方式描述一个容器import modal sb modal.Sandbox.create( imagemodal.Image.debian_slim().pip_install(requests), cmd[sleep, infinity], )创建成功后Sandbox 会一直运行直到你调用sb.terminate()或容器内命令结束。Sandbox 内部和我们平时用的 Docker 容器类似有文件系统、有网络、可以执行命令。因此你完全可以在这个容器里下载actions-runner、配置 Runner、然后让它持续运行。3.3 两者桥接的关键点把 GitHub Actions Runner 跑在 Modal Sandbox 上核心思路是在本地或一个管理进程中调用 Modal SDK创建一个 Sandbox。在 Sandbox 内部下载并配置actions-runner。启动 Runner 程序让它开始轮询 GitHub 任务。当 Sandbox 被销毁时Runner 随之消失。这里的难点在于“如何让 Sandbox 知道自己要执行什么命令”。Modal 支持两种方式在Sandbox.create时指定cmd执行启动脚本。使用sb.exec()在 Sandbox 运行后向其中发送命令。第一种方式更适合在 CI 中“启动即注册”的场景Sandbox 一启动就自动把 Runner 跑起来不需要额外人工介入。下面这个流程图可以帮助理解整体调用关系GitHub 仓库 workflow | | 触发 job v GitHub Actions 服务 | | 检测到 runner label 匹配 v Modal Sandbox 中的 actions-runner 程序 | | 从 GitHub 拉取任务 v 在 Sandbox 内执行构建步骤4. 完整实战在 Modal Sandbox 上运行自托管 Runner下面我们进入实战环节。目标是在一个名为modal-runner-demo的仓库中配置 workflow 使用跑在 Modal Sandbox 中的自托管 runner。4.1 创建项目结构本地创建项目目录mkdir modal-runner-demo cd modal-runner-demo建议的项目结构如下modal-runner-demo/ ├── sandbox_runner.py # Modal Sandbox 启动脚本 ├── run_runner.sh # Sandbox 内部执行的 runner 启动脚本 └── .github/ └── workflows/ └── demo.yml # 测试 workflow其中sandbox_runner.py在本地或者管理机执行run_runner.sh会被复制到 Sandbox 内部执行。4.2 获取 Runner 注册 Token先打开 GitHub 仓库的 Settings 页面Settings - Actions - Runners点击 “New self-hosted runner” 按钮页面下方会显示注册命令示例同时展示一个--token参数。这个 token 就是注册 Runner 的临时凭证。在实际自动化过程中可以通过 GitHub API 获取这个 tokencurl -X POST \ -H Authorization: token PAT \ -H Accept: application/vnd.github.v3json \ https://api.github.com/repos/owner/repo/actions/runners/registration-token返回结果中token字段即为注册 token。由于这是自动化教程我们把 token 通过环境变量传给 Python 脚本而不是硬编码在代码里。4.3 编写 Sandbox 启动脚本在项目根目录创建sandbox_runner.pyimport os import pathlib import modal # 定义一个镜像这里以 debian_slim 为基础并安装 curl 和 tar image ( modal.Image.debian_slim() .apt_install(curl, tar) ) # 读取本地脚本并挂载到 Sandbox runner_script pathlib.Path(__file__).parent / run_runner.sh modal.function(imageimage) def start_runner(repo_url: str, token: str, labels: str): # 创建 Sandbox sb modal.Sandbox.create( imageimage, mounts[ modal.Mount.from_local_file(runner_script, remote_path/root/run_runner.sh), ], cmd[bash, /root/run_runner.sh], environment{ REPO_URL: repo_url, RUNNER_TOKEN: token, RUNNER_LABELS: labels, }, timeout60 * 60, # 1 小时超时 ) # 保持 Sandbox 运行并实时输出日志 for log in sb.iter_logs(): print(log, end) # 如果 Sandbox 异常退出则抛出错误 exit_code sb.wait() if exit_code ! 0: raise RuntimeError(fSandbox exited with code {exit_code}) if __name__ __main__: repo os.environ[REPO_URL] token os.environ[RUNNER_TOKEN] labels os.environ.get(RUNNER_LABELS, self-hosted,linux,modal) start_runner.remote(repo, token, labels)这段代码有几个关键点modal.Mount.from_local_file会把本地的run_runner.sh挂载到 Sandbox 内。environment参数可以把注册所需的变量传入 Sandbox。sb.iter_logs()可以实时查看 Sandbox 内部日志方便排查问题。timeout设置了沙箱超时时间避免因为异常导致 Sandbox 一直运行。4.4 编写 Sandbox 内部的 runner 启动脚本在项目根目录创建run_runner.sh#!/usr/bin/env bash set -euxo pipefail # 参数来自环境变量 REPO_URL${REPO_URL:?REPO_URL is required} RUNNER_TOKEN${RUNNER_TOKEN:?RUNNER_TOKEN is required} RUNNER_LABELS${RUNNER_LABELS:-self-hosted,linux,modal} RUNNER_VERSION2.317.0 # 创建 runner 目录 mkdir -p /home/runner cd /home/runner # 下载 actions-runner curl -sSL https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz -o actions-runner.tar.gz tar xzf actions-runner.tar.gz rm actions-runner.tar.gz # 注册 runner ./config.sh --url ${REPO_URL} --token ${RUNNER_TOKEN} --labels ${RUNNER_LABELS} --unattended --replace # 启动 runner 并持续运行 ./run.sh几个细节RUNNER_VERSION这里写的是示例版本实际使用请去 GitHub Actions Runner Releases 页面查看最新版本。Runner 版本更新比较频繁最好在脚本中通过变量维护避免每换一次版本就改一次脚本。--unattended表示非交互式注册适合自动化场景。--replace表示如果已经存在同名 Runner则替换避免重复注册报错。set -euxo pipefail用于在脚本出现错误时立即退出方便定位问题。4.5 创建测试 Workflow在.github/workflows/demo.yml中创建一个简单 workflow使用我们自定义的 labelsname: Demo Modal Runner on: push: jobs: test: runs-on: [self-hosted, linux, modal] steps: - uses: actions/checkoutv4 - name: Show system info run: | uname -a cat /etc/os-release - name: Run a command run: echo Hello from Modal Sandbox!注意runs-on的值是数组形式[self-hosted, linux, modal]。这表示 GitHub 只会把这个 job 派发给同时包含这些标签的自托管 Runner。4.6 本地启动 Sandbox Runner先把run_runner.sh加上执行权限chmod x run_runner.sh然后通过环境变量运行启动脚本export REPO_URLhttps://github.com/owner/repo export RUNNER_TOKENregistration-token export RUNNER_LABELSself-hosted,linux,modal python sandbox_runner.py如果一切正常你会看到类似下面的日志Runner registration successful. Running runner ... Starting runner listener...这时候回到 GitHub 仓库的 Runner 设置页面你会看到在线Online状态多了一个 Runner。4.7 触发 Workflow 验证向仓库推送任意 commit或者直接在 GitHub 页面上手动运行 workflowActions - Demo Modal Runner - Run workflow打开 workflow 运行日志你会看到 job 使用了self-hosted标签并且构建步骤都是在 Modal Sandbox 中执行的。预期输出Linux runner 5e3a2b1c0d 4.18.0-... #1 SMP ... PRETTY_NAMEDebian GNU/Linux 12 (bookworm) Hello from Modal Sandbox!这说明 Runner 已经成功运行在 Modal Sandbox 中。5. 常见问题与排查思路在实际运行过程中我遇到过不少问题下面整理成表格方便大家按图索骥。问题现象常见原因解决思路Sandbox 启动后立即退出run_runner.sh中某个命令失败set -e导致脚本退出先去掉set -e手动调试或在脚本适当位置加echo输出关键变量Runner 注册失败提示 token 无效注册 Token 有效期只有 1 小时生成后太久没用重新生成 registration-token并尽快执行注册仓库 Runner 列表中出现多个同名 Runner每次 Sandbox 启动都使用相同 hostname且没有--replace增加--replace参数或在启动时生成随机 hostnameWorkflow 一直卡在排队中runs-on标签和 Runner 标签不匹配在仓库 Runner 设置页面确认 Runner 的标签并检查 workflow 中的 label 是否完全匹配Sandbox 能启动但网络拉取代码失败Sandbox 默认网络策略限制或 GitHub API 不可达检查 Modal 网络配置确认可以访问github.com和api.github.comRunner 运行一段时间后自动停止Sandbox 超过 timeout 被销毁根据单次构建耗时调整 timeout或拆分长任务下面展开说几个重点问题。5.1 Token 过期问题这是一个非常容易踩的坑。GitHub 的 registration token 有有效期限制通常是 1 小时。如果你在脚本里写的 token 是 1 小时前生成的注册时就会报错。解决方案在本地先调用 GitHub API 获取最新 token。将 token 通过环境变量传给sandbox_runner.py。不要手动复制粘贴过期 token。如果需要长期自动化可以考虑用 GitHub App 动态生成 installation token这样就不会有过期问题。5.2 Sandbox 的 Hostname 冲突Runner 的身份是保存在.runner文件中的。如果你每次都从同一个镜像启动 Sandbox并且没有清理旧文件那么新老 Sandbox 可能使用相同的 Runner 名称导致 GitHub 侧出现“Runner 状态异常”或“重复注册”的问题。最简单的方法是加--replace。更规范的做法是在每次启动时生成一个唯一后缀作为 Runner 名称RUNNER_NAMEmodal-runner-$(date %s) ./config.sh --name ${RUNNER_NAME} --replace ...5.3 网络策略问题Modal Sandbox 默认可以访问公网但某些企业环境可能在网络层面做了限制。如果你的 Sandbox 无法下载 GitHub Actions Runner 包或者无法访问api.github.com可以先在 Sandbox 中手动执行curl -I https://github.com测试连通性。6. 最佳实践与工程建议当你已经跑通最小示例后接下来要考虑的是生产环境的工程化问题。6.1 安全边界不要让 Runner 成为“不设防”的执行环境自托管 Runner 无论运行在哪里都必须把它视为“不可信代码的执行环境”。workflow 中的代码可能是仓库贡献者提交的如果仓库允许外部 PR那么这些代码就有可能在你的 Runner 上执行。强烈建议只对受信任的仓库启用自托管 Runner。不要让 Runner 拥有访问第三方系统的高权限密钥。将密钥存储在 GitHub Secrets 中并在 workflow 中按需引用。对 Runner 网络策略做最小化设置避免它可以随意访问内网资源。6.2 使用 GitHub App 代替 PAT 管理 Runner前文提到 PAT 权限范围大不适合长期凭证管理。在团队协作中更好的方案是创建一个 GitHub App只授予特定的仓库权限。使用 installation token 动态注册 Runner。可以在 GitHub App 配置中限制其可访问的资源。这样即使某个 key 泄露影响范围也被限制在指定仓库而不是整个账号。6.3 成本控制与 Sandbox 超时策略Modal Sandbox 是按秒计费的但如果没有设置合理的超时异常场景下 Sandbox 可能长时间运行产生不必要的费用。建议根据典型 job 耗时设置合理的 Sandbox timeout。在Sandbox.create中明确设置timeout。监控 Modal 的用量报表分析是否存在长时间运行的异常 Sandbox。可以为不同任务类型设置不同的 Sandbox 镜像避免所有任务都用“全家桶”镜像导致启动缓慢。6.4 镜像与依赖管理每次 Sandbox 都从镜像启动因此“环境一致性”是 Modal Sandbox 方案的一大优势。但镜像本身需要维护把常用依赖打包进镜像避免每次 job 都执行耗时的apt install或pip install。使用 Modal 的Image分层缓存机制将稳定依赖放在镜像构建阶段把频繁变动的依赖放在 job 内安装。给镜像打上明确的版本标签方便回滚。6.5 日志与可观测性Runner 运行在 Modal Sandbox 中日志默认会输出到 Modal 的日志流里。生产环境建议在sandbox_runner.py中把 Sandbox 日志转发到集中日志平台。记录 Runner 注册结果、job 执行结果、Sandbox 存活时间等关键指标。如果某个 job 执行失败要能快速定位是 Runner 问题、Sandbox 网络问题还是构建脚本问题。6.6 多 Runner 并发与队列管理如果团队的构建任务较多可以同时启动多个 Sandbox每个 Sandbox 各跑一个 Runner。GitHub Actions 会按照标签和并发策略自动分发任务。一个简单的并发思路是在 CI 中同时运行多个sandbox_runner.py实例每个实例启动一个 Sandbox Runner。你还可以把并发数做成参数根据业务高峰动态调整。6.7 构建缓存策略self-hosted runner 和 GitHub 托管 runner 的一个差别是GitHub 托管 runner 自带 Actions 缓存服务而自托管 runner 需要自己处理缓存。在 Modal Sandbox 场景下每次都是全新环境建议使用actions/cache上传和恢复依赖缓存。缓存存放到对象存储或 GitHub Actions 的缓存服务中而不是依赖 Sandbox 本地文件系统。避免在 Sandbox 内部保留持久化状态因为 Sandbox 销毁后状态会丢失。7. 总结与学习路线这篇文章从 GitHub Actions Runner 的概念讲起介绍了 Modal Sandboxes 的核心机制然后通过一个最小示例带大家完成了“在 Modal Sandbox 上启动自托管 Runner并让 workflow 使用它”的完整流程。关键点可以归结为几句话自托管 Runner 的本质是运行在任意机器上的actions-runner程序。Modal Sandbox 提供了“按秒计费、秒级启动、用完即销毁”的容器环境。把两者结合可以让 CI 任务获得弹性的算力资源且不需要长期维护一台常驻服务器。安全上必须把 Runner 当作不可信环境来对待避免赋予过高权限生产环境推荐用 GitHub App 代替 PAT 管理 Runner 注册凭证。如果你是从零接触这套方案下一步可以在三个方向继续深入把sandbox_runner.py改造成一个可接收参数的 CLI 工具例如支持指定 CPU/GPU、label、超时时间等。研究 Modal 的镜像构建机制把常用的 Node.js、Python、Java 等构建环境镜像化做成团队共享的基础镜像。结合 GitHub App 的 installation token写一套完整的 Runner 注册生命周期管理脚本实现“按需扩容、空闲自动回收”。如果这篇文章对你有帮助可以收藏备用后续你真正落地这套方案时也可以对照本文的操作步骤和排查思路来处理问题。当然Modal 和 GitHub Actions 的产品更新比较快具体参数和界面可能会变化建议在动手前查阅官方文档。祝你的 CI 越来越快、越来越省。
返回列表