Loading...

文章背景图

(六) 实战场景 + 排错 + 最佳实践

2026-08-10
0
-
- 分钟

一、实战场景模板

1.1 Node.js 前端项目

stages:
  - install
  - lint
  - test
  - build
  - deploy

default:
  image: node:20-alpine

cache:
  key:
    files:
      - package-lock.json
  paths:
    - node_modules/
    - .npm/

install:
  stage: install
  cache:
    policy: pull-push
  script:
    - npm ci
  artifacts:
    paths:
      - node_modules/
    expire_in: 1 hour

lint:
  stage: lint
  cache:
    policy: pull
  script:
    - npm run lint
  allow_failure: true

unit-test:
  stage: test
  cache:
    policy: pull
  script:
    - npm test -- --coverage
  artifacts:
    reports:
      junit: junit.xml
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml

build:
  stage: build
  cache:
    policy: pull
  script:
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 day

deploy-vercel:
  stage: deploy
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  script:
    - npx vercel deploy --prod --token=$VERCEL_TOKEN
  environment:
    name: production
    url: https://your-app.vercel.app

1.2 Java Spring Boot 项目 + Docker 镜像

stages:
  - test
  - build
  - docker
  - deploy

variables:
  MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
  DOCKER_REGISTRY: registry.example.com
  IMAGE_NAME: $DOCKER_REGISTRY/$CI_PROJECT_NAME

cache:
  key:
    files:
      - pom.xml
  paths:
    - .m2/repository/

default:
  image: maven:3.9-eclipse-temurin-21

unit-test:
  stage: test
  script:
    - mvn test
  artifacts:
    reports:
      junit:
        - target/surefire-reports/TEST-*.xml

