当前位置: 首页 > news >正文

GitHub Actions 自定义 Runner 镜像实战:把初始化环境提前做好

前言

我以前优化 GitHub Actions 时,最先看的通常是缓存。Node 项目就看setup-node的缓存,Java 项目就看 Maven 或 Gradle 缓存,Docker 项目就看 layer cache。大部分项目做到这一步,CI 时间已经能降下来不少。

但有些项目不只是安装依赖慢。它们每次运行都要安装一堆系统工具、配置内部证书、下载私有二进制文件、准备 SDK、安装浏览器依赖,甚至还要拉内部网络里的基础包。这个时候继续堆actions/cache会有点别扭,因为你想缓存的已经不只是包管理器目录,而是整个 Runner 环境。

自定义 Runner 镜像解决的是这个层面的问题。它允许组织或企业为 GitHub-hosted larger runners 准备一套预热环境,把工具、依赖和配置提前放进镜像里。后续 job 启动时,Runner 直接从这个环境开始,不再每次从干净系统里重新装一遍。这个能力目前面向使用 GitHub Team 或 GitHub Enterprise Cloud 的组织和企业,依附于 GitHub-hosted larger runners。

它不适合所有项目。普通 Web 项目如果只是npm ci慢,先用依赖缓存就够了。自定义镜像更适合那些初始化步骤复杂、构建频率高、团队规模比较大,而且已经在使用 larger runners 的项目。

一、先判断是不是镜像问题

我不会一看到 CI 慢就直接上自定义镜像。

GitHub Actions 里有几类慢法,处理方式不一样。

依赖下载慢,先用包管理器缓存。

-uses:actions/setup-node@v6with:node-version:24cache:npmcache-dependency-path:package-lock.json

构建产物要传给后续 job,用 artifact。

-uses:actions/upload-artifact@v7with:name:web-distpath:dist/retention-days:7

Docker 构建慢,用 layer cache。

-uses:docker/build-push-action@v6with:context:.push:falsecache-from:type=ghacache-to:type=gha,mode=max

自定义 Runner 镜像处理的是另一类问题:每次 Runner 启动后都要重复准备系统环境。

比如下面这些步骤,如果每次 CI 都跑一遍,时间会很快堆上去。

sudoapt-getupdatesudoapt-getinstall-ylibgtk-3-0 libnss3 libxss1curl-Lhttps://internal.example.com/tools/buildkit.tar.gz|tar-xz-C/opt ./scripts/install-internal-certificates.sh ./scripts/install-mobile-sdk.sh

这些内容和项目依赖不同。它们更像“构建机器应该自带的环境”。如果每天跑几十次、几百次工作流,每次都从头装,确实会浪费很多时间。

我会先看 Actions 日志,把耗时拆开:

Set up job 花了多久 安装系统依赖花了多久 语言依赖安装花了多久 构建和测试本身花了多久 镜像构建花了多久

如果慢在系统工具和基础环境准备,自定义镜像才值得考虑。GitHub-hosted larger runners 的自定义镜像可以预装工具、依赖和配置,后续 job 会从预热环境启动,减少重复下载和安装。

二、snapshot 要写在 job 上

自定义镜像的生成流程并不复杂,但原稿里有一个容易误导的地方:snapshot不是放在某个 step 后面,而是放在 job 级别。

一个最小示例大概这样:

name:Build custom runner imageon:workflow_dispatch:schedule:-cron:'0 3 * * 1'timezone:'Asia/Shanghai'jobs:build-node-image:runs-on:my-image-generation-runnersnapshot:node-web-cisteps:-uses:actions/checkout@v6-name:Install system packagesrun:|sudo apt-get update sudo apt-get install -y libgtk-3-0 libnss3 libxss1-name:Install Node.js toolingrun:|corepack enable npm install -g pnpm@latest-name:Install internal toolsrun:|sudo mkdir -p /opt/internal-tools curl -L https://example.com/tools/release-tool.tar.gz \ | sudo tar -xz -C /opt/internal-tools-name:Record image contentsrun:|node --version pnpm --version ls -la /opt/internal-tools

