GitLab CI/CD 最佳实践:从入门到生产级流水线

GitLab CI/CD 是 DevOps 工具链的核心组件。很多团队的流水线停在"能跑就行":测试全跑在一台机器上、依赖每次都重新下载、生产部署靠人肉点按钮。这篇文章从流水线设计讲到安全扫描,给出可以照着搭的生产级方案。

在动手写 .gitlab-ci.yml 之前,值得先想清楚一个目标:流水线不是"把命令串起来",而是把"验证—构建—交付"的过程固化成规则,让每一次提交都走同样的路径,让人为失误没有机会发生。下面的配置示例按一个典型 Node.js 项目展开,你可以替换成自己的技术栈。

流水线结构设计

stages:
  - lint
  - test
  - build
  - deploy

variables:
  DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA

lint:
  stage: lint
  image: node:20
  script:
    - npm ci
    - npm run lint

test:
  stage: test
  image: node:20
  script:
    - npm ci
    - npm run test:ci
  coverage: '/Lines\s*:\s*(\d+\.\d+%)/'

stage 的顺序就是执行顺序,前一个阶段全部通过才会进入下一个。把 lint、test、build、deploy 拆开有几个好处:一是失败早暴露,代码风格问题不用等测试跑完才发现;二是可以按阶段并行,test 里的多个 job 能同时跑;三是 deploy 单独成段,方便加人工确认。coverage 正则用来从测试输出里抓取覆盖率,会自动显示在合并请求的详情里。

如果项目里模块多、构建依赖复杂,还可以用 needs 关键字打破严格的阶段顺序,让某个 job 只等它真正依赖的 job 完成就开跑,进一步缩短流水线耗时。比如 UI 测试只需要等 build 完,不必等后端集成测试跑完。

构建产物传递(artifact)

test 和 build 之间要传文件,靠的是 artifact 而不是 cache:

build:
  stage: build
  script:
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 week

deploy:
  stage: deploy
  script:
    - ls dist/ && ./deploy.sh
  dependencies:
    - build

dist/ 在 build 阶段打包好,通过 artifact 传给 deploy,expire_in: 1 week 防止仓库无限膨胀。artifact 是"必须存在"的交付物,cache 是"尽力而为"的加速手段——两者用途完全不同,别混用。很多人踩过的坑是:把需要部署的文件放进 cache 而不是 artifact,结果下一台 Runner 上 cache 命中失败,deploy job 里 ls dist/ 直接报错。

Runner 与变量配置

流水线跑在哪、密钥放哪,是新手最容易踩坑的地方。共享 Runner 免费但会排队,自建 Runner 要保证环境一致,建议用 Docker Executor 让每个 job 都在干净容器里跑。密钥和敏感变量用 CI/CD 变量管理,不要在 .gitlab-ci.yml 里写明文,相关做法可参考环境变量与密钥管理。

这里给一个对比:共享 Runner 适合中小团队,零成本但排队时流水线慢;自建 Runner 适合对资源隔离和缓存要求高的场景,可以把 Docker、构建缓存都放在本机,第一次构建后速度明显更快。无论哪种,都要注意把 SSH 私钥、云厂商密钥这类敏感信息放到 GitLab 的 CI/CD 变量里,并开启 "Masked" 选项,避免在日志里泄露。

缓存策略

cache:
  key: $CI_COMMIT_REF_SLUG
  paths:
    - node_modules/
    - .npm/
  policy: pull-push

cache 和 artifact 是两个容易混淆的概念:cache 缓存依赖,让 npm ci 更快;artifact 传递构建产物,比如把编译好的包传给部署 job。key 按分支区分,避免不同分支互相污染。注意 cache 是尽力而为,不要在里面存必须存在的东西。policy: pull-push 表示流水线既拉取旧缓存也写入新缓存;如果某个 job 只读依赖不写,可以用 policy: pull 省掉上传步骤,减少 Runner 的 IO 压力。

多环境部署

deploy-review:
  stage: deploy
  only:
    - merge_requests
  environment:
    name: review/$CI_MERGE_REQUEST_IID
    url: https://$CI_MERGE_REQUEST_IID.example.com
  script:
    - ./deploy-review.sh

deploy-production:
  stage: deploy
  only:
    - main
  when: manual
  environment:
    name: production
    url: https://example.com
  script:
    - ./deploy.sh

每个合并请求拉起一套 review 环境,方便设计师和产品直接在真实页面上看效果;生产部署加上 when: manual,由人确认后再点按钮,避免合并即上线。环境名写清楚,GitLab 的环境页会自动聚合每套环境的部署记录和 URL。一个实用的补充是给 review 环境设置自动清理:合并请求关闭后,对应的环境实例如果没人管,会一直占着资源,可以用 GitLab 的清理策略或 cron job 定期回收。

安全扫描

include:
  - template: Jobs/SAST.gitlab-ci.yml
  - template: Jobs/Dependency-Scanning.gitlab-ci.yml
  - template: Jobs/Secret-Detection.gitlab-ci.yml

GitLab 内置的安全模板可以一键接入:SAST 做静态代码扫描、Dependency-Scanning 检查依赖漏洞、Secret-Detection 扫描是否误把密钥提交进了仓库。三者都不用自己写规则,直接用官方模板即可。建议至少在 main 分支上强制开启这些扫描,并把扫描结果接入合并请求的审查流程——一个在合并前就被拦下的密钥提交,比上线后泄露再补救便宜得多。

常见问题

  • npm ci 每次都重新下载依赖? 检查 cache 的 key 和 paths 是否写对,node_modules 路径要包含在 paths 里。
  • 部署到服务器用什么方式? 常见做法是用 SSH 加部署密钥,或直接推送到容器镜像仓库后由服务器拉取,相关可参考网站 CI/CD 流水线搭建和Docker Compose 生产部署。
  • 流水线一直排队怎么办? 大概率是共享 Runner 资源紧张,考虑自建 Runner 或给关键 job 提高优先级。
  • 合并请求里看不到测试覆盖率? 确认 coverage 正则和测试输出格式匹配,CI 里 test:ci 的输出需要包含类似 Lines: 85.5% 的行。

参考

参考:GitLab CI/CD 官方文档 https://docs.gitlab.com/ci/
参考:GitLab CI/CD 变量 https://docs.gitlab.com/ci/variables/
参考:GitLab Cache 与 Artifact https://docs.gitlab.com/ci/caching/