build:
  stage: build
  script:
    - mvn package -DskipTests
  artifacts:
    paths:
      - target/*.jar
    expire_in: 1 hour

docker-build:
  stage: docker
  image: docker:latest
  services:
    - docker:dind
  variables:
    DOCKER_TLS_CERTDIR: ""
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $DOCKER_REGISTRY
  script:
    - docker build -t $IMAGE_NAME:$CI_COMMIT_SHORT_SHA .
    - docker tag $IMAGE_NAME:$CI_COMMIT_SHORT_SHA $IMAGE_NAME:latest
    - docker push $IMAGE_NAME:$CI_COMMIT_SHORT_SHA
    - docker push $IMAGE_NAME:latest

deploy-k8s:
  stage: deploy
  image: bitnami/kubectl:latest
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  script:
    - kubectl set image deployment/$CI_PROJECT_NAME \
        app=$IMAGE_NAME:$CI_COMMIT_SHORT_SHA \
        --namespace=production
  environment:
    name: production

1.3 Python 项目

stages:
  - test
  - build
  - deploy

default:
  image: python:3.12-slim

cache:
  key:
    files:
      - requirements.txt
      - pyproject.toml
  paths:
    - .venv/
    - ~/.cache/pip/

before_script:
  - pip install --upgrade pip
  - pip install -r requirements.txt

lint:
  stage: test
  script:
    - pip install ruff
    - ruff check .
    - ruff format --check .
  allow_failure: true

test:
  stage: test
  script:
    - pip install pytest pytest-cov
    - pytest --cov=. --cov-report=xml --junitxml=junit.xml
  artifacts:
    reports:
      junit: junit.xml
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml

docker-build:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  script:
    - docker build -t myapp:$CI_COMMIT_SHORT_SHA .
    - docker push myapp:$CI_COMMIT_SHORT_SHA
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_COMMIT_TAG

1.4 多环境部署(dev / staging / production)

stages:
  - build
  - deploy-dev
  - deploy-staging
  - deploy-prod

.deploy-template: &deploy-template
  image: alpine:latest
  before_script:
    - apk add --no-cache openssh-client
  script:
    - |
      ssh $DEPLOY_HOST "
        cd /app &&
        docker pull $IMAGE:$CI_COMMIT_SHORT_SHA &&
        docker stop app-${ENV} || true &&
        docker run -d --name app-${ENV} --restart always \
          -p ${PORT}:8080 $IMAGE:$CI_COMMIT_SHORT_SHA
      "

deploy-dev:
  <<: *deploy-template
  stage: deploy-dev
  variables:
    ENV: dev
    PORT: "8081"
    DEPLOY_HOST: $DEV_HOST
  environment:
    name: development
  rules:
    - if: $CI_COMMIT_BRANCH == "develop"

deploy-staging:
  <<: *deploy-template
  stage: deploy-staging
  variables:
    ENV: staging
    PORT: "8080"
    DEPLOY_HOST: $STAGING_HOST
  environment:
    name: staging
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

deploy-prod:
  <<: *deploy-template
  stage: deploy-prod
  variables:
    ENV: prod
    PORT: "8080"
    DEPLOY_HOST: $PROD_HOST
  environment:
    name: production
  when: manual
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/

1.5 Monorepo 差异化构建

stages:
  - detect
  - build
  - deploy

# 第一步:检测变更,设定变量
detect-changes:
  stage: detect
  image: alpine:latest
  script:
    - |
      if git diff --name-only HEAD~1 | grep -q "^services/api/"; then
        echo "BUILD_API=true" >> build.env
      fi
      if git diff --name-only HEAD~1 | grep -q "^services/web/"; then
        echo "BUILD_WEB=true" >> build.env
      fi
      if git diff --name-only HEAD~1 | grep -q "^services/worker/"; then
        echo "BUILD_WORKER=true" >> build.env
      fi
  artifacts:
    reports:
      dotenv: build.env           # ← 将变量传递给后续 Job

build-api:
  stage: build
  needs: [detect-changes]
  rules:
    - if: $BUILD_API == "true"
  script:
    - cd services/api && dotnet publish

build-web:
  stage: build
  needs: [detect-changes]
  rules:
    - if: $BUILD_WEB == "true"
  script:
    - cd services/web && npm run build

build-worker:
  stage: build
  needs: [detect-changes]
  rules:
    - if: $BUILD_WORKER == "true"
  script:
    - cd services/worker && go build

1.6 代码质量 + 安全扫描一体

include:
  - template: Security/SAST.gitlab-ci.yml
  - template: Security/Secret-Detection.gitlab-ci.yml
  - template: Security/Dependency-Scanning.gitlab-ci.yml
  - template: Code-Quality.gitlab-ci.yml

stages:
  - test
  - build

# 自定义代码质量门槛
code-quality-check:
  stage: test
  image:
    name: registry.gitlab.com/gitlab-org/ci-cd/codequality:latest
    entrypoint: [""]
  script:
    - /run.sh
  artifacts:
    reports:
      codequality: gl-code-quality-report.json
  allow_failure: true

二、常见错误排查指南

2.1 "yaml invalid" —— YAML 语法错误

症状:Pipeline 页面显示 "YAML invalid",配置未生效。

常见原因

  • 缩进混用了 Tab 和空格

  • 冒号后没有空格(image:node:20 应为 image: node:20)

  • 字符串中包含冒号但未加引号

解决步骤

  1. CI Lint 验证:项目 → Build → Pipeline Editor → Validate

  2. 如果用 VS Code,安装 "GitLab CI/CD" 插件获得实时语法高亮

  3. 检查是否有不可见字符:cat -A .gitlab-ci.yml

2.2 "This job is stuck" —— Job 卡住不动

症状:Job 一直处于 pending 状态,显示为灰色。

排查清单

检查项

命令/位置

Runner 是否在线

GitLab UI → Settings → CI/CD → Runners(绿色圆点 = 在线)

tags 是否匹配

Job 的 tags 和 Runner 的 tag-list 必须全部匹配

Runner 是否被暂停

sudo gitlab-runner list 查看状态

并发是否已满

检查 config.toml 中的 concurrent 和 limit

2.3 "no runner with tags matching xxx"

症状:Job 提示 "This job is stuck because you don't have any active runners with these tags assigned to them."

解决

  • 检查 Runner 标签和 Job 标签是否一致(注意大小写)

  • 确认 Runner 没有被标记为 paused

  • 如果是 Shared Runner,确认项目没有禁用 Shared Runner:Settings → CI/CD → Runners → "Enable shared runners"

2.4 "permission denied" —— 权限不足

常见场景与解决

# 场景 1:Docker 命令权限不足
script:
  - docker build -t myapp .         # ❌ permission denied

解决:Runner 注册时挂载 Docker Socket,或在 config.toml 中设置 privileged = true

# 场景 2:SSH 部署密钥权限
script:
  - ssh user@server "deploy.sh"     # ❌ permission denied

解决:在 GitLab Variables 中创建 File 类型的 SSH 私钥变量

deploy:
  before_script:
    - chmod 600 $SSH_PRIVATE_KEY
    - mkdir -p ~/.ssh
    - echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
  script:
    - ssh -i $SSH_PRIVATE_KEY user@server "deploy.sh"

2.5 缓存不生效

调试方法:在 Job 脚本中打印目录内容确认:

script:
  - ls -la node_modules/        # 存在吗?
  - du -sh node_modules/        # 大小合理吗?

常见原因

  1. cache:key 不匹配 → 不同分支的 $CI_COMMIT_REF_SLUG 不同,缓存在不同分支间不共享

  2. paths 使用了绝对路径 → 必须用相对路径

  3. 缓存文件太大超过限制(默认 100MB)→ 排除不必要文件

  4. Shell Executor 默认不支持缓存 → 需要配置缓存目录

2.6 artifacts 找不到

常见原因

  1. 生成文件的命令执行失败,但被 allow_failure: true 掩盖了

  2. paths 中的文件确实不存在(构建失败)

  3. 依赖的 Job 没有运行(被 rules 跳过了)

  4. dependencies 限制了产物传递

调试:在获取 artifacts 的 Job 中加 ls 确认:

deploy:
  dependencies:
    - build
  script:
    - ls -la dist/              # 确认文件存在
    - ls -la target/            # 确认文件存在

三、最佳实践清单

3.1 安全实践

实践

说明

🔒 敏感变量用 Masked

Token、密码类变量必须勾选 "Masked"

🔒 生产变量用 Protected

生产环境凭证仅保护分支/标签可用

🔒 使用 File 类型变量存证书

SSH 私钥、K8s kubeconfig 用 File 类型

🔒 不要 echo 敏感变量

echo $PASSWORD 会暴露在日志中

🔒 定期轮换 Token

CI/CD 变量中的 API Token 定期更换

🔒 审计 Runner 权限

Runner 不要以 root 运行(除非必要)

3.2 性能实践

实践

说明

精确的 cache:key

使用 files: [package-lock.json] 而非固定字符串

cache:policy: pull

只有安装依赖的 Job 用 pull-push,其余用 pull

合理使用 needs

打破不必要的阶段串行,缩短整体时间

轻量镜像

用 alpine 版本镜像(如 node:20-alpine)

依赖锁文件

用 npm ci 而非 npm install,pip 加 --no-deps

并行测试

用 parallel 拆分测试套件

镜像拉取策略

设置 pull_policy: if-not-present 减少网络开销

3.3 可维护性实践

实践

说明

📦 拆分大型 YAML

超过 200 行就考虑用 include:local 拆分

📦 继承消除重复

公共配置提为隐藏 Job(.xxx:)用 extends

📦 使用 YAML 锚点

小范围重复可以用 &anchor + <<: *anchor

📦 锁定模板版本

include:project 务必指定 ref(Tag 或 SHA)

📦 CI Lint 预检

每次修改 YAML 前先 Lint

📦 Pipeline Editor

善用 GitLab 内置的 Pipeline Editor 编写配置

3.4 架构设计实践

实践

说明

🏗️ stages 排序

把失败成本最低、速度最快的放最前面

🏗️ 快速失败

lint 和 typecheck 放在 test 之前

🏗️ 幂等的脚本

每个 Job 可以安全地重复执行

🏗️ 明确的超时

为耗时 Job 显式设置 timeout

🏗️ 环境隔离

不同环境使用不同的 environment:name

🏗️ Review Apps

利用动态 environment 为每个 MR 创建预览环境

3.5 推荐目录结构

项目根目录/
├── .gitlab-ci.yml              # 入口文件(stages 定义 + includes)
├── ci/
│   ├── common.yml              # 全局变量、默认值、隐藏 Job 基类
│   ├── build.yml               # 构建相关 Job
│   ├── test.yml                # 测试相关 Job
│   ├── deploy.yml              # 部署相关 Job
│   └── security.yml            # 安全扫描 Job

四、关键词速查表

关键词

作用

所属篇

stages

定义阶段顺序

3

stage

Job 归属的阶段

3

script

Job 执行的命令

3

image

执行环境的 Docker 镜像

3

tags

Runner 选择标签

2, 3

variables

定义/管理变量

3

cache

跨 Pipeline 缓存依赖

3

artifacts

传递和保存构建产物

3

default

全局默认值

3

before_script

Job 前置脚本

3

after_script

Job 后置脚本

3

rules

Job 条件触发

4

workflow

Pipeline 全局开关

4

when

Job 执行时机

4

needs

DAG 依赖编排

4

parallel

并行执行 / 矩阵构建

4

retry

失败自动重试

4

timeout

超时控制

4

allow_failure

允许失败不阻塞

4

interruptible

允许自动取消

4

include

引入外部配置

5

extends

继承复用配置

5

trigger

触发下游 Pipeline

5

environment

环境管理

5

services

附加服务容器

5

release

自动创建 Release

5

dependencies

控制 artifacts 传递

3


五、系列总结:从入门到精通的成长路径

回顾本系列的六篇文章:

┌────────────────────────────────────────────┐
│  📖 第一篇:概念入门                        │
│  Pipeline / Stage / Job / Runner 是什么?   │
│  写出第一份 .gitlab-ci.yml                  │
├────────────────────────────────────────────┤
│  🔧 第二篇:Runner 部署                     │
│  Shell / Docker / K8s 三种执行器            │
├────────────────────────────────────────────┤
│  🧱 第三篇:核心关键字                      │
│  stages / jobs / variables / cache / artifacts│
├────────────────────────────────────────────┤
│  🧠 第四篇:流程控制                        │
│  rules / workflow / when / needs / parallel  │
├────────────────────────────────────────────┤
│  🏗️ 第五篇:进阶与复用                      │
│  include / extends / trigger / environment   │
├────────────────────────────────────────────┤
│  🎯 第六篇:实战 + 排错 + 最佳实践          │
│  拿来就用的模板 + 常见问题排查               │
└────────────────────────────────────────────┘

如果你跟着这个路径走下来,现在应该能:

  • 独立为任何项目编写 CI/CD 配置

  • 理解并使用 30+ 个 YAML 关键字

  • 搭建模块化、可复用的配置体系

  • 快速排查 Pipeline 故障

  • 应用经过验证的最佳实践

GitLab CI/CD 的世界远不止于此——还有 Auto DevOps、CI/CD Catalog、GitLab Duo AI 辅助等更高级的功能等你探索。但掌握好本系列的基础,你已经站在了一个坚实的起点上。Happy CI/CD! 🚀


参考资料:

原创

(六) 实战场景 + 排错 + 最佳实践

本文链接: (六) 实战场景 + 排错 + 最佳实践

本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

文章目录