这里的runs-on必须指向一个 image-generation runner。这个 Runner 要先在组织或企业里创建,并且平台要和目标镜像一致。目前支持 Linux x64、Linux ARM64、Windows x64。创建镜像生成 Runner 时,可以从 GitHub-owned image 开始,也可以选择干净的系统镜像作为基础。

snapshot: node-web-ci表示这个 job 成功完成后,GitHub 会基于当前环境生成一个名为node-web-ci的自定义镜像。如果一个 workflow 里有多个 job 都写了snapshot,每个 job 都会生成独立镜像。想只生成一个镜像,就把镜像准备步骤放在同一个 job 里。

也可以用 mapping 语法指定主版本:

jobs:build-node-image:runs-on:my-image-generation-runnersnapshot:image-name:node-web-civersion:2.*steps:-uses:actions/checkout@v6-run:./scripts/prepare-runner-image.sh

这里可以指定 major version,minor 版本会自动递增。patch 版本不支持。比如version: 2.*下面后续可能出现2.02.1这类版本。

我更倾向把镜像准备脚本放进仓库里,比如:

.github/runner-images/node-web-ci/ prepare.sh README.md tools.lock

这样镜像里装了什么,不会只存在于某个 Actions 日志里。团队以后排查环境差异,也能从仓库里找到来源。

三、镜像要按版本用,不要一直追最新

镜像生成后,需要创建或修改 GitHub-hosted larger runner,让它使用这个自定义镜像。创建 Runner 时要选择同样的平台,在 Custom tab 里选择镜像,再选择Latest或某个具体版本。Runner 的存储大小也要能容纳镜像大小,比如镜像是在 8-core Runner 上生成的,运行时就要选择 8-core 或更大的 Runner。

使用镜像的 workflow 反而很简单:

name:Web CIon:pull_request:jobs:test:runs-on:node-web-ci-runnersteps:-uses:actions/checkout@v6-run:pnpm install--frozen-lockfile-run:pnpm test

这里的runs-on写的是安装了自定义镜像的 larger runner 名称,不是镜像名称。

版本策略要提前想好。

如果 Runner 选择Latest,新镜像生成后会自动跟上最新版本。这个方式适合开发环境或测试仓库。镜像每周更新一次,Runner 自动拿到新版环境,维护成本低一些。

生产关键 workflow 我更愿意锁定具体版本。镜像更新后,先让测试仓库跑一轮,再手动把生产 Runner 切过去。这样慢一点,但不会因为某次镜像更新把核心 CI 全部带坏。

一个可操作的节奏是:

每周一凌晨生成新镜像 测试仓库先使用 Latest 跑完整 CI 确认没问题后,把生产 Runner 从 1.4 切到 1.5 保留最近几个可回滚版本 清理长期不用的旧版本

自定义镜像每次成功运行带snapshot的 job 都会生成新版本。如果镜像构建很频繁,又保留很多历史版本,Actions storage 会增长得很快。使用镜像的 job 按对应 larger runner 的每分钟费率计费,镜像存储另外计费。

四、镜像里别放太多东西

自定义镜像最容易变成“万能环境”。

一开始只是为了前端项目加 Node、pnpm、浏览器依赖。后来又塞 Python、Java、Android SDK、内部部署工具、数据库客户端。再往后,所有项目都想用同一个镜像。镜像越来越大,生成越来越慢,安全面也越来越宽。

我不太建议做这种通用大镜像。

更稳的方式是按项目类型拆。

node-web-ci:Node.js、pnpm、浏览器测试依赖 python-api-ci:Python、Poetry、内部证书、数据库客户端 java-service-ci:JDK、Maven、Gradle、内部 Maven 配置 mobile-android-ci:JDK、Android SDK、构建工具

每个镜像只放这类项目真正需要的工具。这样镜像更小,生成日志也更容易看。

