Linux运维10 min read

GitLab CI/CD 流水线实战:从代码提交到生产部署的全自动化

一、为什么需要 CI/CD

第一次带团队做发布时,我们的流程是:本地 mvn package → 把 jar 包 scp 到服务器 → 杀掉旧进程 → 启动新进程。一次发布要 20 分钟,且三个人发布的姿势各不相同。某次线上事故的根源,正是某同事本地 JDK 版本比生产环境高了一个小版本,导致一个语法特性在服务器上直接崩溃。

CI/CD 的核心价值不是"快",而是确定性:无论谁触发、在哪台机器触发,构建与部署的结果都完全一致。GitLab CI/CD 把这套确定性固化成了仓库里的一个 .gitlab-ci.yml 文件。

经验法则:凡是需要"人工记住步骤"的发布流程,迟早会出事故。把步骤写进流水线,事故率至少下降一个数量级。

二、核心概念:Pipeline / Stage / Job

GitLab CI 的三层模型必须先在脑子里建清楚:

Pipeline(一次提交触发的完整流程)
 ├── Stage: build   (阶段,按顺序执行)
     ├── Job: compile   (任务,同一 Stage 内并行)
     └── Job: lint
 ├── Stage: test
     ├── Job: unit-test
     └── Job: integration-test
 └── Stage: deploy
      ├── Job: deploy-staging
      └── Job: deploy-prod
  • Job 是最小执行单元,对应 Runner 上跑的一段脚本。
  • Stage 定义执行顺序,前一个 Stage 全部成功后才会进入下一个。
  • Pipeline 是一次 Push 或 Merge Request 触发的所有 Job 集合。

一个最朴素的 .gitlab-ci.yml

stages:
  - build
  - test
  - deploy

compile:
  stage: build
  script:
    - echo "编译中..."
    - make build
  artifacts:
    paths:
      - dist/

unit-test:
  stage: test
  script:
    - make test

deploy-staging:
  stage: deploy
  script:
    - ./deploy.sh staging
  only:
    - main

三、Runner 注册:流水线的发动机

GitLab 本身不执行任务,真正干活的是 Runner。它可以是共享 Runner(GitLab.com 提供),也可以是你自己部署的私有 Runner。

私有 Runner 注册流程(以 Shell Executor 为例):

# 安装 gitlab-runner
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh" | sudo bash
sudo yum install gitlab-runner -y

# 注册 Runner 到项目
sudo gitlab-runner register \
  --url "https://gitlab.com/" \
  --registration-token "YOUR_PROJECT_TOKEN" \
  --description "prod-runner-01" \
  --executor "shell" \
  --tag-list "prod,docker" \
  --run-untagged="false"

注册后几个关键配置(在 /etc/gitlab-runner/config.toml):

[[runners]]
  name = "prod-runner-01"
  url = "https://gitlab.com/"
  token = "xxxx"
  executor = "shell"
  # 限制并发,避免把机器打满
  limit = 4
  [runners.cache]
    # 用 S3 或本地目录做跨 Job 缓存
    Type = "local"
    Path = "/cache"

避坑 1:Shell Executor 默认以 gitlab-runner 用户执行,很多命令需要 sudo。不要图省事把 Runner 配成 root 执行器——这是严重的安全风险。正确做法是用 sudoers 给该用户放开特定命令:

# /etc/sudoers.d/gitlab-runner
gitlab-runner ALL=(root) NOPASSWD: /usr/bin/systemctl restart myapp, /usr/bin/docker

四、缓存与产物:速度的分水岭

CI 慢,90% 是因为依赖每次重新下载。GitLab 提供两种"中间产物"机制,很多人分不清:

机制 作用域 典型用途
cache 跨 Pipeline 复用(不保证存在) node_modules、maven .m2
artifacts 跨 Job 传递(保证存在,有保留期) 编译产物、测试报告
variables:
  MAVEN_OPTS: "-Dmaven.repo.local=.m2"

cache:
  key: ${CI_COMMIT_REF_SLUG}
  paths:
    - .m2/
    - node_modules/

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

避坑 2cache 在多 Runner 环境下默认不共享(除非用分布式缓存后端)。如果你有两台 Runner 且任务可能漂移到不同机器,本地缓存会"有时命中有时不命中"。生产环境建议配置 S3 兼容的分布式缓存:

[runners.cache.s3]
  ServerAddress = "oss.example.com"
  BucketName = "gitlab-cache"
  BucketLocation = "cn-hangzhou"

五、环境变量与 Secrets:别把密码写进仓库

.gitlab-ci.yml 会被提交到仓库,所以任何密钥都不能硬编码。GitLab 的项目设置 → CI/CD → Variables 里配置加密变量,流水线中直接以环境变量读取:

deploy-prod:
  stage: deploy
  script:
    - ssh -o StrictHostKeyChecking=no deploy@$PROD_HOST "cd /app && ./update.sh"
  only:
    - main
  environment:
    name: production

对于更敏感的场景(如云厂商 AK/SK),建议接 Vault 或 GitLab 的 Masked Variable(变量值在生产日志里会被自动打码成 [masked])。

避坑 3:Masked Variable 要求变量值必须是单行、且匹配特定正则(不能包含空格、斜杠等)。如果你的密钥含特殊字符导致无法 Mask,改用文件型变量(file: true),GitLab 会把内容写入临时文件并把路径存入变量:

# 变量类型选 File,名为 KUBE_CONFIG
deploy-k8s:
  script:
    - export KUBECONFIG="$KUBE_CONFIG"
    - kubectl apply -f manifests/

六、Docker 构建:dind 模式

现代部署几乎都围绕镜像。在 Runner 里构建镜像最干净的方式是 Docker-in-Docker(dind)

build-image:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  variables:
    DOCKER_DRIVER: overlay2
    DOCKER_TLS_CERTDIR: "/certs"
  script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

避坑 4:dind 默认需要 --privileged 模式,生产环境有安全隐患。替代方案是用 Kaniko(无需特权,纯用户态构建)或 buildah

build-kaniko:
  image:
    name: gcr.io/kaniko-project/executor:debug
    entrypoint: [""]
  script:
    - /kaniko/executor --context $CI_PROJECT_DIR --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

七、多环境部署:用 environment 管理

GitLab 的 environment 关键字能把部署和"环境"关联,在 GitLab 界面上形成可视化部署历史,还支持一键回滚:

deploy-staging:
  stage: deploy
  script: ./deploy.sh staging
  environment:
    name: staging
    url: https://staging.example.com
  only: [develop]

deploy-prod:
  stage: deploy
  script: ./deploy.sh prod
  environment:
    name: production
    url: https://example.com
  when: manual          # 生产环境手动点击触发,防误发
  only: [main]

when: manual 是生产发布的护身符——它强制一次人工确认,避免 main 分支的任何提交自动流向线上。

八、生产级模板:多阶段 + 并行测试

下面是一个我实际在用的完整模板,整合了缓存、并行测试、镜像构建、多环境部署:

stages: [build, test, package, deploy]

variables:
  IMAGE_TAG: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"

cache:
  key: $CI_COMMIT_REF_SLUG
  paths: [node_modules/]

build:
  stage: build
  image: node:20
  script:
    - npm ci
    - npm run build
  artifacts:
    paths: [dist/]
    expire_in: 1 day

test-unit:
  stage: test
  image: node:20
  script: [npm run test:unit]
  coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'

test-e2e:
  stage: test
  image: cypress/base:20
  script: [npm run test:e2e]
  needs: [build]          # 依赖 build 的产物,但可与 test-unit 并行

package:
  stage: package
  image: docker:24
  services: [docker:24-dind]
  script:
    - docker build -t $IMAGE_TAG .
    - docker push $IMAGE_TAG
  needs: [test-unit, test-e2e]

deploy-staging:
  stage: deploy
  script: ./deploy.sh staging
  environment: { name: staging }
  needs: [package]

deploy-prod:
  stage: deploy
  script: ./deploy.sh prod
  environment: { name: production }
  when: manual
  needs: [package]
  only: [main]

needs 关键字打破了默认的 Stage 顺序依赖——test-e2e 只等 build 完成即可与 test-unit 并行,整体流水线耗时从 12 分钟压到 5 分钟。

九、避坑清单(血泪总结)

# 现象 解决
1 Runner 并发打满 任务长时间 pending config.tomllimit,或扩容 Runner
2 缓存不命中 每次都重装依赖 用分布式缓存或固定 cache:key
3 密钥泄露 日志打印密码 变量设为 Masked,用文件型变量
4 dind 特权风险 安全审计不过 换 Kaniko / buildah
5 生产误发 提交即上线 生产 Job 加 when: manual
6 大仓库克隆慢 流水线卡在 clone GIT_DEPTH: "1" 浅克隆
7 产物过大 存储爆满 artifacts:expire_in 设保留期
8 Windows 路径 脚本在 Win Runner 失败 extends 统一 tags 隔离平台

十、小结

GitLab CI/CD 真正难的不是语法,而是把团队里那些"只有老王知道"的发布步骤,翻译成所有人都能复现的声明式配置。当你第一次看到一次 Push 自动走完"构建→测试→出镜像→部署预发→人工确认→生产上线"全流程,且任何人都无需记一条命令时,你就理解了 DevOps 的精髓。

下一步建议:把 when: manual 的生产发布接上蓝绿部署金丝雀发布,让回滚从"手工 ssh 救火"变成"点一下按钮"。

分享:

相关文章

评论区