症状:iOS 流水线排队、签名凭证混在普通任务里,重启后 Runner 又离线。
最快解法:先部署一台只承载可信项目的专用 macOS 节点,完成构建、签名与重启恢复试点,再依据真实队列数据扩容。
这篇文章适合正在把 iOS 项目迁入 GitLab CI/CD、需要补齐 macOS Runner 的平台工程负责人;也适合负责代码签名、网络隔离和凭证审计的企业 IT 或安全负责人。如果你正在比较购买、租赁或混合配置 Mac 构建节点,下面的时间线可以直接作为实施计划。
第一步:先划定构建节点的边界
不要一开始就把 macOS Runner 注册为实例级 Runner,也不要让所有仓库都能调用它。GitLab 官方明确提醒,Shell executor 的隔离能力有限,任务可以使用 Runner 用户权限,并可能读取同一主机上其他项目的数据,因此只适合可信代码。(GitLab Shell executor 安全说明)
部署前先回答 4 个问题:
- 哪些仓库由内部团队维护,依赖来源可审计?
- 开发测试、夜间构建和正式发布是否需要同一套凭证?
- 当前项目需要一个固定 Xcode 版本,还是需要并行维护多个版本?
- 峰值期间是并发任务增加,还是单个项目构建时间过长?
建议把节点先分成两类:
✅ 普通构建节点:运行编译、单元测试、静态检查和模拟器测试,不保存生产发布证书。
⚠️ 发布签名节点:只接收受保护分支或受保护标签任务,保存或临时加载发布所需签名材料。
如果你的团队只有一个项目,可以从一台节点开始。但这台机器仍应是“专用节点”,而不是开发者日常办公机。开发工具升级、个人登录状态和临时脚本都会改变构建结果,也会增加审计难度。
第二步:在 macOS 上准备专用运行账号
GitLab Runner 在 macOS 上以用户模式 LaunchAgent 运行。它随当前登录用户启动并停止,配置通常位于该用户的 ~/.gitlab-runner/config.toml,同时需要用户钥匙串和图形会话来支持代码签名及 iOS Simulator。GitLab 官方不支持把它作为系统级 LaunchDaemon 运行。(GitLab macOS 安装文档)
这会带来一个容易被忽略的运维条件:“主机开机”不等于“Runner 可用”。
准备顺序建议如下:
- 创建专用本地运行账号,不使用个人 Apple Account,也不使用日常管理员账号。
- 通过 SSH 或远程管理入口完成基础配置,但不要只依赖 SSH 会话安装和验证 Runner。
- 为该账号配置最小必要权限,避免把长期使用的管理员密码写入脚本。
- 开启磁盘加密,并把 FileVault 恢复密钥放到与主机分离的企业密钥托管位置。Apple 文档说明,组织可以通过设备管理服务托管个人恢复密钥。(Apple FileVault 部署文档)
- 规划重启后的自动登录、远程解锁或现场救援路径。
自动登录可以减少重启后 Runner 长时间离线,但会改变物理访问风险。对于保存发布签名材料的节点,不能只看“自动登录是否方便”,还要确认主机所在机房、远程控制通道和磁盘解锁流程是否符合企业安全要求。
第三步:锁定 Apple Silicon、macOS 与 Xcode 基线
GitLab Runner 官方支持在 Apple Silicon 和 Intel Mac 上安装。对于新建的 iOS 构建环境,Apple Silicon 通常更容易与当前工具链保持一致,但你仍要以项目依赖、插件兼容性和历史产物为准,不要仅凭芯片名称采购。(GitLab macOS Runner 支持说明)
在安装依赖前,先记录这组基线:
- macOS 版本与系统架构;
- Xcode 版本及其完整路径;
xcodebuild -version输出;- Swift、Ruby、CocoaPods、Fastlane 或其他构建工具版本;
- 可用磁盘空间和工作目录位置;
- 默认 Shell 与区域设置。
Apple 的文档指出,完整 Xcode 才包含 xcodebuild 和 xcrun;单独安装 Command Line Tools 并不能替代完整 Xcode。安装后还要选择正确的活动开发者目录。(Apple Command Line Tools 文档)
一个足够小的检查骨架如下:
uname -m
sw_vers
xcodebuild -version
xcode-select -p
gitlab-runner --version
如果主机上并存多个 Xcode 版本,不要让流水线依赖开发者手工切换。可以把每个节点绑定一个明确的 Xcode 版本,并用 Runner tag 体现能力,例如 macos-arm64-xcode-main。如果项目确实需要多版本并行,优先拆成多个节点,而不是让同一节点在任务之间反复切换开发者目录。
第四步:注册 Runner,并把任务路由到正确节点
Runner 的暴露范围通常有项目级、群组级和实例级。企业部署建议从项目级开始;当多个项目确实属于同一可信边界,再升级到群组级。实例级 Runner 只有在所有可访问仓库都经过同等安全审查时才考虑。GitLab 官方说明,Runner 的 scope 会决定哪些项目可以使用它,tags 则是任务筛选可用 Runner 的主要方式。(GitLab Runner 注册与路由文档)
注册时重点控制 4 项:
- executor:iOS 本机构建使用
shell; - tags:标出系统架构、Xcode 版本和用途;
- run untagged:关闭,避免普通任务误入;
- protected:发布节点开启,只接受受保护分支或标签。
最小配置骨架可以是:
gitlab-runner register \
--url "https://gitlab.example.com/" \
--token "$RUNNER_AUTHENTICATION_TOKEN" \
--executor "shell" \
--description "ios-build-arm64" \
--tag-list "macos,arm64,ios-build" \
--run-untagged="false" \
--access-level="ref_protected"
命令中的地址、Token 和 tag 需要替换为你的 GitLab 环境。新建流程应优先使用 Runner authentication token;GitLab 已将旧的 registration token 流程标记为弃用,并计划移除旧参数。(GitLab Runner 新建流程说明)
.gitlab-ci.yml 中也必须显式指定 tags:
ios_build:
stage: build
tags:
- macos
- arm64
- ios-build
script:
- xcodebuild -version
- xcodebuild -workspace App.xcworkspace \
-scheme App \
-sdk iphoneos \
-configuration Release \
build
不要只在界面中看到 Runner 显示在线就宣布注册完成。至少要验证任务是否被正确路由、未打 tag 的任务是否被拒绝,以及普通分支是否无法触发发布节点。
第五步:用最小流水线打通构建、测试和产物
首条流水线不要直接接正式发布。先完成一条不接生产签名的验证链:
- 拉取代码与锁定依赖。
- 输出 macOS、架构、Xcode 和工具链版本。
- 执行一次干净构建。
- 执行单元测试或指定的模拟器测试。
- 保存测试结果与构建日志。
- 归档一个可识别的构建产物。
- 清理工作目录、缓存和临时文件。
这一步的目标不是追求最快,而是确认环境可重复。建议把失败日志保存为试点证据,至少记录失败发生在依赖安装、编译、测试、签名还是归档阶段。
缓存要分开看:
✅ CocoaPods、Swift Package 或其他公共依赖缓存,通常有明确的速度收益。
⚠️ 包含项目私有文件、认证信息或生成配置的缓存,可能把一个项目的数据带到另一个项目。
❌ 不要把证书、描述文件、钥匙串导出包或包含 Token 的目录放进跨项目缓存。
Shell executor 的工作目录会在主机上持续存在。GitLab 文档也提醒,使用 Shell executor 时,跨任务保留的全局 Git 配置和认证信息可能造成凭证残留或不同任务之间的安全问题。(GitLab CI 子模块与 Runner 工作目录说明)
第六步:把 iOS 签名材料从普通构建中隔离
代码签名不是“把证书文件放到 Runner 上”这么简单。真正需要管理的是证书、私钥、描述文件、Keychain 解锁状态、CI/CD 变量权限和构建日志泄露风险。
推荐采用以下边界:
- 普通测试节点不保存发布私钥;
- 发布流水线只允许受保护分支或受保护标签触发;
- 证书、描述文件和密码存放在受保护的 CI/CD 变量中;
- 流水线运行时创建临时 Keychain;
- 导入签名身份后只授予必要工具访问权限;
- 构建完成后删除临时 Keychain、导出包和描述文件;
- 检查日志,确认没有打印变量、证书路径中的敏感信息或解锁命令。
Apple 文档说明,Xcode 和命令行工具都可以参与签名流程,签名身份由 Keychain 等安全存储提供。(Apple 代码签名文档)
你可以把签名任务单独设置为:
ios_release:
stage: release
tags:
- macos
- ios-release
rules:
- if: '$CI_COMMIT_TAG'
script:
- ./ci/import-temporary-signing-assets.sh
- xcodebuild archive
- ./ci/export-ipa.sh
- ./ci/cleanup-signing-assets.sh
脚本名称只是示例。真正上线前,要审查每个脚本是否会把密码写入 Shell history、临时目录、崩溃日志或归档产物。
⚠️ 经验提醒:如果同一台机器既运行来自外部贡献者的合并请求,又持有发布签名私钥,那么“分支保护”并不能自动解决所有风险。最稳妥的做法是让普通构建节点与发布签名节点物理或逻辑分离。
生产前 FAQ:几个容易被低估的部署问题
macOS 重启后,Runner 怎样自动回到可用状态?
它需要已登录的用户会话,因为 macOS 模式使用 LaunchAgent,而不是系统级后台服务。你必须验证自动登录、用户会话恢复、钥匙串可用性和 Runner 自动上线。重启后只显示主机在线,但 Runner 没有接到测试任务,仍然不能算恢复成功。
iOS 项目在 GitLab 上构建,Shell 是否更合适?
对于需要直接访问 Xcode、模拟器和 macOS 钥匙串的任务,Shell executor 是常见选择。但它不是强隔离环境。只允许可信项目进入,并通过项目或群组 scope、tags、受保护分支和受保护标签控制任务来源。
一台 macOS Runner 能不能服务多个项目?
可以,但要先确认项目之间的代码访问权限、依赖来源、工作目录清理方式和签名材料边界。多个普通测试项目可以共用节点;发布项目、外部贡献代码和高敏感项目不应默认共享同一台 Shell Runner。
iOS 证书和描述文件放在 Runner 上,怎样降低泄露风险?
建议把签名材料作为受保护变量注入,在临时 Keychain 中完成导入和构建,结束后执行清理。不要把证书放在仓库、共享缓存或永久工作目录。发布节点还应限制可触发任务的分支和标签。
Mac 构建节点的数量,应该依据什么来定?
先部署一台可信节点,连续记录队列等待、构建耗时、失败率、磁盘增长和 Xcode 切换造成的阻塞。只有当排队持续影响发布窗口,或多个版本和安全域互相干扰时,才增加节点。节点数量应由流水线数据决定,而不是由开发者人数直接推算。
第七步:完成一周无人值守恢复测试
生产准入前,至少安排一次计划内重启和一次模拟故障恢复。测试项目不要只包含“重启后 Runner 是否显示在线”,还要覆盖:
- 主机重启后是否进入预期用户会话。
- FileVault 解锁路径是否有人负责。
LaunchAgent是否自动加载。- Runner 是否能从 GitLab 接收新任务。
- Xcode、Keychain 和模拟器是否可用。
- 远程 SSH 或 VNC 是否仍能进入。
- 任务中断后工作目录是否被清理。
- 失败日志是否仍能被平台团队获取。
把每个结果记录为“通过、失败、需要人工介入”。如果恢复依赖某位工程师在办公室按电源键,这台机器就不应被描述为无人值守节点。
安全加固还应包括网络分段、最小 SSH 权限、Runner Token 轮换、CI/CD 变量审计、构建后清理和变更审批。企业如果需要更完整的检查路径,可以参考 企业远程 Mac 构建机安全验收清单,将节点访问、日志和凭证项目纳入同一份验收记录。
稳定运行后,怎样决定扩容方式?
先用下面的条件分支做决定:
- 若只有一个 Xcode 版本、任务峰值低、队列等待可接受:保留单节点,优先完善恢复和清理流程。
- 若多个项目只做普通测试,且代码来源可信:可以增加共享测试节点,但继续关闭未打 tag 任务。
- 若发布任务与普通测试互相阻塞:增加独立发布签名节点,不要只提高单机并发。
- 若不同 Xcode 版本需要长期并行:按 Xcode 版本建立节点池,避免流水线运行中反复切换。
- 若发布高峰明显、节点需求按周期变化,或你没有现场运维能力:优先评估可按周、月或季度扩容的远程 Mac。
- 若数据合规要求节点必须在指定地点或必须接入物理设备:优先考虑自购或混合架构,并把远程节点用于非敏感测试任务。
容量记录至少保留这些字段:队列等待时间、任务持续时间、同时运行任务数、失败重试次数、磁盘增长、Xcode 升级停机时间和人工恢复耗时。你可以进一步阅读 iOS CI/CD 并发容量估算,再把记录转换成年度节点需求。
成本不要先写成“购买一定便宜”或“租赁一定便宜”。可以使用这个变量公式:
年度自购成本 = 硬件采购价 + 配件与托管 + 运维人力 + 故障替换成本 + 折旧成本
年度租赁成本 = 周期租金 × 使用周期 + 网络与存储附加成本 + 企业安全接入成本
把实际报价、合同周期、节点交付方式和数据出口要求填入内部采购表后,再比较自购、远程租赁和混合部署。若你要查看远程 Mac 的区域与套餐入口,可先从 KVMNODE 的远程 Mac 方案了解可选路径,但最终节点数量仍应以试点流水线记录为准。
最终 Go/No-Go:什么时候可以进入生产?
✅ Go:
- 可信项目可以稳定拉取和构建;
- Xcode 版本与依赖基线已记录;
- 普通构建与发布签名任务已经分离;
- 受保护分支和标签路由验证通过;
- 临时 Keychain 能创建并清理;
- 重启后 Runner、用户会话和远程入口均恢复;
- 队列与构建日志可以被平台团队审计;
- 已明确下一次扩容的触发条件。
❌ No-Go:
- 所有仓库都能调用同一台 Shell Runner;
- 发布证书长期放在普通构建节点;
- 重启后需要人工到现场登录;
- 只验证“控制台在线”,没有执行真实流水线;
- 缓存中可能包含跨项目凭证;
- Xcode 版本靠人工临时切换;
- 没有队列等待和构建耗时记录。
对企业来说,最危险的方案不是节点少,而是边界不清、恢复路径不明、签名权限过宽。先用小规模真实流水线完成试点,再决定长期节点数量和租赁周期,通常比一次性采购整批 Mac 更容易控制风险。
如果你的现有方案是开发者各自维护本地 Mac,常见缺点是环境漂移、设备闲置与峰值不足;如果直接购买多台 Mac,又会提前承担折旧、硬件故障、场地托管和版本切换成本。完成单节点验证后,你可以申请一台 KVMNODE 远程 Mac,把峰值并发、Xcode 版本、发布频率和恢复记录带入完整试点;用真实流水线决定是否长期租赁,以及应该保留多少台生产节点。