为什么我放弃了传统方案,改用 GitLab CI/CD

devops进阶8 分钟阅读2026/10/29

为什么我放弃了传统方案,改用 GitLab CI/CD

一切始于一次凌晨两点的部署事故

去年我们团队还在用“传统方案”发布代码:开发在本地打包,通过一个维护了三年的 Shell 脚本把构建产物 scp 到服务器上,然后重启服务。脚本是三年前离职的老王写的,没人敢动。某个周五晚上,一个同事忘改配置文件就发了包,生产环境挂了两个小时。我凌晨两点被电话叫醒,挂掉电话的那一刻,我下定决心:必须搞一套像样的 CI/CD。

对比了一圈 Jenkins、CircleCI 和 GitLab CI/CD 之后,我选了 GitLab。原因很简单:我们代码仓库本来就在 GitLab 私有部署上,CI/CD 是自带的,不用再维护一台 Jenkins 服务器,也不用担心源码传到第三方。

下面是我从零到上线的完整过程,包括我踩过的坑。

第一步:从最简单的流水线开始

GitLab CI/CD 的核心就是在仓库根目录放一个 .gitlab-ci.yml。我的第一版非常简陋:

stages:
  - build
  - deploy

build:
  stage: build
  script:
    - mvn clean package -DskipTests

deploy:
  stage: deploy
  script:
    - ./deploy.sh

第一次提交后我满怀期待地打开 Pipelines 页面,结果卡在 pending。折腾了半小时才明白:流水线需要一个 Runner 来执行。我们自建的 GitLab 不会自动带 Runner。

于是我在一台内网机器上装了个 Runner:

# 安装(Debian/Ubuntu)
curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash
sudo apt install gitlab-runner

# 注册到 GitLab 实例
sudo gitlab-runner register \
  --url https://gitlab.内网地址.com \
  --token glrt-xxxxxxxx \
  --executor shell

这里我第一次踩坑:我一开始选了 shell executor,构建环境全靠这台机器上碰巧装了什么。同事 A 装了 JDK 17,流水线就能跑;后来机器重装成 JDK 11,流水线莫名其妙就挂了。教训是:除非有特殊原因,老老实实用 Docker executor,让构建环境声明式地写在配置里。

第二步:用镜像锁定环境

改成 Docker executor 之后,配置变成:

build:
  stage: build
  image: maven:3.9-eclipse-temurin-17
  script:
    - mvn clean package -DskipTests
  artifacts:
    paths:
      - target/*.jar
    expire_in: 1 week

惊喜之一:artifacts 可以在 job 之间传文件,deploy 阶段直接就能拿到 jar 包,不用再传一次。expire_in: 1 week 控制存储占用,不然默认一直保存会吃掉不少磁盘。

第三步:把测试纳入流水线

原来传统方案最大的问题就是“测试靠自觉”。我把测试强制塞进了流水线:

test:
  stage: test
  image: maven:3.9-eclipse-temurin-17
  script:
    - mvn test
  coverage: '/Total.*?([0-9]{1,3})%/'

再配合合并请求设置里的 "Pipelines must succeed",测试不过就不许合并。第一次强制推行时同事有怨言——因为有些“祖传测试”本来就一直是挂的。花了一周修掉那些僵尸测试之后,大家对红色叉号变得非常敏感,这反而是件好事。

第四步:部署与敏感信息

部署需要服务器的 SSH 私钥。我一开始图省事把私钥直接写进了 yml 文件,好在提交前被同事拦住了。正确做法是用 GitLab 的 CI/CD Variables(Settings → CI/CD → Variables):

  • 添加 DEPLOY_SSH_KEY,类型选 File,勾选 Protected(只在受保护分支可用)
  • 流水线里通过文件路径读取

部署 job 用 SSH 执行:

deploy-staging:
  stage: deploy
  image: alpine:3.19
  before_script:
    - apk add openssh-client
    - chmod 600 "$DEPLOY_SSH_KEY"
  script:
    - scp -i "$DEPLOY_SSH_KEY" -o StrictHostKeyChecking=no target/app.jar user@staging:/opt/app/
    - ssh -i "$DEPLOY_SSH_KEY" user@staging "systemctl restart app"
  environment:
    name: staging
    url: https://staging.example.com
  rules:
    - if: '$CI_COMMIT_BRANCH == "develop"'

environment 关键词值得一提:它会在 GitLab 里生成环境视图,谁在什么时候部署了什么版本一目了然,回滚也能一键操作。这是我以前 Shell 脚本方案完全做不到的。

rules 控制了只在 develop 分支部署到 staging。生产环境我用的是手动触发:

deploy-production:
  stage: deploy
  script:
    - ./scripts/deploy_prod.sh
  environment: production
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: manual

第五步:提速——缓存是关键

流水线跑顺了之后大家开始嫌慢,每次下载 Maven 依赖要四分钟。加上缓存:

build:
  cache:
    key: maven-repo
    paths:
      - .m2/repository
  variables:
    MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"

构建时间从 6 分钟降到了 2 分钟。另外我还用 rules:changes 给无关的 job 做了条件触发——前端目录没改就不跑前端构建,流水线平均又省了 40 秒。

几个月的实战心得

真实收益:

  • 部署从“人肉+老王脚本”变成点一下按钮,出错率大幅下降
  • 每次变更都有流水线日志,出了问题能翻历史,审计有据可查
  • 新人入职当天就能发起部署,不需要任何口口相传的隐式知识

踩过的坑:

  1. Runner 磁盘要定期清理,Docker 镜像层和 artifacts 会悄悄把磁盘吃满。我后来加了个每周清理的 cron。
  2. 自建 Runner 只有一台时,多个流水线会排队。现在我们有两台,用标签分开跑构建和部署。
  3. include 拆分配置文件很好用,但我们拆得太碎,十几个 yml 互相引用,新人根本看不懂。三个以内的文件就够了。
  4. 敏感信息务必用 Protected/Masked variables,并且定期轮换。我们发生过一次离职同事仍能读到变量的事故,之后把权限收紧了。

诚实说局限性: .gitlab-ci.yml 的调试体验一般,语法错误要提交了才能发现(本地可以用 gitlab-ci-local 或 gitlab-runner exec 缓解,但不是完美方案)。复杂矩阵构建不如一些专业工具灵活。免费版的功能限制(比如部分安全扫描)对大团队来说可能是个坎。

但总的来说,把部署从“玄学”变成“工程”,是我去年做的最值的一件事。如果你还在用几台服务器加一堆 Shell 脚本发版,哪怕先从最简单的 build + test 流水线开始,也值得迈出这一步。

相关 Agent

D

Docker

Docker 是一个容器化平台,允许开发者将应用及其依赖项打包成轻量级、可移植的容器。它简化了跨环境的开发、测试和部署过程。该描述强调它最适合容器化和开发环境,提供免费方案和团队方案,团队方案从每用户每月5美元起。

了解更多 →