GitLab Runner避坑:从手动发布到自动化的3次进化

3年前,我曾以为“自动化部署”是大厂的专利,直到我在周五晚上因为传错了一个jar包,被迫在机房待到了凌晨2点。

那时我还在一家几十人的软件外包公司,每次发布版本,整个流程充满了“原始部落”的气息:本地打包 -> 打开FTP/Xshell -> 停止服务 -> 备份 -> 上传新包 -> 重启。

这一套动作,熟练的运维可能只要10分钟,但如果是一个刚入职的开发,或者是疲惫的周五下午,出错率几乎是100%。

也就是那个凌晨,我痛定思痛,决定必须把CI/CD(持续集成/持续部署)这块骨头啃下来。今天我想站在一个“过来人”的角度,聊聊如何利用 GitLab Runner 搭建一套私有化的自动化流水线。不讲枯燥的概念,只聊我踩过的坑和落地的实操。


一、 为什么要自己搞Runner?(别信“共享”的鬼话)

刚开始接触GitLab CI的时候,我犯过一个新手常犯的错:直接用GitLab官方提供的Shared Runners(共享构建器)。

起初觉得挺香,不用配置服务器,提交代码自动跑。但很快,痛点就来了:

  1. 慢到怀疑人生:作为免费用户,排队是常态。有时候改一行代码,等Runner分配资源要等10分钟,黄花菜都凉了。
  2. 环境不可控:今天要用Node 14,明天项目升级要Node 18,共享环境里各种依赖冲突,每次构建都在“赌运气”。

真实的转折点发生在一次紧急热修复。客户系统崩了,我代码改好了,结果卡在GitLab的共享队列里排队了半小时。老板在后面站着,我在前面汗流浃背。

那一刻我明白:只有私有化部署Runner,才能把控制权握在自己手里。

简单理解,GitLab Server是“包工头”,负责发号施令;GitLab Runner就是“搬砖工”,负责干活。我们现在要做的,就是雇一个只听命于我们自己的“搬砖工”,安在自己的服务器上。

配图


二、 只要两步,给你的代码配个“私人管家”

很多人被官方文档里长篇大论的安装劝退。其实,对于中小团队,最稳妥、最干净的方式只有一种:Docker安装

我大概每周三下午会抽空检查一遍基础设施,用了两年Docker方式部署Runner,它最大的好处是——即使搞挂了,删了容器重来就行,不会污染宿主机环境。

第一步:启动Runner容器

别去折腾什么RPM包安装了,直接在你的服务器(可以是测试服)上跑这个:

docker run -d --name gitlab-runner --restart always \
  -v /srv/gitlab-runner/config:/etc/gitlab-runner \
  -v /var/run/docker.sock:/var/run/docker.sock \
  gitlab/gitlab-runner:latest

这行命令里有个核心细节:-v /var/run/docker.sock:/var/run/docker.sock

踩坑预警:如果你希望你的Runner能在构建过程中也能使用Docker(比如打包镜像),这个挂载必须加!这叫“Docker in Docker”或者是让容器控制宿主机的Docker守护进程。

第二步:把“管家”介绍给“包工头”(注册)

Runner启动了,但它还不知道要听谁的指挥。你需要去GitLab项目页面 -> Settings -> CI/CD -> Runners,找到 URLRegistration token

然后进入容器内部执行注册:

docker exec -it gitlab-runner gitlab-runner register

配图

接下来会是一次简单的问答交互,我建议的配置如下:

  1. URL: 填你在GitLab页面看到的。
  2. Token: 填页面上的Token。
  3. Description: 给Runner起个名,比如 dev-runner-01
  4. Tags: 关键点!docker,deploy(逗号分隔)。后续在代码里指定tag,只有匹配的Runner才会接单。
  5. Executor: 重中之重!docker
  6. Docker Image: 填个默认的,比如 alpine:latest 或者 node:16

搞定!回到GitLab页面,看到那个绿色的圆点亮起,你就拥有了一个专属的自动化构建工人。


三、 编写“施工图纸”:让自动化动起来

有了工人,还得有图纸。这就是项目根目录下的 .gitlab-ci.yml 文件。

记得带我的实习生小王第一次写这个文件时,他恨不得把所有脚本都堆在一个stage里。结果只要一步出错,查日志查到眼瞎。

经验告诉我,流水线必须分层。 一个标准的后端项目(比如Spring Boot或Go),我通常这么设计:

stages:
  - build
  - test
  - deploy

# 全局变量定义,方便维护
variables:
  MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"

# 缓存依赖,不用每次都重新下载jar包,速度提升50%
cache:
  paths:
    - .m2/repository/

# 第一步:打包
build_job:
  stage: build
  image: maven:3.8-jdk-11
  script:
    - echo "开始打包..."
    - mvn clean package -DskipTests
  artifacts: # 产物传递,把打好的jar包传给下一步
    paths:
      - target/*.jar
  tags:
    - docker # 指定用我们刚才注册的Runner

# 第二步:测试(哪怕只是跑个简单的单元测试,也是心理安慰)
test_job:
  stage: test
  image: maven:3.8-jdk-11
  script:
    - echo "运行测试..."
    - mvn test
  tags:
    - docker

# 第三步:部署(利用SSH免密登录部署到目标服)
deploy_job:
  stage: deploy
  image: ubuntu:latest
  before_script:
    # 这里通常需要配置SSH私钥,稍微复杂点,但只需配一次
    - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
    - mkdir -p ~/.ssh
    - chmod 700 ~/.ssh
    - echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
  script:
    - echo "开始部署..."
    - scp target/*.jar user@192.168.1.100:/app/
    - ssh user@192.168.1.100 "sh /app/restart.sh"
  tags:
    - docker
  only:
    - main # 只有合并到main分支才触发部署

这里有个反直觉的设计:部署操作为什么要单独用一个ubuntu镜像?

很多新手喜欢直接在Runner所在的宿主机上操作。但我建议,让Runner保持纯净,通过SSH远程去操作目标服务器(即使目标服务器就是Runner所在的那台机器)。这样能最大程度解耦,就算以后应用扩容到10台机器,改改脚本就行,不用动Runner架构。


结尾:解放双手,去喝杯咖啡吧

自从落地了这套GitLab Runner方案,那种“周五傍晚不敢提交代码”的恐惧感彻底消失了。现在,我的日常变成了:写代码 -> 提交 -> 泡咖啡 -> 收到钉钉/企业微信通知“部署成功”。

如果你还在犹豫是否要引入CI/CD,不妨先从一个小项目试起。

这里给大家留一个小调研: 在配置Runner执行器(Executor)时,你更倾向于简单粗暴的 Shell 模式,还是隔离性更好的 Docker 模式? 为什么?欢迎在评论区告诉我你的选择。

最后,总结一下今天能落地的3个行动步骤:

  1. 找台空闲服务器(2核4G足矣),安装Docker。
  2. docker run 启动并注册 一个私有GitLab Runner。
  3. 在项目根目录新建 .gitlab-ci.yml,先写一个最简单的 echo "Hello World" 跑通流程。

别光看,去试试。当你第一次看到那个绿色的 “Passed” 对勾亮起时,你会发现,技术带来的不仅是效率,更是尊严。