镜像内容最好写一份清单。比如在仓库里放README.md

# node-web-ci ## Included - Ubuntu base image - Node.js 24 - pnpm latest through Corepack - Chromium runtime dependencies - Internal release tool under /opt/internal-tools - Company root CA certificates ## Not included - Project node_modules - Production secrets - Deployment credentials - Application build output

尤其不要把生产密钥、token、云服务凭据写进镜像。镜像是环境基础,不应该成为密钥仓库。需要凭据的地方继续用 GitHub Actions secrets、OIDC、environment secrets 等机制。

这里还有一个安全边界:生成生产镜像的 runner group 不要和开发、测试仓库共用。能够访问 image-generation runner 的仓库,有机会影响后续镜像内容。生产镜像生成 Runner 应该放在专门的 runner group 里,并且不要开放给 public repositories。

五、它和 Docker 镜像不是一回事

很多团队已经在 CI 里使用 Docker 镜像,于是会问:既然 Docker 能做统一构建环境,还要自定义 Runner 镜像做什么?

这两个东西解决的问题不一样。

Docker 镜像适合把构建步骤放进容器。你可以在 GitLab CI、CircleCI、本地 Docker、Kubernetes 里复用同一个镜像。它的可移植性强,也更适合跨平台 CI 策略。

GitHub Actions 自定义 Runner 镜像则是 GitHub-hosted larger runners 的预热环境。它让整个 Runner 以预配置状态启动,包括系统层工具、证书、SDK、二进制文件和 Runner 运行环境。它更贴近 VM 级别的构建机快照。

我会这样判断:

希望跨 CI 平台复用环境,优先 Docker 镜像 只在 GitHub Actions larger runners 里使用,且初始化系统环境很重,可以考虑自定义 Runner 镜像 需要访问宿主机级别工具、系统包、内部证书,自定义 Runner 镜像更合适 只是应用依赖下载慢,先用 cache

如果你已经有成熟的 Docker 构建镜像,而且 CI 主要跑在容器里,自定义 Runner 镜像不一定能带来太多收益。它适合那些容器之外还要准备大量 Runner 系统环境的工作流。

六、迁移时先挑最慢的一条流水线

我不建议一上来全组织推广。

更稳的做法是先挑一条最慢、最稳定、收益最容易衡量的流水线。

比如一个前端 E2E 项目,每次都安装浏览器依赖和内部测试工具,可以先做node-e2e-ci镜像。迁移前先记录三组数据:

Set up job 耗时 安装系统依赖耗时 整体 CI 耗时 失败率和失败原因

迁移后再看同样的数据。不要只看一次运行,要看一周。因为镜像生成、缓存命中、队列状态、Runner 负载都会影响单次结果。

迁移流程可以这样走:

选一个耗时明显的项目 写镜像准备 workflow 创建 image-generation runner 生成第一版镜像 创建测试 runner 使用该镜像 在测试仓库跑完整 CI 让一个非关键 PR 使用新 runner 观察一周 再决定是否推广到核心 workflow

同时保留回退路径。不要把旧 workflow 立刻删干净,可以先保留一段时间:

jobs:test-standard:if:${{vars.USE_CUSTOM_IMAGE!='true'}}runs-on:ubuntu-lateststeps:-uses:actions/checkout@v6-uses:actions/setup-node@v6with:node-version:24cache:npm-run:pnpm install--frozen-lockfile-run:pnpm testtest-custom-image:if:${{vars.USE_CUSTOM_IMAGE == 'true'}}runs-on:node-web-ci-runnersteps:-uses:actions/checkout@v6-run:pnpm install--frozen-lockfile-run:pnpm test

这段配置不是长期方案,但迁移期有用。出问题时改一个变量,就能退回标准 runner。

总结

GitHub Actions 自定义 Runner 镜像适合处理“每次都要重新准备 Runner 环境”的问题。它可以把系统工具、语言运行时、内部证书、自定义二进制文件和 SDK 提前放进 GitHub-hosted larger runner 的镜像里,让后续工作流从预热环境启动。

