054-GitLab Runner如何配置?自动化构建与部署实战

用 GitLab 的团队迟早会碰到 Runner 这个概念。CI 配置文件写好了、流水线点下去了,结果一直卡在 "pending" 状态不动——十有八九是 Runner 没配好或者根本没注册成功。Runner 是整个 GitLab CI 体系里最容易出问题的部分,因为它不在 GitLab 服务器上,而在另一台(或几台)机器上,跨机器的信任关系、权限、网络都要自己打通。

这篇文章讲的就是把这层打通:Runner 到底扮演什么角色,怎么装,怎么注册(包括新版把 registration token 换成 runner token 这个让人困惑的变化),三种主流 executor 分别适合什么场景,.gitlab-ci.yml 的各个关键字怎么写,缓存和制品的区别在哪,最后给一个真实可用的自动部署示例。

如果你只用过 GitLab 网页界面、从没登过 Runner 机器,这篇文章应该能让你从零到能跑通一条包含构建和部署的流水线。

一、Runner 在 GitLab CI 里到底做什么

GitLab 本体是"指挥部":它管理代码、解析 .gitlab-ci.yml、决定每个 job 什么时候该跑、把结果收集起来展示。但它自己不干活——真正执行 script 里那些命令的,是 Runner。

运行过程大致是这样:开发者 push 代码,GitLab 发现仓库里有 .gitlab-ci.yml,于是创建一条流水线(pipeline),把里面的 job 按 stage 排好队。接着 GitLab 在"待命池"里找一个满足条件的 Runner——条件是标签(tag)匹配、且它挂在对应的项目或群组上。找到后把 job 的描述、代码、变量发过去,Runner 在自己的机器上准备环境、拉代码、执行脚本、回收产物,最后把状态、日志、制品传回 GitLab。

这个模型决定了几个重要事实。第一,Runner 是出站连接——它主动去连 GitLab,不需要 GitLab 反过来访问 Runner 的端口,所以在内网机器上放 Runner 是完全可行的,只要它能访问到 GitLab 地址。第二,Runner 上执行的就是脚本,它能做什么完全取决于你给它的权限,所以 Runner 的隔离设计(用 Docker 还是直接跑在宿主机上)是一个安全决策,不只是技术选型。第三,Runner 可以有多个,也可以分布在不同网络区域,这就是为什么它能同时服务"构建在云上、部署在内网"这类混合场景。

二、安装 Runner

有几种装法:官方仓库包、二进制单体文件、Docker。这里讲最常用的官方仓库包,因为它会自动生成 systemd 服务,省去自己写守护进程配置的麻烦。

# RHEL / Rocky / AlmaLinux
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh" | sudo bash
sudo dnf install -y gitlab-runner
# CentOS 7 用 yum
# sudo yum install -y gitlab-runner

# Debian / Ubuntu
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
sudo apt install -y gitlab-runner

# 验证
gitlab-runner --version

# 服务状态
sudo systemctl status gitlab-runner

安装包会把 Runner 注册成系统服务,跑在 gitlab-runner 这个用户下。这一点非常关键:Runner 之后执行的所有命令都是以这个用户的身份,它的家目录是 /home/gitlab-runner,你对目录和权限的一切设计都要围绕这个身份来做。

如果不想用包管理器,也可以直接下载二进制:

# 下载对应架构的二进制,放到 PATH 里即可
sudo curl -L -o /usr/local/bin/gitlab-runner \
  https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64
sudo chmod +x /usr/local/bin/gitlab-runner

# 手工创建用户和服务
sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
sudo gitlab-runner start

三、注册:两种 token 的区别

这是近几年变化最大、也最容易搞混的地方。老版本 GitLab 用 registration token 注册,新版本(GitLab 16.0 起,后续版本逐步不再支持前者)改用 runner authentication token,命令行参数从 --registration-token 变成了 --token。

对比项 registration token(旧) runner authentication token(新)
形式 项目/群组/实例的注册令牌 形如 glrt-xxxxxxxx 的令牌
获取位置 设置 → CI/CD → Runners 同位置新建 Runner 时一次性展示
特点 一个 token 可注册多个 Runner 一个 token 对应一个 Runner 配置
优势 批量注册方便 可单独吊销,泄露影响面小
状态 逐步弃用 推荐

新版的做法很直白:在 GitLab 界面"设置 → CI/CD → Runners → 新建项目 Runner",填上标签和描述,创建后页面会显示一个 glrt- 开头的 token,同时给出注册命令,照着敲就行。注意这个 token 只在创建时显示一次,关掉页面就看不到了,虽然可以重新创建,但老的就得作废。

# 新版(推荐):创建 Runner 时界面会直接给出这条命令
sudo gitlab-runner register \
  --non-interactive \
  --url "https://gitlab.example.com/" \
  --token "glrt-你的token" \
  --executor "docker" \
  --docker-image "alpine:latest"

旧版命令长这样,如果你面对的是老版本 GitLab,参考这段:

# 旧版:使用 registration token
sudo gitlab-runner register \
  --non-interactive \
  --url "https://gitlab.example.com/" \
  --registration-token "你的registration-token" \
  --executor "shell" \
  --description "build-runner-01" \
  --tag-list "build,deploy" \
  --run-untagged="false"

注册成功后配置会写进 /etc/gitlab-runner/config.toml,这是 Runner 的全部行为定义文件,改完要 sudo systemctl restart gitlab-runner 才生效。注册完回到 GitLab 界面,Runner 列表里应该能看到它,状态是绿色的在线。

标签(tag)是这里最值得花心思的地方。它的作用是把 job 和 Runner 对应起来:如果 .gitlab-ci.yml 里写了 tags: [deploy],那这个 job 只有带 deploy 标签的 Runner 才会接。常见做法是按用途打标签,比如 build、deploy、shell、docker,让构建任务和部署任务落在不同的机器上。如果标签写错了,症状就是 job 一直 pending,界面提示"此作业被卡住,因为没有在线 Runner 可用"。

四、executor 类型怎么选

executor 决定"job 在什么环境里执行",这是 Runner 配置里最重要的一个选择。

shell executor 直接在 Runner 所在的宿主机上跑命令,用的是 gitlab-runner 用户和它的环境。优点是简单、快、能直接访问宿主机上的工具和文件,适合部署类任务。缺点是所有 job 共用一台机器的环境,A 项目的依赖升级可能弄坏 B 项目,互相污染,而且清理不彻底。

docker executor 每个 job 起一个全新容器,用完销毁。优点是环境干净、可复现、互不干扰,还能通过 image: 指定任意基础镜像。缺点是每次拉镜像和准备环境有开销,宿主机需要有 Docker,而且 job 里如果能挂载宿主目录就是个安全隐患。

kubernetes executor 在 K8s 集群里为每个 job 起一个 Pod。优点是不用管宿主机、弹性好、能按需扩缩容,适合大规模或已经很容器化的团队。缺点是配置复杂,需要一套跑得起来的 K8s,对中小团队来说学习成本偏高。

executor 隔离性 启动速度 环境一致性 适合场景
shell 无 最快 差,依赖宿主机 单项目部署、简单脚本
docker 好 中(取决于镜像缓存) 好 绝大多数构建场景
kubernetes 最好 中 好 已有 K8s 的团队
ssh 好 中 好 需要跨机执行命令

一个务实的组合是"两个 Runner 分工":一个 docker executor 的 Runner 专门跑构建和测试,保证环境干净;另一个 shell executor 的 Runner 装在内网部署机上,专门跑部署脚本,需要访问内网资源。这样既保证了构建质量,又避免了把生产凭据放进公共构建环境。

五、.gitlab-ci.yml 的结构

这个文件放在仓库根目录,GitLab 每次 push 都会解析它。结构其实很简单:一堆 job,按 stage 分组,stage 之间有先后顺序。

# 定义流水线的阶段顺序,同名 job 会并行跑,不同 stage 串行
stages:
  - build
  - test
  - deploy

# 全局变量,所有 job 可见
variables:
  MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"