它不应该替代所有缓存策略。依赖下载慢,先看setup-nodesetup-pythonsetup-javaactions/cache。构建产物传递,用 artifact。Docker 构建慢,用 layer cache。只有系统环境准备本身占了很多时间,而且项目已经在使用 larger runners,自定义镜像才更值得投入。

真正落地时,要把镜像当作构建产物管理。镜像内容写进仓库,定期生成,测试后推广,生产 Runner 尽量锁定版本,旧版本保留一段时间,生成 Runner 放在专门的 runner group 里。这样它才能帮 CI 少做重复初始化,而不是变成另一套难维护的基础设施。

http://www.jsqmd.com/news/888196/

相关文章:

  • 音频运放与电阻测试平台:标准化设计与实测指南
  • 2026年知名的冷库板/冷库工程/冷库安装/冷库维修优质厂家汇总推荐 - 行业平台推荐
  • 创建了安卓模拟器却运行不了,改GVM为aehd成功了
  • 2026年质量好的济南生物质壁炉/嵌入壁炉/燃木壁炉/颗粒取暖壁炉厂家综合对比分析 - 品牌宣传支持者
  • A/B测试与Split平台:数据驱动决策的实践指南
  • 七天掌握全栈开发:Next.js + TypeScript + tRPC 实战学习系统
  • 嵌入式通信连接器(ECC)设计:统一接口规范与旋转连接技术
  • 手把手教你用Python解析GY-95T IMU原始数据包:从十六进制流到ROS2 sensor_msgs/Imu消息
  • IDEA Diagrams保姆级教程:5分钟看懂Java类图,还能一键定位源码
  • 构建分布式Saga智能体:从状态机到可观测性的工程实践
  • 5分钟配置GitHub汉化插件:让英文界面秒变中文的实战应用指南
  • Docker 部署 MongoDB 的可重现性实践与生产就绪指南
  • 2026年比较好的别墅电梯/曳引别墅电梯/无障碍别墅电梯推荐厂家精选 - 品牌宣传支持者
  • 60项核心功能深度解析:HsMod如何彻底改变炉石传说游戏体验
  • 手把手教你用 zcat 和 zgrep 玩转 /proc/config.gz:内核调试必备的5个技巧
  • Unity UGUI性能优化实战:用UIEffect替代传统粒子,实现轻量级屏幕过渡与高级模糊
  • 告别网络卡顿:RouterOS负载均衡配置全解析,从Mangle规则到DHCP设置的保姆级教程
  • JWT攻防实战:5种高危漏洞利用手法详解
  • 2026年比较好的真火壁炉/别墅取暖壁炉用户口碑推荐厂家 - 品牌宣传支持者
  • Qt5.12.9属性表控件实战:手把手教你定制一个仿Qt Designer的配置面板
  • 从语音合成实战出发:ConvTranspose1d在Tacotron2等模型里到底是怎么‘拉长’梅尔频谱的?
  • 深度学习硬件加速:地址中心化数据流与VPU协同设计
  • AI Coding时代:淘汰你的不是AI,是会用AI的同行
  • 别再只盯着频率了!手把手教你读懂DDR内存条标签上的‘2Rx8’、‘PC3-10600S’到底啥意思
  • SymPy符号计算入门:保真推导与工程化实践
  • Unity Aseprite Importer:像素动画工作流的语义级导入方案
  • 基于Kotlin与Jetpack Compose构建本地AI提示词管理工具
  • 2026年比较好的紫铜线/黄铜线/铜线/铍铜线可靠供应商推荐 - 行业平台推荐
  • 2026年知名的自建房家用电梯/山东观光家用电梯/家用电梯/别墅家用电梯公司选择指南 - 行业平台推荐
  • AWS Bedrock多代理系统集成Agent Veil Protocol实现动态信任门控委托