# 全局 before_script:每个 job 执行前都跑
before_script:
  - echo "开始执行,提交: $CI_COMMIT_SHORT_SHA"

build-job:
  stage: build
  image: maven:3.9-eclipse-temurin-17
  script:
    - mvn clean package -DskipTests
  # 制品:会保存到 GitLab,可在界面下载
  artifacts:
    paths:
      - target/*.jar
    expire_in: 1 week
  # 缓存:存在 Runner 本地,用于加速后续构建
  cache:
    key: "$CI_COMMIT_REF_SLUG"
    paths:
      - .m2/repository/

test-job:
  stage: test
  image: maven:3.9-eclipse-temurin-17
  script:
    - mvn test
  # 允许失败,不阻塞流水线
  allow_failure: true

deploy-job:
  stage: deploy
  tags:
    - deploy
  script:
    - echo "部署到生产"
  # 只在 main 分支上执行这个 job
  only:
    - main
  when: manual

几个必须理解的关键字。stage 决定执行顺序,是流水线的骨架。script 是真正执行的命令,任何一条返回非零就整个 job 失败。image 只在 docker/k8s executor 下有效。artifacts 和 cache 的区别是新手最容易搞混的:artifacts 是 job 的产出物,会传给后续 stage,也会上传到 GitLab 保存,用于"把构建结果交给部署环节";cache 是加速用的,存在 Runner 机器上,用于"下次构建不用重新下载依赖"。简单记:制品是给人看的、跨 stage 传的;缓存是给机器加速的、可以随时丢的。

GitLab 还注入了一批预定义变量,常用的有 CI_COMMIT_SHA(完整提交哈希)、CI_COMMIT_SHORT_SHA(短哈希)、CI_COMMIT_REF_NAME(分支或标签名)、CI_PIPELINE_ID、CI_JOB_TOKEN(临时令牌,可用来访问 GitLab API 或拉取其他私有仓库)、CI_PROJECT_DIR(代码检出目录)。做版本号、打标签、通知时都用得上。

六、缓存与制品实践

缓存配得好不好,直接决定流水线跑多久。一个 Java 项目首次构建可能要下载几百兆依赖,用缓存能把这部分省掉,第二次构建可能从五分钟降到一分钟。

缓存的 key 选择有讲究。用 $CI_COMMIT_REF_SLUG(按分支)能让不同分支各自有独立缓存,避免互相干扰;用固定的 key 则所有分支共享一份,节省空间但可能因为版本冲突互相覆盖。实践中推荐按分支加依赖描述文件的哈希,比如 key: "$CI_COMMIT_REF_SLUG-deps",并在更新依赖后主动清一次缓存。

缓存有一个反直觉的点:它默认只在同一个 Runner 上有效。如果你有多个 Runner 或者用了 Docker 容器,缓存目录需要在 Runner 配置里指定为宿主机上的固定卷,否则每次都在容器里丢失。这就是为什么很多人"明明配了 cache 却感觉没生效"。

制品则要控制体积和保留期。expire_in 必须配,否则制品会无限堆积。GitLab 管理员还应在服务器层面配置制品保留策略。对于大产物(比如几百兆的安装包),更推荐推到对象存储而不是留在 GitLab,GitLab 本身的磁盘经不起长期堆积。

七、一个可用的自动部署示例

下面是一个把 jar 构建出来、然后通过 SSH 部署到目标服务器的完整思路。部署阶段用 shell executor 的 Runner 直接在本机操作更简单,这里给 SSH 版本,因为它更通用。

stages:
  - build
  - deploy

build:
  stage: build
  image: maven:3.9-eclipse-temurin-17
  script:
    - mvn clean package -DskipTests
    - ls -lh target/
  artifacts:
    paths:
      - target/app.jar
    expire_in: 3 days

deploy:
  stage: deploy
  tags:
    - deploy
  script:
    # 1) 复制产物到目标服务器
    - scp -o StrictHostKeyChecking=no target/app.jar deploy@10.0.0.10:/opt/app/app.jar.new
    # 2) 原子替换并重启,失败则回滚
    - ssh deploy@10.0.0.10 "
        set -e;
        cp /opt/app/app.jar /opt/app/app.jar.bak;
        mv /opt/app/app.jar.new /opt/app/app.jar;
        sudo systemctl restart myapp;
        sleep 5;
        systemctl is-active myapp || (mv /opt/app/app.jar.bak /opt/app/app.jar && sudo systemctl restart myapp && exit 1);
      "
  only:
    - main

这段脚本里有两个值得学的设计。一是先传到临时文件再 mv 替换,mv 在同一个文件系统内是原子操作,避免出现"文件传了一半服务就读到了"的情况。二是替换前先备份,重启后做健康检查,不健康就自动还原并让 job 失败——这样 GitLab 上会亮红灯,人立刻知道出事了。

凭据不能明文写在 yml 里,要用 GitLab 的 CI/CD Variables(设置 → CI/CD → 变量),把 SSH 私钥存成类型为 File 的变量,脚本里用 $DEPLOY_KEY 引用。变量可以勾选"受保护",这样只有受保护分支的流水线才能读到,是多环境隔离的标准做法。

风险提示:这份示例里的 mv、systemctl restart 都是会影响线上服务的操作,正式使用前请先在测试环境完整验证一遍,尤其要确认回滚分支真的能跑通,而不是等出事时才发现备份文件根本不存在。另外 StrictHostKeyChecking=no 会跳过主机指纹校验,方便但降低了安全性,生产环境建议预先写入 known_hosts。

常见问题

Q:job 一直 pending,提示没有可用 Runner,怎么办?

按顺序查:Runner 服务在不在线、config.toml 里的 URL 和 token 对不对、job 的 tags 和 Runner 的 tag-list 是否匹配、这个 Runner 有没有"允许接收未打标签的作业"(run_untagged)。其中 tag 不匹配占了绝大多数。

Q:Runner 注册时报 403 或 "invalid token"?

先确认 GitLab 版本和 token 类型是否匹配——新版必须用 glrt- 令牌配 --token,拿旧的 registration token 去配会失败。另外确认 URL 结尾有没有多余的斜杠、token 是否粘贴完整、有没有被换行截断。

Q:.gitlab-ci.yml 改了但流水线行为没变?

检查有没有语法错误,GitLab 界面上的 CI Lint 工具(项目 → CI/CD → 编辑器)可以校验。另外注意流水线用的是触发时的配置,改完要重新触发才会生效。如果配置里用了 include,被包含的文件也要检查。

Q:docker executor 下脚本里要用 Docker 命令,报找不到?

需要在 config.toml 的 [runners.docker] 段里挂载 /var/run/docker.sock 或配置 dind(Docker in Docker)服务。挂 socket 相当于给容器宿主机 root 权限,安全性要评估;dind 更隔离但配置更麻烦。

Q:部署 job 里的 SSH 一直免密失败?

Runner 跑在 gitlab-runner 用户下,私钥要放在这个用户能读到的位置且权限 600,known_hosts 也要针对这个用户配置。用 sudo -u gitlab-runner ssh -v ... 在 Runner 机器上手工验证一次,比在流水线里瞎猜快得多。

总结

Runner 本身不复杂,出问题通常出在三个地方:注册时的 token 类型、job 和 Runner 的标签匹配、以及 Runner 服务账号的权限与环境。

配置时记住这几条:新版 GitLab 统一用 glrt- 令牌注册,老命令只适用于旧版本;executor 上,构建用 docker 保证环境干净,部署用 shell 或 ssh 以便访问内网;缓存按分支设 key 并确认位于持久化路径,否则白配;制品一定要设 expire_in,避免磁盘长期堆积。

.gitlab-ci.yml 的门槛比想象中低,掌握 stages、script、artifacts、cache、only、when 这几个关键字就能覆盖大部分场景。真正的功夫在部署脚本的健壮性上:原子替换、备份、健康检查、失败自动回滚,这四条做到了,才敢让流水线自动碰生产环境。