部署与运维

CI/CD 流水线:设计与 GitLab、GitHub Actions 实战

从持续集成概念与流水线设计原则出发,给出 GitLab CI 与 GitHub Actions 的完整配置示例,以及多环境部署、蓝绿、金丝雀与回滚策略。

CI/CD 核心概念

CI、持续交付与持续部署是三个递进的阶段,分别对应提交合并、随时可发布与自动发布。

持续集成(Continuous Integration)

开发人员频繁(每天至少一次)将代码合并到主干,每次合并都触发自动化构建和测试。

核心价值: 尽早发现问题。代码冲突、编译错误、单元测试失败尽早暴露,并把失败反馈给提交者。

典型流程:

plaintext1 行
代码提交 → 触发 CI → 编译 → 单元测试 → 静态分析 → 构建产物 → 通知

持续交付(Continuous Delivery)

在 CI 基础上,确保代码随时可以部署到生产环境。部署到生产需要手动审批。

核心价值: 部署能力回归业务决策。技术上随时可发,业务上选择合适时机。

持续部署(Continuous Deployment)

在持续交付基础上更进一步:代码通过所有测试后自动部署到生产,零人工干预。

核心价值: 最快的交付速度,适合成熟团队和高自动化水平的系统。

三者关系

plaintext5 行
持续集成 ⊂ 持续交付 ⊂ 持续部署

CI:   代码 → 构建 → 测试 → 制品
CD*:  制品 → 预发布环境 → [手动审批] → 生产环境(持续交付)
CD**: 制品 → 预发布环境 → 自动 → 生产环境(持续部署)

选型建议:

  • 初创团队 / 新项目:先做好 CI
  • 中型团队:持续交付(推荐大多数场景)
  • 成熟微服务 + 完善监控:持续部署

流水线设计原则

快速反馈

流水线每一秒都在消耗团队的时间。反馈越快,修复成本越低。

实践要点:

  • 单元测试 < 5 分钟,集成测试 < 15 分钟
  • 失败时立即通知,不要等整条流水线跑完
  • 分层测试:快速检查先行,慢速检查后置

GitLab CI 示例 — 阶段化流水线:

plaintext26 行
stages:
  - lint        # 30 秒
  - unit-test   # 2 分钟
  - build       # 3 分钟
  - integration # 10 分钟
  - deploy      # 2 分钟

lint:
  stage: lint
  script:
    - npm run lint
    - npm run type-check
  rules:
    - changes:
        - "src/**/*"

unit-test:
  stage: unit-test
  script:
    - npm run test:unit -- --coverage
  coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml

并行化

独立任务并行执行,缩短总耗时。

plaintext20 行
# GitLab CI 并行示例
unit-test:
  stage: test
  parallel: 4  # 分成 4 个并行 Job
  script:
    - jest --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL

# GitHub Actions 矩阵构建
jobs:
  test:
    strategy:
      matrix:
        node-version: [22, 24]
        os: [ubuntu-latest, macos-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm test

幂等性

流水线多次执行结果一致,不会产生副作用。

plaintext10 行
# 幂等部署脚本示例
deploy:
  stage: deploy
  script:
    # 检查当前版本,避免重复部署
    - CURRENT=$(kubectl get deployment app -o jsonpath='{.metadata.annotations.version}')
    - if [ "$CURRENT" = "$CI_COMMIT_SHA" ]; then echo "Already deployed"; exit 0; fi
    # 使用声明式部署
    - kubectl apply -f k8s/deployment.yaml
    - kubectl rollout status deployment/app --timeout=300s

可回滚

任何变更都能快速回退到上一个已知正常版本。

plaintext17 行
# 保留最近 10 个版本的制品
deploy:
  artifacts:
    paths:
      - dist/
    expire_in: 30 days

# 回滚 Job
rollback:
  stage: deploy
  when: manual
  script:
    - kubectl rollout undo deployment/app
    - kubectl rollout status deployment/app --timeout=120s
  environment:
    name: production
    action: stop

多环境部署

环境分层

典型的三环境模型:

plaintext5 行
dev (开发) → staging (预发布) → production (生产)
  ↓              ↓                  ↓
 自动部署      自动部署          手动审批后部署
 功能验证      集成验证          线上验证
 随时重置      接近生产          高可用保障

环境差异管理

原则:代码不变,配置外置。

GitLab CI 多环境模板:

plaintext56 行
.deploy_template: &deploy_template
  stage: deploy
  image: bitnami/kubectl:latest
  script:
    - kubectl config use-context $KUBE_CONTEXT
    - envsubst < k8s/deployment.template.yaml | kubectl apply -f -
    - kubectl rollout status deployment/$APP_NAME -n $NAMESPACE --timeout=300s
  variables:
    APP_NAME: my-service

deploy:dev:
  <<: *deploy_template
  variables:
    KUBE_CONTEXT: dev-cluster
    NAMESPACE: dev
    REPLICAS: "1"
    CPU_LIMIT: "500m"
    MEMORY_LIMIT: "512Mi"
    LOG_LEVEL: "debug"
  environment:
    name: development
    url: https://dev.example.com
  rules:
    - if: $CI_COMMIT_BRANCH == "develop"

deploy:staging:
  <<: *deploy_template
  variables:
    KUBE_CONTEXT: staging-cluster
    NAMESPACE: staging
    REPLICAS: "2"
    CPU_LIMIT: "1000m"
    MEMORY_LIMIT: "1Gi"
    LOG_LEVEL: "info"
  environment:
    name: staging
    url: https://staging.example.com
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

deploy:prod:
  <<: *deploy_template
  variables:
    KUBE_CONTEXT: prod-cluster
    NAMESPACE: production
    REPLICAS: "5"
    CPU_LIMIT: "2000m"
    MEMORY_LIMIT: "2Gi"
    LOG_LEVEL: "warn"
  environment:
    name: production
    url: https://www.example.com
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
  when: manual
  allow_failure: false

配置注入

方式一:Kubernetes ConfigMap + Secret

yaml23 行
# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: ${NAMESPACE}
data:
  DATABASE_HOST: "${DB_HOST}"
  REDIS_URL: "redis://${REDIS_HOST}:6379"
  LOG_LEVEL: "${LOG_LEVEL}"
  FEATURE_FLAG_NEW_UI: "true"

---
# k8s/secret.yaml (由 CI 变量注入)
apiVersion: v1
kind: Secret
metadata:
  name: app-secret
  namespace: ${NAMESPACE}
type: Opaque
data:
  DATABASE_PASSWORD: "${DB_PASSWORD_B64}"
  API_KEY: "${API_KEY_B64}"

方式二:Vault 动态密钥

plaintext9 行
# 使用 Vault Agent Sidecar 注入
annotations:
  vault.hashicorp.com/agent-inject: "true"
  vault.hashicorp.com/role: "my-service"
  vault.hashicorp.com/agent-inject-secret-db: "database/creds/my-service"
  vault.hashicorp.com/agent-inject-template-db: |
    {{- with secret "database/creds/my-service" -}}
    DATABASE_URL=postgresql://{{ .Data.username }}:{{ .Data.password }}@db:5432/mydb
    {{- end }}

GitLab CI/CD 实战

本文是服务器运维笔记系列的第 13 篇,聚焦 GitLab CI/CD 的实际应用。从基础概念到高级配置,从单机部署到 Kubernetes 集成,提供一套可以直接复用的实战指南。


GitLab CI 基础

架构概览

GitLab CI/CD 的核心架构由三个角色组成:

  • GitLab Server:托管代码仓库,解析 .gitlab-ci.yml,调度流水线任务
  • GitLab Runner:执行流水线中具体 Job 的代理程序,可部署在任意机器上
  • Executor:Runner 内部的执行环境(Docker、Shell、Kubernetes 等)

工作流程:开发者推送代码 → GitLab 解析 CI 配置 → 按 Stages 调度 Jobs → Runner 拉取代码并执行 → 结果回写 GitLab UI。

Runner 类型

类型 说明 适用场景
Shared Runner 全局共享,所有项目可用 小团队、公共资源池
Group Runner 组级别共享,组内项目可用 部门/团队级别隔离
Specific Runner 绑定到单个项目 敏感项目、特殊环境需求

.gitlab-ci.yml 核心语法

plaintext16 行
# 定义流水线阶段(按顺序执行)
stages:
  - build
  - test
  - deploy

# 定义一个 Job
build-app:
  stage: build
  image: node:24-alpine
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/

核心概念:

  • stages:定义阶段顺序,同阶段 Jobs 并行,不同阶段串行
  • jobs:定义在顶层的 key(非保留字),每个 Job 必须属于一个 stage
  • script:Job 中实际执行的命令列表(必填)
  • image:指定 Docker 镜像(Docker Executor 下生效)
  • tags:匹配特定 Runner 的标签

Runner 配置

安装与注册

安装 GitLab Runner(以 Ubuntu/Debian 为例):

plaintext8 行
# 添加官方仓库
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash

# 安装
sudo apt-get install gitlab-runner

# 验证版本
gitlab-runner --version

注册 Runner:

plaintext10 行
sudo gitlab-runner register \
  --non-interactive \
  --url "https://gitlab.example.com/" \
  --registration-token "PROJECT_TOKEN_HERE" \
  --executor "docker" \
  --docker-image "alpine:latest" \
  --description "my-docker-runner" \
  --tag-list "docker,linux" \
  --run-untagged="true" \
  --locked="false"

注册完成后,配置文件位于 /etc/gitlab-runner/config.toml。

Docker Executor 配置

Docker Executor 是最常用的执行器,每次 Job 在独立容器中运行,天然隔离。

plaintext20 行
# /etc/gitlab-runner/config.toml
concurrent = 4          # 最大并发 Job 数
check_interval = 3      # 轮询间隔(秒)

[[runners]]
  name = "docker-runner"
  url = "https://gitlab.example.com/"
  token = "RUNNER_TOKEN"
  executor = "docker"

  [runners.docker]
    image = "alpine:latest"           # 默认镜像
    privileged = false                # 是否特权模式(Docker-in-Docker 需要)
    disable_entrypoint_overwrite = false
    oom_kill_disable = false
    disable_cache = false
    volumes = ["/cache"]              # 挂载缓存卷
    shm_size = 0                      # 共享内存大小
    pull_policy = "if-not-present"    # 拉取策略:always/if-not-present/never
    allowed_images = ["ruby:*", "python:*", "node:*"]  # 镜像白名单

Docker-in-Docker(DinD)方案:需要在 CI 配置中构建 Docker 镜像时使用。

plaintext10 行
build-docker-image:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  script:
    - docker build -t my-app:$CI_COMMIT_SHA .
    - docker push registry.example.com/my-app:$CI_COMMIT_SHA

Shell Executor 配置

Shell Executor 直接在宿主机上执行命令,性能最好但隔离性最差。

plaintext8 行
[[runners]]
  name = "shell-runner"
  url = "https://gitlab.example.com/"
  token = "RUNNER_TOKEN"
  executor = "shell"

  [runners.custom_build_dir]
    enabled = true    # 允许自定义构建目录

Shell Executor 下所有 Job 共享同一环境,注意依赖冲突。建议仅用于特定场景(如需要访问宿主机硬件、GPU 等)。

自动伸缩配置(Docker Machine / Autoscaler)

大规模场景下可配置 Runner 自动伸缩,按需创建和销毁 Runner 实例。

plaintext31 行
[[runners]]
  name = "autoscale-runner"
  url = "https://gitlab.example.com/"
  token = "RUNNER_TOKEN"
  executor = "docker+machine"
  limit = 10                    # 最大实例数

  [runners.machine]
    IdleCount = 2               # 空闲实例数
    IdleTime = 600              # 空闲超时(秒)
    MaxBuilds = 100             # 单实例最大构建次数
    MachineName = "runner-%s"
    MachineDriver = "amazonec2"
    MachineOptions = [
      "amazonec2-instance-type=t3.medium",
      "amazonec2-region=cn-north-1",
      "amazonec2-vpc-id=vpc-xxx",
      "amazonec2-subnet-id=subnet-xxx",
      "amazonec2-security-group=sg-xxx",
    ]

    [[runners.machine.autoscaling]]
      Periods = ["* * 8-18 * * mon-fri *"]   # 工作时间
      IdleCount = 5
      IdleTime = 300
      Timezone = "Asia/Shanghai"

    [[runners.machine.autoscaling]]
      Periods = ["* * * * * sat,sun *"]       # 周末
      IdleCount = 0
      IdleTime = 60

流水线设计

Stages 与 Jobs

plaintext18 行
stages:
  - lint
  - build
  - test
  - security
  - deploy

# 同一 stage 的 jobs 会并行执行
lint-js:
  stage: lint
  script:
    - npx eslint . --max-warnings=0

lint-python:
  stage: lint
  script:
    - flake8 src/
    - mypy src/

Dependencies 与制品传递

使用 dependencies 控制 Job 之间的制品传递,避免不必要的下载。

plaintext24 行
build-frontend:
  stage: build
  script:
    - npm ci && npm run build
  artifacts:
    paths:
      - dist/frontend/

build-backend:
  stage: build
  script:
    - go build -o bin/server ./cmd/server
  artifacts:
    paths:
      - bin/

deploy-staging:
  stage: deploy
  dependencies:
    - build-frontend     # 只下载 frontend 的制品
    - build-backend      # 只下载 backend 的制品
  script:
    - scp dist/frontend/* staging:/var/www/html/
    - scp bin/server staging:/opt/app/

Artifacts 配置

plaintext15 行
test-unit:
  stage: test
  script:
    - go test ./... -coverprofile=coverage.out -v 2>&1 | tee test-output.txt
  artifacts:
    when: always              # always/on_success/on_failure
    paths:
      - coverage.out
      - test-output.txt
    reports:
      junit: test-output.txt  # 解析测试报告在 MR 中展示
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml
    expire_in: 7 days         # 制品过期时间

Cache 配置

plaintext16 行
variables:
  npm_config_cache: "$CI_PROJECT_DIR/.npm"

cache:
  key:
    files:
      - package-lock.json     # 基于文件内容生成缓存 key
  paths:
    - .npm/
    - node_modules/

build:
  stage: build
  script:
    - npm ci --cache .npm
    - npm run build

变量管理

预定义变量

GitLab 提供了大量内置变量,常用的包括:

plaintext15 行
# 在 script 中直接使用
deploy-production:
  stage: deploy
  script:
    - echo "项目: $CI_PROJECT_NAME"
    - echo "分支: $CI_COMMIT_REF_NAME"
    - echo "提交: $CI_COMMIT_SHA"
    - echo "短 SHA: $CI_COMMIT_SHORT_SHA"
    - echo "标签: $CI_COMMIT_TAG"
    - echo "MR 源分支: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME"
    - echo "Pipeline ID: $CI_PIPELINE_ID"
    - echo "Job ID: $CI_JOB_ID"
    - echo "Runner 描述: $CI_RUNNER_DESCRIPTION"
    - echo "Registry: $CI_REGISTRY"
    - echo "Registry 镜像: $CI_REGISTRY_IMAGE"

完整的预定义变量列表可参考:GitLab 官方文档 → CI/CD → Predefined variables。

自定义变量

变量可以在三个层级定义,优先级:Job 级 > Pipeline 级 > 项目/组 Settings。

plaintext12 行
# Pipeline 级变量
variables:
  APP_NAME: "my-awesome-app"
  DEPLOY_ENV: "staging"

build:
  stage: build
  variables:
    BUILD_TYPE: "release"       # Job 级变量
  script:
    - echo "Building $APP_NAME ($BUILD_TYPE)"
    - make BUILD_TYPE=$BUILD_TYPE

Protected 与 Masked 变量

在 Settings → CI/CD → Variables 中配置:

  • Protected:仅在受保护分支/标签的流水线中可见
  • Masked:在 Job 日志中自动隐藏(显示 [MASKED]),值必须是 Base64 编码或满足正则规则
plaintext9 行
# 使用 Protected + Masked 变量(在 Settings 中配置,不在 YAML 中暴露)
deploy-production:
  stage: deploy
  script:
    - echo "$PROD_DB_PASSWORD" | docker secret create db_password -  # 日志中显示 [MASKED]
  only:
    - main                          # 仅 main 分支触发
  environment:
    name: production

Secrets 管理(Vault 集成)

对于更高安全要求,可集成 HashiCorp Vault:

plaintext15 行
# 需要在项目 Settings 中配置 Vault 集成
deploy-with-vault:
  stage: deploy
  id_tokens:
    VAULT_ID_TOKEN:
      aud: https://vault.example.com
  secrets:
    DB_PASSWORD:
      vault:
        engine: { name: kv-v2, path: secret }
        path: production/db
        field: password
  script:
    - echo "DB password loaded from Vault"   # 不要打印实际值
    - deploy --db-password "$DB_PASSWORD"

常用场景

构建→测试→部署(完整流水线)

plaintext59 行
stages:
  - build
  - test
  - deploy

variables:
  DOCKER_REGISTRY: "registry.example.com"

# ============ Build ============
build:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  script:
    - docker build -t $DOCKER_REGISTRY/$CI_PROJECT_NAME:$CI_COMMIT_SHA .
    - docker push $DOCKER_REGISTRY/$CI_PROJECT_NAME:$CI_COMMIT_SHA

# ============ Test ============
test-unit:
  stage: test
  image: $DOCKER_REGISTRY/$CI_PROJECT_NAME:$CI_COMMIT_SHA
  script:
    - npm test -- --coverage
  coverage: '/All files\s*\|\s*([\d.]+)/'
  artifacts:
    reports:
      junit: junit.xml
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml

test-integration:
  stage: test
  image: $DOCKER_REGISTRY/$CI_PROJECT_NAME:$CI_COMMIT_SHA
  services:
    - postgres:15
    - redis:7
  variables:
    POSTGRES_DB: test_db
    POSTGRES_USER: test
    POSTGRES_PASSWORD: test
    DATABASE_URL: "postgresql://test:test@postgres:5432/test_db"
    REDIS_URL: "redis://redis:6379"
  script:
    - npm run test:integration

# ============ Deploy ============
deploy-staging:
  stage: deploy
  environment:
    name: staging
    url: https://staging.example.com
  script:
    - kubectl set image deployment/$CI_PROJECT_NAME
        app=$DOCKER_REGISTRY/$CI_PROJECT_NAME:$CI_COMMIT_SHA
  only:
    - develop
  when: manual                  # 手动触发

多环境部署

plaintext33 行
.deploy_template: &deploy_template
  stage: deploy
  image: bitnami/kubectl:latest
  script:
    - kubectl config use-context $KUBE_CONTEXT
    - envsubst < k8s/deployment.yml | kubectl apply -f -
    - kubectl rollout status deployment/$CI_PROJECT_NAME -n $K8S_NAMESPACE

deploy-staging:
  <<: *deploy_template
  variables:
    KUBE_CONTEXT: staging
    K8S_NAMESPACE: staging
    REPLICAS: "2"
  environment:
    name: staging
    url: https://staging.example.com
  only:
    - develop

deploy-production:
  <<: *deploy_template
  variables:
    KUBE_CONTEXT: production
    K8S_NAMESPACE: production
    REPLICAS: "5"
  environment:
    name: production
    url: https://www.example.com
  only:
    - main
  when: manual
  allow_failure: false          # 不允许跳过

并行任务

plaintext16 行
test-suite:
  stage: test
  parallel: 4                   # 启动 4 个并行 Job
  script:
    - |
      case $CI_NODE_INDEX in
        1) TEST_PATTERN="tests/unit/models/**" ;;
        2) TEST_PATTERN="tests/unit/services/**" ;;
        3) TEST_PATTERN="tests/unit/controllers/**" ;;
        4) TEST_PATTERN="tests/unit/middleware/**" ;;
      esac
    - pytest $TEST_PATTERN --junitxml=results-$CI_NODE_INDEX.xml
  artifacts:
    reports:
      junit:
        - results-*.xml

矩阵构建

plaintext13 行
build-matrix:
  stage: build
  parallel:
    matrix:
      - GO_VERSION: ["1.21", "1.22"]
        OS: ["linux", "darwin"]
        ARCH: ["amd64", "arm64"]
  image: golang:$GO_VERSION
  script:
    - GOOS=$OS GOARCH=$ARCH go build -o bin/app-$OS-$ARCH ./cmd/app
  artifacts:
    paths:
      - bin/

制品管理

Artifacts 详细配置

plaintext17 行
build:
  stage: build
  script:
    - make build
    - make docs
  artifacts:
    name: "$CI_PROJECT_NAME-$CI_COMMIT_REF_SLUG-$CI_PIPELINE_ID"
    paths:
      - bin/
      - docs/
      - config/production.yml
    exclude:
      - "**/*.test"             # 排除测试文件
      - "**/*.log"
    when: on_success            # on_success(默认)/ on_failure / always
    expire_in: 30 days
    access: all                 # all / developer / none

过期策略

plaintext11 行
# 快速过期的中间产物
test-results:
  artifacts:
    expire_in: 3 days

# 永久保留的发布制品
release-package:
  artifacts:
    expire_in: never            # 永不过期
  only:
    - tags                      # 仅标签触发

依赖传递控制

plaintext27 行
build-frontend:
  stage: build
  script: npm run build
  artifacts:
    paths: [dist/]

build-backend:
  stage: build
  script: go build ./...
  artifacts:
    paths: [bin/]

# 默认行为:每个 Job 会下载前面所有阶段的制品
# 显式控制:
deploy:
  stage: deploy
  dependencies:                 # 只下载指定 Job 的制品
    - build-frontend
  script:
    - ls dist/                  # [可用] 有 frontend
    - ls bin/ 2>/dev/null       # [不适用] 没有 backend

# 不下载任何制品
lint:
  stage: test
  dependencies: []              # 空数组 = 不下载
  script: eslint .

部署策略

SSH 部署

plaintext14 行
deploy-ssh:
  stage: deploy
  before_script:
    - apt-get update && apt-get install -y openssh-client
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
    - mkdir -p ~/.ssh && chmod 700 ~/.ssh
    - ssh-keyscan -H $DEPLOY_HOST >> ~/.ssh/known_hosts
  script:
    - scp -r dist/* deploy@$DEPLOY_HOST:/opt/app/
    - ssh deploy@$DEPLOY_HOST "cd /opt/app && docker compose pull && docker compose up -d"
  environment:
    name: production
    url: https://www.example.com

Docker 部署(带 Registry)

plaintext35 行
build-and-push:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  before_script:
    - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
  script:
    - |
      docker build \
        --cache-from $CI_REGISTRY_IMAGE:latest \
        --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA \
        --tag $CI_REGISTRY_IMAGE:latest \
        .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    - docker push $CI_REGISTRY_IMAGE:latest

deploy-docker:
  stage: deploy
  before_script:
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | ssh-add -
  script:
    - |
      ssh deploy@$DEPLOY_HOST << 'EOF'
        docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
        docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
        docker stop app || true
        docker rm app || true
        docker run -d --name app \
          --restart unless-stopped \
          -p 8080:8080 \
          -e DATABASE_URL="$DATABASE_URL" \
          $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
      EOF

Kubernetes 部署

plaintext23 行
deploy-k8s:
  stage: deploy
  image:
    name: bitnami/kubectl:latest
    entrypoint: [""]
  before_script:
    - echo "$KUBE_CONFIG" | base64 -d > /tmp/kubeconfig
    - export KUBECONFIG=/tmp/kubeconfig
  script:
    # 替换镜像标签
    - kubectl set image deployment/$CI_PROJECT_NAME
        app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
        -n $K8S_NAMESPACE
    - kubectl rollout status deployment/$CI_PROJECT_NAME
        -n $K8S_NAMESPACE
        --timeout=300s
  after_script:
    - rm -f /tmp/kubeconfig
  environment:
    name: production
    url: https://www.example.com
    kubernetes:
      namespace: production

使用 Helm 部署:

plaintext20 行
deploy-helm:
  stage: deploy
  image:
    name: alpine/helm:3.14
    entrypoint: [""]
  before_script:
    - echo "$KUBE_CONFIG" | base64 -d > /tmp/kubeconfig
    - export KUBECONFIG=/tmp/kubeconfig
    - helm repo add myrepo https://charts.example.com
    - helm repo update
  script:
    - |
      helm upgrade --install $CI_PROJECT_NAME myrepo/app-chart \
        --namespace $K8S_NAMESPACE \
        --create-namespace \
        --set image.repository=$CI_REGISTRY_IMAGE \
        --set image.tag=$CI_COMMIT_SHA \
        --set ingress.host=$DEPLOY_HOST \
        --values values-production.yaml \
        --wait --timeout 5m

Auto DevOps

GitLab 内置的 Auto DevOps 可以零配置完成从构建到部署的主要流程:

plaintext12 行
# 项目根目录创建 .gitlab-ci.yml 仅需一行
include:
  - template: Auto-DevOps.gitlab-ci.yml

# 或者自定义覆盖
variables:
  AUTO_DEVOPS_DEPLOY_TARGET: kubernetes
  KUBE_NAMESPACE: my-namespace
  HELM_UPGRADE_EXTRA_ARGS: "--set replicas=3"

include:
  - template: Auto-DevOps.gitlab-ci.yml

缓存优化

全局缓存

plaintext19 行
# 全局缓存配置,所有 Job 继承
cache:
  key: "${CI_COMMIT_REF_SLUG}"    # 按分支隔离缓存
  paths:
    - node_modules/
    - .npm/
    - .cache/
  policy: pull-push                # pull / push / pull-push

# 仅 pull(不更新缓存,用于只读 Job)
test:
  stage: test
  cache:
    key: "${CI_COMMIT_REF_SLUG}"
    paths:
      - node_modules/
    policy: pull                   # 只读缓存
  script:
    - npm test

Per-Job 缓存

plaintext20 行
build-frontend:
  stage: build
  cache:
    - key: frontend-deps
      paths:
        - frontend/node_modules/
    - key: frontend-build-cache
      paths:
        - frontend/.cache/
  script:
    - cd frontend && npm ci && npm run build

build-backend:
  stage: build
  cache:
    - key: backend-deps
      paths:
        - backend/vendor/
  script:
    - cd backend && go mod download && go build ./...

缓存键策略

plaintext29 行
# 策略 1:基于 Lock 文件生成缓存 key(推荐)
cache:
  key:
    files:
      - package-lock.json          # 文件内容变化 → 新缓存
  paths:
    - node_modules/

# 策略 2:分支 + Lock 文件组合
cache:
  key:
    files:
      - package-lock.json
    prefix: $CI_COMMIT_REF_SLUG
  paths:
    - node_modules/

# 策略 3:固定 key + 手动版本控制
cache:
  key: "deps-v2"                   # 需要清除缓存时改版本号
  paths:
    - node_modules/

# 清除缓存的技巧:
# 方式 1:在 GitLab UI → CI/CD → Pipelines → Clear runner caches
# 方式 2:修改 cache key 中的版本号
# 方式 3:使用 API
#   curl --request DELETE --header "PRIVATE-TOKEN: $TOKEN" \
#     "https://gitlab.example.com/api/v4/projects/$PROJECT_ID/clean"

安全扫描

SAST(静态应用安全测试)

plaintext7 行
include:
  - template: Security/SAST.gitlab-ci.yml

sast:
  variables:
    SAST_EXCLUDED_ANALYZERS: "spotbugs"     # 排除特定分析器
    SAST_EXCLUDED_PATHS: "test,spec,docs"   # 排除目录

DAST(动态应用安全测试)

plaintext10 行
include:
  - template: Security/DAST.gitlab-ci.yml

dast:
  variables:
    DAST_WEBSITE: "https://staging.example.com"
    DAST_FULL_SCAN_ENABLED: "true"
    DAST_AUTH_URL: "https://staging.example.com/login"
    DAST_USERNAME: "scanner@example.com"
    DAST_PASSWORD: "$DAST_PASSWORD"         # 在 CI Variables 中配置

依赖扫描

plaintext6 行
include:
  - template: Security/Dependency-Scanning.gitlab-ci.yml

dependency_scanning:
  variables:
    DS_EXCLUDED_PATHS: "test,spec,docs"

容器扫描

plaintext7 行
include:
  - template: Security/Container-Scanning.gitlab-ci.yml

container_scanning:
  variables:
    CS_IMAGE: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
    CS_SEVERITY_THRESHOLD: "high"           # 只报告 high 及以上

完整安全扫描流水线示例:

plaintext21 行
include:
  - template: Security/SAST.gitlab-ci.yml
  - template: Security/Dependency-Scanning.gitlab-ci.yml
  - template: Security/Container-Scanning.gitlab-ci.yml
  - template: Security/Secret-Detection.gitlab-ci.yml

stages:
  - build
  - test
  - security-sast
  - security-container
  - deploy

build:
  stage: build
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

# 所有安全扫描 Job 自动从 include 模板继承配置
# 扫描结果会自动合并到 MR 的安全面板中

最佳实践

YAML 锚点复用

plaintext30 行
# 定义可复用的 Job 片段
.docker_login: &docker_login
  before_script:
    - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin

.base_deploy: &base_deploy
  stage: deploy
  image: bitnami/kubectl:latest
  <<: *docker_login
  when: manual
  allow_failure: false

# 使用锚点扩展
deploy-staging:
  <<: *base_deploy
  variables:
    K8S_NAMESPACE: staging
  environment:
    name: staging
  only:
    - develop

deploy-production:
  <<: *base_deploy
  variables:
    K8S_NAMESPACE: production
  environment:
    name: production
  only:
    - main

使用 extends(更推荐)

plaintext23 行
.docker_login:
  before_script:
    - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin

.base_deploy:
  stage: deploy
  image: bitnami/kubectl:latest
  extends: .docker_login
  when: manual

deploy-staging:
  extends: .base_deploy
  variables:
    K8S_NAMESPACE: staging
  environment:
    name: staging

deploy-production:
  extends: .base_deploy
  variables:
    K8S_NAMESPACE: production
  environment:
    name: production

extends 比 YAML 锚点更强大:支持多级继承、深度合并对象和数组。推荐优先使用 extends。

Monorepo 策略

plaintext38 行
# 使用 rules:changes 按文件变更路径触发
build-frontend:
  stage: build
  rules:
    - changes:
        - frontend/**/*
        - shared/**/*
      when: on_success
  script:
    - cd frontend && npm ci && npm run build

build-backend:
  stage: build
  rules:
    - changes:
        - backend/**/*
        - shared/**/*
      when: on_success
  script:
    - cd backend && go build ./...

deploy-frontend:
  stage: deploy
  rules:
    - changes:
        - frontend/**/*
      when: manual
  script:
    - echo "Deploy frontend"

deploy-backend:
  stage: deploy
  rules:
    - changes:
        - backend/**/*
      when: manual
  script:
    - echo "Deploy backend"

调试技巧

plaintext16 行
debug-job:
  stage: test
  image: ubuntu:22.04
  variables:
    CI_DEBUG_TRACE: "1"             # 开启 Shell 调试(set -x)
  script:
    - echo "Debug info:"
    - echo "Shell: $0"
    - echo "User: $(whoami)"
    - echo "PWD: $(pwd)"
    - echo "ENV:"
    - env | sort
    - echo "Network:"
    - ip addr show || ifconfig
    - echo "Disk:"
    - df -h

其他调试方法:

plaintext16 行
# 1. 本地运行 Job(需要安装 gitlab-runner)
gitlab-runner exec docker build-job

# 2. 使用 CI_DEBUG_SERVICES 查看 service 日志
#    在 Variables 中设置 CI_DEBUG_SERVICES=true

# 3. 在 Job 失败时保留容器(Docker Executor)
#    config.toml 中设置 [runners.docker] cleanup = false

# 4. 使用 after_script 查看失败后的环境状态
job:
  script:
    - make test
  after_script:
    - ls -la /builds/
    - cat /tmp/*.log 2>/dev/null || true

常见坑与解决方案

坑 1:Job 日志中变量值泄露

plaintext10 行
#  错误:直接打印密码
deploy:
  script:
    - echo "Connecting with password: $DB_PASSWORD"    # 日志中可见!
    - mysql -u root -p$DB_PASSWORD

#  正确:使用 Masked 变量 + 避免打印
deploy:
  script:
    - mysql -u root -p"$DB_PASSWORD" -e "SELECT 1"    # 变量在 Settings 中设为 Masked

坑 2:Docker Executor 中文件权限问题

plaintext10 行
# [不适用] 以 root 运行的容器中创建的文件,宿主机 runner 用户无法访问
build:
  script:
    - touch output.txt

# [可用] 使用 --user 指定用户,或在 script 中修改权限
build:
  script:
    - touch output.txt
    - chmod 644 output.txt

坑 3:缓存未生效

plaintext9 行
# [不适用] 不同分支使用相同缓存 key 但 Lock 文件不同
cache:
  key: "deps"

# [可用] 基于 Lock 文件生成 key
cache:
  key:
    files:
      - package-lock.json

坑 4:rules 与 only/except 混用

plaintext12 行
# [不适用] 不要混用 rules 和 only/except(已废弃)
job:
  only:
    - main
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

# [可用] 统一使用 rules
job:
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
    - if: $CI_COMMIT_TAG

坑 5:Artifact 超大导致上传失败

plaintext10 行
# [不适用] 打包整个 node_modules(可能上 GB)
artifacts:
  paths:
    - node_modules/

# [可用] 只打包构建产物,依赖用 cache 管理
artifacts:
  paths:
    - dist/
  expire_in: 7 days

附录:完整项目配置示例

以下是一个完整的 .gitlab-ci.yml 模板,涵盖了本文讨论的大部分功能:

plaintext133 行
# ============================================
# .gitlab-ci.yml - 完整项目模板
# ============================================

stages:
  - prepare
  - build
  - test
  - security
  - deploy

variables:
  DOCKER_REGISTRY: "$CI_REGISTRY"
  DOCKER_IMAGE: "$CI_REGISTRY_IMAGE"
  K8S_NAMESPACE: "default"

# ---- 全局缓存 ----
default:
  cache:
    key:
      files:
        - package-lock.json
    paths:
      - node_modules/
      - .npm/
  image: node:24-alpine

# ---- 复用模板 ----
.docker_auth: &docker_auth
  before_script:
    - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY

.kubectl_base: &kubectl_base
  image:
    name: bitnami/kubectl:latest
    entrypoint: [""]
  before_script:
    - echo "$KUBE_CONFIG" | base64 -d > /tmp/kubeconfig
    - export KUBECONFIG=/tmp/kubeconfig

# ============ Prepare Stage ============
install-deps:
  stage: prepare
  script:
    - npm ci --cache .npm
  cache:
    key:
      files:
        - package-lock.json
    paths:
      - node_modules/
      - .npm/
    policy: pull-push

# ============ Build Stage ============
build-app:
  stage: build
  script:
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 7 days

build-docker:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  <<: *docker_auth
  script:
    - docker build --cache-from $DOCKER_IMAGE:latest -t $DOCKER_IMAGE:$CI_COMMIT_SHA -t $DOCKER_IMAGE:latest .
    - docker push $DOCKER_IMAGE:$CI_COMMIT_SHA
    - docker push $DOCKER_IMAGE:latest

# ============ Test Stage ============
test-unit:
  stage: test
  script:
    - npm run test:unit -- --coverage
  coverage: '/Lines\s*:\s*([\d.]+)/'
  artifacts:
    reports:
      junit: junit.xml
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml

test-integration:
  stage: test
  services:
    - postgres:15
    - redis:7
  variables:
    POSTGRES_DB: test
    POSTGRES_USER: test
    POSTGRES_PASSWORD: test
    DATABASE_URL: "postgresql://test:test@postgres:5432/test"
  script:
    - npm run test:integration

# ============ Security Stage ============
include:
  - template: Security/SAST.gitlab-ci.yml
  - template: Security/Dependency-Scanning.gitlab-ci.yml
  - template: Security/Secret-Detection.gitlab-ci.yml

# ============ Deploy Stage ============
deploy-staging:
  <<: *kubectl_base
  stage: deploy
  script:
    - kubectl set image deployment/app app=$DOCKER_IMAGE:$CI_COMMIT_SHA -n staging
    - kubectl rollout status deployment/app -n staging --timeout=300s
  environment:
    name: staging
    url: https://staging.example.com
  rules:
    - if: $CI_COMMIT_BRANCH == "develop"

deploy-production:
  <<: *kubectl_base
  stage: deploy
  script:
    - kubectl set image deployment/app app=$DOCKER_IMAGE:$CI_COMMIT_SHA -n production
    - kubectl rollout status deployment/app -n production --timeout=300s
  environment:
    name: production
    url: https://www.example.com
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: manual
  allow_failure: false

流水线凭据与制品边界

不可信分支或合并请求中的代码可能在构建步骤执行。Runner、受保护变量和发布环境应按信任范围分开,不能给所有流水线同一组生产权限。

测试、扫描和发布应指向同一制品摘要。构建完成后重新从分支构建发布包,会让已测试内容与实际发布内容发生偏差。回滚记录制品版本与数据库迁移边界,验证失败时阻止后续发布任务。

GitHub Actions 实战

前言

GitHub Actions 是 GitHub 原生的 CI/CD 平台,自 2019 年正式发布以来,已成为 DevOps 领域最受欢迎的自动化工具之一。它直接集成在代码仓库中,无需额外搭建 Jenkins 或 GitLab CI,就能实现从代码提交到生产部署的主要流程自动化。

本篇笔记系统梳理 GitHub Actions 的核心概念、语法细节、常用场景和最佳实践,涵盖从入门到进阶的完整知识体系。所有配置示例均来自实际生产环境,可直接复用。


GitHub Actions 基础

核心概念

理解 GitHub Actions,先搞清楚这几个核心概念:

  • Workflow(工作流):一个自动化流程的定义文件,存放在 .github/workflows/ 目录下,使用 YAML 格式。
  • Event(事件):触发 Workflow 的动作,比如 push、pull_request、schedule、workflow_dispatch 等。
  • Job(作业):Workflow 中的一个独立执行单元,由多个 Step 组成。多个 Job 默认并行执行,也可以用 needs 依赖关系控制串行。
  • Step(步骤):Job 中的单个任务,可以是运行一条 Shell 命令(run),也可以是调用一个 Action(uses)。
  • Action(动作):可复用的步骤单元,可以来自 GitHub Marketplace,也可以自己编写。
  • Runner(运行器):执行 Job 的机器。GitHub 提供托管 Runner(ubuntu-latest、windows-latest 等),也支持自托管 Runner。
  • Artifact(制品):Job 运行过程中产生的文件,可以在 Job 之间传递,也可以下载保存。

它们的关系是:一个 Workflow 文件定义了完整的流程 → 响应某个 Event 触发 → 包含一个或多个 Job → 每个 Job 由多个 Step 组成 → Step 在 Runner 上执行。

Workflow 文件结构

一个标准的 Workflow 文件长这样:

plaintext32 行
# .github/workflows/ci.yml
name: CI Pipeline          # Workflow 名称,显示在 GitHub Actions 页面

on:                         # 触发条件
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:                        # 全局环境变量
  NODE_VERSION: '24'
  REGISTRY: ghcr.io

jobs:                       # 作业定义
  build:
    name: Build & Test
    runs-on: ubuntu-latest  # 运行环境
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

触发机制详解

on 字段定义了 Workflow 何时被触发,支持多种事件类型:

plaintext40 行
on:
  # 1. Push 事件 —— 推送代码到指定分支时触发
  push:
    branches: [main, 'release/**']
    tags: ['v*']
    paths: ['src/**', 'package.json']        # 只有这些路径的文件变更才触发
    paths-ignore: ['docs/**', '*.md']         # 排除这些路径

  # 2. Pull Request 事件
  pull_request:
    types: [opened, synchronize, reopened]    # PR 状态变更时触发
    branches: [main]

  # 3. 定时任务(Cron 表达式,UTC 时间)
  schedule:
    - cron: '0 2 * * 1'  # 每周一 UTC 02:00(北京时间 10:00)

  # 4. 手动触发
  workflow_dispatch:
    inputs:
      environment:
        description: '部署环境'
        required: true
        default: 'staging'
        type: choice
        options: [dev, staging, prod]
      dry_run:
        description: '模拟运行'
        required: false
        type: boolean
        default: false

  # 5. 其他 Workflow 完成后触发
  workflow_run:
    workflows: ["Build"]
    types: [completed]

  # 6. 当收到 issue comment 时触发(常用于 ChatOps)
  issue_comment:
    types: [created]

实用技巧:paths 过滤在 monorepo 中特别有用,可以避免不相关的代码变更触发不必要的 CI。


Workflow 语法详解

Jobs 与依赖关系

plaintext18 行
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Linting..."

  test:
    runs-on: ubuntu-latest
    needs: lint              # lint 完成后才执行
    steps:
      - run: echo "Testing..."

  deploy:
    runs-on: ubuntu-latest
    needs: [lint, test]      # 两个都完成后才执行
    if: github.ref == 'refs/heads/main'  # 仅 main 分支部署
    steps:
      - run: echo "Deploying..."

条件执行:if 表达式支持 GitHub Actions 的上下文变量和函数:

plaintext16 行
steps:
  - name: Only on main
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    run: echo "Deploy to production"

  - name: Skip for bot PRs
    if: "!contains(github.event.pull_request.title, '[bot]')"
    run: echo "Run tests"

  - name: Continue on failure
    if: failure()           # 前一步失败时执行
    run: echo "Handling failure..."

  - name: Always run
    if: always()            # 无论前面成功失败都执行
    run: echo "Cleanup"

Steps 详解

每个 Step 可以有以下属性:

plaintext14 行
steps:
  - name: Step name              # 显示名称
    id: my_step                  # 唯一 ID,用于后续引用输出
    if: success()                # 条件执行
    working-directory: ./src     # 工作目录
    env:                         # Step 级别的环境变量
      MY_VAR: 'hello'
    run: |                       # Shell 命令
      echo "Value: $MY_VAR"
      echo "output=value" >> $GITHUB_OUTPUT  # 设置输出
    continue-on-error: true      # 允许失败而不中断

  - name: Use previous output
    run: echo "Got ${{ steps.my_step.outputs.output }}"

Matrix 策略

Matrix 让你用一份配置跑多组参数,非常适合多版本、多平台测试:

plaintext25 行
jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node-version: [22, 24]
        exclude:                    # 排除特定组合
          - os: windows-latest
            node-version: 24
        include:                    # 追加额外组合
          - os: ubuntu-latest
            node-version: 22
            experimental: true
      fail-fast: false              # 一个失败不取消其他
      max-parallel: 3               # 最多并行 3 个
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci && npm test
      - name: Notify experimental failure
        if: failure() && matrix.experimental
        run: echo "Experimental build failed, not blocking"

环境变量与 Secrets

plaintext18 行
env:
  GLOBAL_VAR: 'available to all jobs'

jobs:
  build:
    runs-on: ubuntu-latest
    env:
      JOB_VAR: 'available to all steps in this job'
    steps:
      - name: Use env
        env:
          STEP_VAR: 'available to this step only'
        run: |
          echo "Global: $GLOBAL_VAR"
          echo "Job: $JOB_VAR"
          echo "Step: $STEP_VAR"
          echo "Secret: ${{ secrets.MY_SECRET }}"
          echo "Env from context: ${{ vars.MY_CONFIG }}"

注意:Secrets 在日志中会被自动遮蔽为 ***,但要小心不要通过 echo 或其他方式间接泄露。使用 env 传入环境变量比直接在 run 中引用更安全。


常用 Action

actions/checkout

几乎所有 Workflow 都需要的第一步:

plaintext8 行
- uses: actions/checkout@v4
  with:
    repository: owner/repo       # 检出其他仓库
    ref: develop                 # 指定分支/标签
    token: ${{ secrets.GH_PAT }} # 使用 PAT 访问私有仓库
    submodules: true             # 递归检出子模块
    fetch-depth: 0               # 完整历史(用于 changelog 生成)
    path: my-repo                # 检出到指定目录

actions/setup-node

plaintext6 行
- uses: actions/setup-node@v4
  with:
    node-version: '24'           # 支持 semver,如 '20.x'、'^20'
    cache: 'npm'                 # 自动缓存 npm/pnpm/yarn 依赖
    registry-url: 'https://registry.npmjs.org'
    scope: '@my-org'             # npm scope

actions/setup-java

plaintext8 行
- uses: actions/setup-java@v4
  with:
    distribution: 'temurin'      # temurin/zulu/adopt/microsoft/liberica/corretto
    java-version: '21'
    cache: 'gradle'              # 缓存 Gradle 或 Maven 依赖
    # cache: 'maven'
    server-id: github            # Maven settings.xml 配置
    settings-path: ${{ github.workspace }}

actions/setup-python

plaintext6 行
- uses: actions/setup-python@v5
  with:
    python-version: '3.12'
    cache: 'pip'
    # cache: 'pipenv'
    # cache: 'poetry'

docker/build-push-action

构建并推送 Docker 镜像:

plaintext21 行
- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v3

- name: Login to GHCR
  uses: docker/login-action@v3
  with:
    registry: ghcr.io
    username: ${{ github.actor }}
    password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push
  uses: docker/build-push-action@v6
  with:
    context: .
    push: true
    tags: |
      ghcr.io/${{ github.repository }}:latest
      ghcr.io/${{ github.repository }}:${{ github.sha }}
    cache-from: type=gha
    cache-to: type=gha,mode=max
    platforms: linux/amd64,linux/arm64  # 多架构构建

其他常用 Action

plaintext29 行
# 缓存任意目录
- uses: actions/cache@v4
  with:
    path: |
      ~/.cache/go-build
      ~/go/pkg/mod
    key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
    restore-keys: ${{ runner.os }}-go-

# 上传制品
- uses: actions/upload-artifact@v4
  with:
    name: build-output
    path: dist/
    retention-days: 7

# 下载制品
- uses: actions/download-artifact@v4
  with:
    name: build-output
    path: dist/

# 发送 Slack 通知
- uses: slackapi/slack-github-action@v2
  with:
    webhook: ${{ secrets.SLACK_WEBHOOK }}
    webhook-type: incoming-webhook
    payload: |
      {"text": "Build ${{ job.status }}: ${{ github.repository }}"}

自托管 Runner

当项目需要特殊硬件、内网访问、或自定义环境时,自托管 Runner 是必要选择。

安装与配置

plaintext20 行
# 创建目录
mkdir actions-runner && cd actions-runner

# 下载最新版本(Linux x64 为例)
curl -o actions-runner-linux-x64-2.319.1.tar.gz -L \
  https://github.com/actions/runner/releases/download/v2.319.1/actions-runner-linux-x64-2.319.1.tar.gz

tar xzf actions-runner-linux-x64-2.319.1.tar.gz

# 配置(从 GitHub 仓库 Settings > Actions > Runners 获取 token)
./config.sh --url https://github.com/your-org/your-repo \
  --token YOUR_REGISTRATION_TOKEN \
  --labels self-hosted,linux,x64,gpu \
  --name prod-runner-01 \
  --work _work

# 作为 systemd 服务安装运行
sudo ./svc.sh install
sudo ./svc.sh start
sudo ./svc.sh status

标签管理

标签决定了哪些 Job 会被分配到哪个 Runner:

plaintext12 行
jobs:
  # 使用 GitHub 托管 Runner
  lint:
    runs-on: ubuntu-latest

  # 使用自托管 Runner(必须有所有指定标签)
  build:
    runs-on: [self-hosted, linux, gpu]

  # 使用标签做更细粒度的匹配
  deploy:
    runs-on: [self-hosted, linux, production]

Runner 分组(Runner Group)

对于大型团队,可以用 Runner Group 实现权限隔离:

plaintext4 行
jobs:
  deploy:
    runs-on: [self-hosted, linux, production]
    # 在仓库/组织设置中配置 Runner Group 的访问控制

安全考虑

自托管 Runner 的安全至关重要:

plaintext10 行
# 对于公开仓库,强烈建议仅使用临时 Runner
jobs:
  build:
    runs-on: [self-hosted, linux]
    container:
      image: node:24-slim        # 在容器中运行,隔离环境
      options: --read-only        # 只读文件系统
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test

安全清单:

  • 不要在公开仓库使用持久化的自托管 Runner(PR 可能包含恶意代码)
  • 使用容器模式运行 Job,限制资源访问
  • 定期更新 Runner 版本
  • 使用 CODEOWNERS 控制 Workflow 文件的修改权限
  • 为 Runner 设置专用的低权限用户

自动伸缩

使用 Actions Runner Controller (ARC) 在 Kubernetes 上实现 Runner 自动伸缩:

plaintext32 行
# ARC Helm values 示例
# helm install arc --namespace arc-systems \
#   actions-runner-controller/actions-runner-controller \
#   -f arc-values.yaml

# arc-values.yaml
authSecret:
  create: true
  github_token: "ghp_xxxxxxxxxxxx"

runnerDeployment:
  name: org-runner
  replicas: 2
  spec:
    organization: your-org
    labels:
      - k8s
      - linux
    resources:
      limits:
        cpu: "2"
        memory: "4Gi"

# HorizontalRunnerAutoscaler 配置
horizontalRunnerAutoscaler:
  minReplicas: 1
  maxReplicas: 10
  scaleDownDelaySecondsAfterScaleOut: 300
  metrics:
    - type: PercentageRunnersBusy
      scaleUpThreshold: '0.75'
      scaleDownThreshold: '0.25'

Secrets 管理

Secrets 层级

GitHub Actions 的 Secrets 分三个层级,优先级从高到低:

  1. Environment Secrets — 绑定到特定部署环境(如 production),可以配合审批流程
  2. Repository Secrets — 仓库级别,所有 Workflow 可访问
  3. Organization Secrets — 组织级别,可共享给多个仓库
plaintext9 行
steps:
  - name: Use secrets
    env:
      DB_PASSWORD: ${{ secrets.DB_PASSWORD }}           # Repository secret
      DEPLOY_KEY: ${{ secrets.PROD_DEPLOY_KEY }}        # Environment secret
      SHARED_API_KEY: ${{ secrets.ORG_API_KEY }}        # Organization secret
    run: |
      echo "Deploying with key..."
      # 不要 echo secret 值

配置 Environment Secrets

在 GitHub 仓库的 Settings → Environments 中配置:

plaintext11 行
# 使用 Environment
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production          # 引用 environment 名称
    steps:
      - name: Deploy
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: ./deploy.sh

OIDC 联合身份认证(推荐)

OIDC 无需存储长期凭据,是目前最安全的方式:

plaintext24 行
jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write              # 必须声明 id-token 权限
      contents: read
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
          aws-region: ap-southeast-1

      - name: Deploy to S3
        run: aws s3 sync ./dist s3://my-bucket

      # 同样支持 GCP、Azure
      - name: Authenticate to Google Cloud
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: projects/123456/locations/global/workloadIdentityPools/github/providers/github
          service_account: github-actions@my-project.iam.gserviceaccount.com

OIDC 配置步骤:

  1. 在云平台创建身份提供商(Identity Provider),指向 GitHub 的 OIDC 端点
  2. 创建角色并配置信任策略,仅允许特定仓库/分支的 Workflow 假设角色
  3. 在 Workflow 中使用对应的 Action 获取临时凭据

变量(Variables)vs Secrets

非敏感配置使用 Variables,敏感字段使用 Secrets:

plaintext8 行
# Repository/Environment Variables(非敏感,日志中可见)
env:
  APP_URL: ${{ vars.APP_URL }}
  FEATURE_FLAG: ${{ vars.ENABLE_NEW_UI }}

# Secrets(敏感,日志中被遮蔽)
env:
  API_KEY: ${{ secrets.API_KEY }}

常用场景实战

Node.js CI 完整配置

plaintext51 行
name: Node.js CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  contents: read
  pull-requests: write

jobs:
  ci:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 24
          cache: 'npm'

      - run: npm ci

      - name: Lint
        run: npm run lint

      - name: Type check
        run: npm run type-check

      - name: Unit tests
        run: npm test -- --coverage

      - name: Upload coverage
        if: github.event_name == 'pull_request'
        uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage/

      - name: Build
        run: npm run build

      - name: Upload build
        uses: actions/upload-artifact@v4
        with:
          name: build
          path: dist/
          retention-days: 3

Java CI 完整配置(Gradle)

plaintext41 行
name: Java CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '21'
          cache: 'gradle'

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Build
        run: ./gradlew build -x test

      - name: Test
        run: ./gradlew test

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: build/reports/tests/

      - name: Upload JAR
        uses: actions/upload-artifact@v4
        with:
          name: app-jar
          path: build/libs/*.jar

Docker 构建与推送

plaintext51 行
name: Docker Build & Push

on:
  push:
    tags: ['v*']

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      attestations: write
      id-token: write
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          platforms: linux/amd64,linux/arm64

自动发布 npm 包

plaintext35 行
name: Release

on:
  push:
    tags: ['v*']

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 24
          cache: 'npm'
          registry-url: 'https://registry.npmjs.org'

      - run: npm ci

      - run: npm run build

      - name: Publish to npm
        run: npm publish --provenance --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          generate_release_notes: true
          files: dist/*

多环境部署

环境定义与审批流程

plaintext48 行
name: Deploy

on:
  push:
    branches: [main]
  workflow_dispatch:
    inputs:
      environment:
        description: 'Target environment'
        required: true
        type: choice
        options: [staging, production]

jobs:
  deploy-dev:
    runs-on: ubuntu-latest
    environment: dev
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to dev
        run: |
          echo "Deploying to dev..."
          # 实际部署命令

  deploy-staging:
    runs-on: ubuntu-latest
    needs: deploy-dev
    environment:
      name: staging
      url: https://staging.example.com
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to staging
        run: echo "Deploying to staging..."

  deploy-production:
    runs-on: ubuntu-latest
    needs: deploy-staging
    environment:
      name: production
      url: https://example.com
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to production
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: echo "Deploying to production..."

环境保护规则

在 GitHub 仓库 Settings → Environments 中配置:

  • Required reviewers:指定审批人,部署前必须有人批准
  • Wait timer:部署前等待指定分钟数(可用于“冷静期”)
  • Deployment branches:限制只有特定分支可以部署到该环境
  • Environment Secrets / Variables:环境级别的配置和凭据

分支策略与自动部署

plaintext27 行
name: Multi-env Deploy

on:
  push:
    branches:
      - develop       # → dev 环境
      - 'release/*'   # → staging 环境
      - main          # → production 环境

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ github.ref_name == 'develop' && 'dev' || startsWith(github.ref_name, 'release/') && 'staging' || 'production' }}
    steps:
      - uses: actions/checkout@v4
      - name: Determine env
        id: env
        run: |
          if [[ "${{ github.ref_name }}" == "develop" ]]; then
            echo "target=dev" >> $GITHUB_OUTPUT
          elif [[ "${{ github.ref_name }}" == release/* ]]; then
            echo "target=staging" >> $GITHUB_OUTPUT
          else
            echo "target=production" >> $GITHUB_OUTPUT
          fi
      - name: Deploy to ${{ steps.env.outputs.target }}
        run: echo "Deploying to ${{ steps.env.outputs.target }}"

复合 Action 与可重用 Workflow

复合 Action(Composite Action)

复合 Action 将多个步骤打包成一个可复用的单元,存放在仓库中:

plaintext32 行
# .github/actions/setup-project/action.yml
name: 'Setup Project'
description: 'Install dependencies and setup environment'

inputs:
  node-version:
    description: 'Node.js version'
    required: false
    default: '20'
  install-command:
    description: 'Install command'
    required: false
    default: 'npm ci'

runs:
  using: 'composite'
  steps:
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: 'npm'

    - name: Install dependencies
      shell: bash
      run: ${{ inputs.install-command }}

    - name: Verify setup
      shell: bash
      run: |
        echo "Node: $(node -v)"
        echo "npm: $(npm -v)"

使用复合 Action:

plaintext6 行
steps:
  - uses: actions/checkout@v4
  - uses: ./.github/actions/setup-project
    with:
      node-version: '22'
  - run: npm test

可重用 Workflow(Reusable Workflow)

可重用 Workflow 通过 workflow_call 触发,适合跨仓库或跨项目的标准化流程:

plaintext39 行
# .github/workflows/reusable-deploy.yml(可重用 Workflow)
name: Reusable Deploy

on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
      image-tag:
        required: true
        type: string
      replicas:
        required: false
        type: number
        default: 2
    secrets:
      DEPLOY_TOKEN:
        required: true
    outputs:
      deploy-url:
        description: "Deployment URL"
        value: ${{ jobs.deploy.outputs.url }}

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    outputs:
      url: ${{ steps.deploy.outputs.url }}
    steps:
      - name: Deploy
        id: deploy
        env:
          TOKEN: ${{ secrets.DEPLOY_TOKEN }}
        run: |
          echo "Deploying ${{ inputs.image-tag }} to ${{ inputs.environment }}"
          echo "Replicas: ${{ inputs.replicas }}"
          echo "url=https://${{ inputs.environment }}.example.com" >> $GITHUB_OUTPUT

调用可重用 Workflow:

plaintext26 行
# .github/workflows/deploy.yml
name: Deploy Application

on:
  push:
    branches: [main]

jobs:
  deploy-staging:
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: staging
      image-tag: ${{ github.sha }}
      replicas: 2
    secrets:
      DEPLOY_TOKEN: ${{ secrets.STAGING_DEPLOY_TOKEN }}

  deploy-production:
    needs: deploy-staging
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: production
      image-tag: ${{ github.sha }}
      replicas: 5
    secrets:
      DEPLOY_TOKEN: ${{ secrets.PROD_DEPLOY_TOKEN }}

跨仓库调用:

plaintext6 行
jobs:
  ci:
    uses: your-org/shared-workflows/.github/workflows/node-ci.yml@main
    with:
      node-version: '24'
    secrets: inherit    # 继承调用方的所有 secrets

缓存优化

actions/cache 基础

plaintext9 行
- name: Cache Go modules
  uses: actions/cache@v4
  with:
    path: |
      ~/.cache/go-build
      ~/go/pkg/mod
    key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
    restore-keys: |
      ${{ runner.os }}-go-

内置缓存(推荐)

很多 setup Action 已内置缓存支持,无需手动配置 actions/cache:

plaintext29 行
# Node.js —— 缓存 npm/pnpm/yarn
- uses: actions/setup-node@v4
  with:
    node-version: 24
    cache: 'npm'           # 自动检测 lockfile 并缓存

# Java —— 缓存 Gradle/Maven
- uses: actions/setup-java@v4
  with:
    distribution: temurin
    java-version: 21
    cache: 'gradle'

# Python —— 缓存 pip/poetry/pipenv
- uses: actions/setup-python@v5
  with:
    python-version: '3.12'
    cache: 'pip'

# Go —— 使用 actions/cache
- uses: actions/cache@v4
  with:
    path: |
      ~/.cache/go-build
      ~/go/pkg/mod
    key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}

# Rust —— 缓存 cargo
- uses: Swatinem/rust-cache@v2

Docker 构建缓存

plaintext33 行
# 方式一:使用 GitHub Actions 缓存后端(推荐)
- uses: docker/build-push-action@v6
  with:
    context: .
    push: true
    tags: my-app:latest
    cache-from: type=gha
    cache-to: type=gha,mode=max

# 方式二:使用 Registry 缓存
- uses: docker/build-push-action@v6
  with:
    context: .
    push: true
    tags: my-app:latest
    cache-from: type=registry,ref=ghcr.io/my-app:buildcache
    cache-to: type=registry,ref=ghcr.io/my-app:buildcache,mode=max

# 方式三:使用本地缓存
- uses: actions/cache@v4
  with:
    path: /tmp/.buildx-cache
    key: ${{ runner.os }}-buildx-${{ github.sha }}
    restore-keys: ${{ runner.os }}-buildx-

- uses: docker/build-push-action@v6
  with:
    context: .
    cache-from: type=local,src=/tmp/.buildx-cache
    cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max

# 防止缓存无限增长
- run: rm -rf /tmp/.buildx-cache && mv /tmp/.buildx-cache-new /tmp/.buildx-cache

缓存最佳实践

  • Key 设计:使用 hashFiles() 基于 lockfile 生成 key,确保依赖变化时缓存失效
  • restore-keys:设置回退 key,即使精确匹配失败也能用到近似的旧缓存
  • 缓存大小:GitHub 托管 Runner 每个仓库有 10GB 缓存上限,超出会自动清理最旧的
  • 跨分支:缓存在同一仓库的分支间共享,main 分支的缓存可以被 PR 分支读取

最佳实践

安全

plaintext23 行
# 1. 最小权限原则
permissions:
  contents: read          # 仅读取代码
  # 不要写 permissions: write-all

# 2. Pin Action 版本(避免供应链攻击)
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1
  # 或至少用主版本号
  # uses: actions/checkout@v4

# 3. 不要在日志中暴露敏感信息
- name: Safe logging
  run: |
    echo "::add-mask::$MY_SECRET"     # 手动遮蔽
    echo "Token length: ${#MY_SECRET}" # 只打印长度

# 4. 在任务权限范围内使用 GITHUB_TOKEN(自动过期,按需授权)
- uses: actions/checkout@v4
  with:
    token: ${{ secrets.GITHUB_TOKEN }}

# 5. 验证第三方 Action 的来源
# 优先使用 GitHub 官方 Action 和知名组织的 Action

性能优化

plaintext35 行
# 1. 使用 Job 级别的 timeout
jobs:
  build:
    timeout-minutes: 15

# 2. 合理使用 needs 做并行
jobs:
  lint:
    runs-on: ubuntu-latest
    steps: [...]
  test:
    runs-on: ubuntu-latest      # 和 lint 并行
    steps: [...]
  build:
    needs: [lint, test]         # lint 和 test 都完成后
    steps: [...]

# 3. 条件跳过不必要的 Job
jobs:
  deploy:
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    steps: [...]

# 4. 合并多个小 Step 减少启动开销
- name: Setup and test
  run: |
    npm ci
    npm run lint
    npm test
    npm run build

# 5. 使用 paths 过滤避免不必要的触发
on:
  push:
    paths: ['src/**', 'tests/**', 'package.json', 'package-lock.json']

调试技巧

plaintext26 行
# 1. 启用调试日志(在仓库 Settings > Secrets 中设置)
# ACTIONS_STEP_DEBUG = true
# ACTIONS_RUNNER_DEBUG = true

# 2. 使用 act 在本地测试(https://github.com/nektos/act)
# act -j build

# 3. 打印上下文信息
- name: Debug context
  env:
    GITHUB_CONTEXT: ${{ toJson(github) }}
    JOB_CONTEXT: ${{ toJson(job) }}
    STEPS_CONTEXT: ${{ toJson(steps) }}
    RUNNER_CONTEXT: ${{ toJson(runner) }}
  run: |
    echo "Event: ${{ github.event_name }}"
    echo "Ref: ${{ github.ref }}"
    echo "SHA: ${{ github.sha }}"
    echo "Actor: ${{ github.actor }}"
    echo "Runner OS: ${{ runner.os }}"

# 4. tmate 远程调试(暂停 Workflow 并提供 SSH 访问)
- name: Setup tmate session
  if: failure()                        # 仅在失败时启动
  uses: mxschmitt/action-tmate@v3
  # 或者用 workflow_dispatch 手动触发调试

成本控制

GitHub Actions 定价要点:

  • 公开仓库免费
  • 私有仓库每月有免费额度(GitHub Free: 2000 分钟/月,Pro: 3000 分钟/月)
  • 不同 Runner 的系数不同:Linux = 1x,macOS = 10x,Windows = 2x

成本优化策略:

plaintext27 行
# 1. 使用 paths 过滤
on:
  push:
    paths: ['src/**']    # 只有源码变更才触发

# 2. 及时取消过时的 Workflow
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true     # 同一分支的新提交会取消旧的运行

# 3. 使用小型 Runner(按需选择)
runs-on: ubuntu-latest          # Linux 最便宜
# runs-on: macos-latest        # 按当前 Runner 类型、套餐和计费表核价

# 4. 缓存依赖,减少安装时间
- uses: actions/setup-node@v4
  with:
    node-version: 24
    cache: 'npm'

# 5. 精简 CI 步骤
# - 跳过 lint 如果有本地 pre-commit hook
# - 只在 PR 时跑完整测试,push 时只跑核心测试

# 6. 合并多个小 Workflow
# - 避免同一个事件触发多个 Workflow
# - 使用 reusable workflow 减少重复配置

维护性

plaintext33 行
# 1. 使用 YAML 锚点减少重复(GitHub Actions 不原生支持,但可以用 reusable workflow 替代)

# 2. 统一 Action 版本管理(使用 Dependabot)
# .github/dependabot.yml
# version: 2
# updates:
#   - package-ecosystem: "github-actions"
#     directory: "/"
#     schedule:
#       interval: "weekly"

# 3. 使用 Matrix 测试多版本
strategy:
  matrix:
    node: [18, 20, 22]

# 4. 为每个 Job 设置有意义的 name
jobs:
  test:
    name: "Test (Node ${{ matrix.node }})"

# 5. 使用 Workflow 链接相关 PR
- name: Add PR comment
  if: github.event_name == 'pull_request'
  uses: actions/github-script@v7
  with:
    script: |
      github.rest.issues.createComment({
        issue_number: context.issue.number,
        owner: context.repo.owner,
        repo: context.repo.repo,
        body: `[可用] Build passed! [View logs](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`
      })

附录:常见问题速查

Q: Workflow 文件在哪里? A: .github/workflows/ 目录下,YAML 格式,文件名任意。

Q: 如何手动触发 Workflow? A: 在 on 中添加 workflow_dispatch,然后在 GitHub Actions 页面点击 “Run workflow”。

Q: Secrets 有大小限制吗? A: 单个 Secret 最大 64KB,每个仓库最多 100 个,组织最多 1000 个。

Q: 如何在 Job 之间传递文件? A: 使用 actions/upload-artifact 和 actions/download-artifact。

Q: Cron 表达式是哪个时区? A: UTC。北京时间要减 8 小时,比如北京时间 10:00 = UTC 02:00。

Q: 如何复用同一仓库的 Workflow? A: 使用 workflow_call 定义可重用 Workflow,然后用 uses: ./.github/workflows/xxx.yml 调用。

Q: 自托管 Runner 如何清理工作目录? A: Runner 默认在每次 Job 结束后清理工作目录。如果需要保留,可以在 Step 中使用 actions/cache。

Action 运行时与应用版本

setup-node 选择应用 Node 版本,Action 本身的运行时由 Action release 决定。更新 Action 主版本还要核对 self-hosted runner 最低版本;应用已切换 Node 24 不代表旧 Runner 能运行新 Action。

发布凭据限制在指定环境与分支。处理外部 PR 时,不能在拥有高权限的上下文执行其未审阅代码。制品按提交与摘要关联,下载后核对来源;缓存用于加速构建,不提供制品可信性保证。

发布模式与回滚

原理

同时维护两套完全相同的生产环境(蓝和绿),任何时候只有一套对外服务。发布新版本时部署到闲置环境,验证通过后切换流量,出问题立即切回。

plaintext13 行
                  ┌─────────────┐
  用户请求 ───→  │  负载均衡器   │
                  └──────┬──────┘
                         │
              ┌──────────┴──────────┐
              ▼                     ▼
        ┌──────────┐          ┌──────────┐
        │  蓝(当前) │          │  绿(新)   │
        │  v1.2.3  │          │  v1.3.0  │
        └──────────┘          └──────────┘

  发布前:流量 → 蓝
  发布后:流量 → 绿(蓝保留作回滚)

Nginx 实现

plaintext18 行
# /etc/nginx/conf.d/blue-green.conf
upstream app {
    # 切换这一行即可完成蓝绿切换
    server 10.0.1.10:8080;  # 蓝环境
    # server 10.0.2.10:8080;  # 绿环境
}

server {
    listen 80;
    server_name www.example.com;

    location / {
        proxy_pass http://app;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

自动化切换脚本:

plaintext27 行
#!/bin/bash
# switch-env.sh - 蓝绿切换脚本
CURRENT=$(grep -oP 'server \K[\d.]+' /etc/nginx/conf.d/blue-green.conf)
BLUE="10.0.1.10"
GREEN="10.0.2.10"

if [ "$CURRENT" = "$BLUE" ]; then
    NEW_TARGET=$GREEN
    ENV_NAME="green"
else
    NEW_TARGET=$BLUE
    ENV_NAME="blue"
fi

# 预检:确认新环境健康
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://${NEW_TARGET}:8080/health)
if [ "$HTTP_CODE" != "200" ]; then
    echo " 新环境健康检查失败 (HTTP $HTTP_CODE),中止切换"
    exit 1
fi

# 执行切换
sed -i "s/server ${CURRENT}:8080/server ${NEW_TARGET}:8080/" /etc/nginx/conf.d/blue-green.conf
nginx -t && nginx -s reload

echo "[可用] 已切换到 ${ENV_NAME} 环境 (${NEW_TARGET})"
echo "回滚命令: sed -i 's/${NEW_TARGET}/${CURRENT}/' /etc/nginx/conf.d/blue-green.conf && nginx -s reload"

Docker Compose 实现

yaml32 行
# docker-compose.blue-green.yml

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - app-blue
      - app-green

  app-blue:
    image: registry.example.com/myapp:${BLUE_VERSION:-1.2.3}
    environment:
      - NODE_ENV=production
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 10s
      timeout: 5s
      retries: 3

  app-green:
    image: registry.example.com/myapp:${GREEN_VERSION:-1.3.0}
    environment:
      - NODE_ENV=production
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 10s
      timeout: 5s
      retries: 3

Kubernetes 实现

yaml46 行
# k8s/blue-green-deployment.yaml
# 蓝环境(当前版本)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-blue
  labels:
    app: myapp
    version: blue
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
      version: blue
  template:
    metadata:
      labels:
        app: myapp
        version: blue
    spec:
      containers:
        - name: app
          image: registry.example.com/myapp:1.2.3
          ports:
            - containerPort: 8080
          readinessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 5

---
# Service 通过 selector 切换蓝绿
apiVersion: v1
kind: Service
metadata:
  name: myapp
spec:
  selector:
    app: myapp
    version: blue  # 改成 green 即切换
  ports:
    - port: 80
      targetPort: 8080

切换命令:

plaintext5 行
# 切换到绿环境
kubectl patch service myapp -p '{"spec":{"selector":{"version":"green"}}}'

# 回滚到蓝环境
kubectl patch service myapp -p '{"spec":{"selector":{"version":"blue"}}}'

金丝雀发布

原理

将新版本逐步推送给一小部分用户,观察指标无异常后再全量发布。像矿井中的金丝雀一样,先让“探路者”验证安全性。

plaintext11 行
  流量分配:
  ┌────────────────────────────────────────┐
  │ ████████████████████████████░░░░ 90%  │  ← 旧版本 (stable)
  │ ████████░░░░░░░░░░░░░░░░░░░░ 10%  │  ← 新版本 (canary)
  └────────────────────────────────────────┘

  时间线:
  0% → 10% → 25% → 50% → 100%
  │      │      │      │      │
  观察   观察   观察   观察   全量发布
  15min  15min  15min  15min

Nginx 流量控制

plaintext26 行
# 基于权重的金丝雀
upstream app {
    server 10.0.1.10:8080 weight=90;  # 稳定版
    server 10.0.2.10:8080 weight=10;  # 金丝雀
}

# 基于 Header 的金丝雀(内部测试用户)
map $http_x_canary $backend {
    "true"  canary_backend;
    default stable_backend;
}

upstream stable_backend {
    server 10.0.1.10:8080;
}

upstream canary_backend {
    server 10.0.2.10:8080;
}

server {
    listen 80;
    location / {
        proxy_pass http://$backend;
    }
}

Kubernetes 金丝雀

yaml55 行
# 稳定版 Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-stable
spec:
  replicas: 9
  selector:
    matchLabels:
      app: myapp
      track: stable
  template:
    metadata:
      labels:
        app: myapp
        track: stable
    spec:
      containers:
        - name: app
          image: registry.example.com/myapp:1.2.3

---
# 金丝雀 Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-canary
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp
      track: canary
  template:
    metadata:
      labels:
        app: myapp
        track: canary
    spec:
      containers:
        - name: app
          image: registry.example.com/myapp:1.3.0

---
# Service 同时匹配 stable 和 canary
apiVersion: v1
kind: Service
metadata:
  name: myapp
spec:
  selector:
    app: myapp  # 不含 track,匹配两者
  ports:
    - port: 80
      targetPort: 8080

监控与自动回滚

Prometheus 告警规则(金丝雀指标监控):

yaml27 行
# prometheus-rules.yaml
groups:
  - name: canary-monitoring
    rules:
      - alert: CanaryHighErrorRate
        expr: |
          sum(rate(http_requests_total{track="canary",status=~"5.."}[5m]))
          /
          sum(rate(http_requests_total{track="canary"}[5m]))
          > 0.05
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "金丝雀错误率超过 5%"
          description: "金丝雀版本错误率 {{ $value | humanizePercentage }},建议回滚"

      - alert: CanaryHighLatency
        expr: |
          histogram_quantile(0.95,
            sum(rate(http_request_duration_seconds_bucket{track="canary"}[5m])) by (le)
          ) > 2
        for: 3m
        labels:
          severity: warning
        annotations:
          summary: "金丝雀 P95 延迟超过 2 秒"

自动回滚脚本:

plaintext14 行
#!/bin/bash
# canary-rollback.sh
ERROR_RATE=$(curl -s 'http://prometheus:9090/api/v1/query' \
  --data-urlencode 'query=sum(rate(http_requests_total{track="canary",status=~"5.."}[5m]))/sum(rate(http_requests_total{track="canary"}[5m]))' \
  | jq -r '.data.result[0].value[1]')

if (( $(echo "$ERROR_RATE > 0.05" | bc -l) )); then
    echo " 金丝雀错误率 ${ERROR_RATE},执行回滚"
    kubectl scale deployment app-canary --replicas=0
    kubectl scale deployment app-stable --replicas=10
    echo "[可用] 回滚完成"
else
    echo "[可用] 金丝雀指标正常,错误率 ${ERROR_RATE}"
fi

Flagger 自动化金丝雀

yaml33 行
# Flagger Canary 资源(适用于 Istio/Linkerd/Nginx Ingress)
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
  name: myapp
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: myapp
  progressDeadlineSeconds: 600
  service:
    port: 80
  analysis:
    interval: 1m
    threshold: 5           # 最大失败次数
    maxWeight: 50          # 最大金丝雀权重
    stepWeight: 10         # 每次增加的权重
    metrics:
      - name: request-success-rate
        thresholdRange:
          min: 99          # 成功率 > 99%
        interval: 1m
      - name: request-duration
        thresholdRange:
          max: 500         # P99 < 500ms
        interval: 1m
    webhooks:
      - name: load-test
        type: rollout
        url: http://flagger-loadtester.test/
        metadata:
          cmd: "hey -z 1m -q 10 -c 2 http://myapp-canary.test/"

滚动更新

策略配置

Kubernetes 原生的默认更新策略,逐步用新 Pod 替换旧 Pod。

yaml31 行
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 10
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 3        # 最多多出 3 个 Pod(13 个总 Pod)
      maxUnavailable: 1  # 最多 1 个 Pod 不可用(至少 9 个可用)
  # 等价于:一次更新 3 个 Pod,保证至少 9 个健康
  template:
    spec:
      containers:
        - name: app
          image: registry.example.com/myapp:1.3.0
          readinessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 15
            periodSeconds: 5
            successThreshold: 1
            failureThreshold: 3
          livenessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 10

参数调优建议:

场景 maxSurge maxUnavailable 说明
高可用 25% 0 不允许任何 Pod 不可用
快速发布 50% 25% 更快的更新速度
资源紧张 0 1 先删后建,不超资源配额

健康检查三件套

plaintext28 行
containers:
  - name: app
    # 启动探针:慢启动应用专用,避免 liveness 误杀
    startupProbe:
      httpGet:
        path: /health
        port: 8080
      failureThreshold: 30    # 最多等 30*5=150 秒
      periodSeconds: 5

    # 就绪探针:决定是否接收流量
    readinessProbe:
      httpGet:
        path: /ready
        port: 8080
      initialDelaySeconds: 5
      periodSeconds: 5
      successThreshold: 1
      failureThreshold: 3     # 连续 3 次失败则摘除

    # 存活探针:决定是否重启容器
    livenessProbe:
      httpGet:
        path: /health
        port: 8080
      initialDelaySeconds: 30
      periodSeconds: 10
      failureThreshold: 3     # 连续 3 次失败则重启

回滚机制

plaintext17 行
# 查看发布历史
kubectl rollout history deployment/myapp

# 回滚到上一版本
kubectl rollout undo deployment/myapp

# 回滚到指定版本
kubectl rollout undo deployment/myapp --to-revision=3

# 查看回滚状态
kubectl rollout status deployment/myapp --timeout=120s

# 暂停发布(发现异常时)
kubectl rollout pause deployment/myapp

# 恢复发布
kubectl rollout resume deployment/myapp

CI/CD 集成回滚:

plaintext11 行
# GitLab CI 自动回滚
deploy:
  stage: deploy
  script:
    - kubectl apply -f k8s/
    - kubectl rollout status deployment/myapp --timeout=300s || {
        echo "部署超时,自动回滚";
        kubectl rollout undo deployment/myapp;
        kubectl rollout status deployment/myapp --timeout=120s;
        exit 1;
      }

回滚策略

版本管理

语义化版本 + 制品不可变:

plaintext6 行
版本号: MAJOR.MINOR.PATCH (如 1.3.2)
  MAJOR: 不兼容的 API 变更
  MINOR: 向后兼容的功能新增
  PATCH: 向后兼容的 Bug 修复

制品标签: v1.3.2 / v1.3.2-abc1234 (带 commit hash)

Docker 镜像版本策略:

plaintext10 行
# [不适用] 避免使用 latest
docker build -t myapp:latest .

# [可用] 使用语义化版本 + commit hash
docker build -t myapp:v1.3.2 -t myapp:v1.3.2-abc1234 .

# CI 中自动生成标签
VERSION=$(git describe --tags --always)
docker build -t registry.example.com/myapp:${VERSION} .
docker push registry.example.com/myapp:${VERSION}

数据库回滚

数据库变更是回滚中最棘手的部分。核心原则:向前兼容,分步执行。

安全的数据库变更模式:

plaintext4 行
第 1 步:新增列(Additive only)→ 部署新代码
第 2 步:新代码同时读写新旧列 → 验证
第 3 步:新代码只读写新列 → 停止写旧列
第 4 步:删除旧列(在下个版本)

回滚脚本模板:

plaintext7 行
-- migration/V3__add_email_column.sql (向前)
ALTER TABLE users ADD COLUMN email VARCHAR(255);
UPDATE users SET email = CONCAT(username, '@example.com');
ALTER TABLE users ALTER COLUMN email SET NOT NULL;

-- rollback/V3__rollback_email_column.sql (回滚)
ALTER TABLE users DROP COLUMN IF EXISTS email;

快速回滚 SOP

plaintext43 行
#!/bin/bash
# rollback.sh - 生产快速回滚 SOP
set -euo pipefail

APP_NAME="${1:?用法: ./rollback.sh <app-name> [revision]}"
REVISION="${2:-}"

echo " 开始回滚 ${APP_NAME}"

# Step 1: 确认当前版本
echo "当前版本:"
kubectl get deployment ${APP_NAME} -o jsonpath='{.spec.template.spec.containers[0].image}'
echo ""

# Step 2: 执行回滚
if [ -n "$REVISION" ]; then
    kubectl rollout undo deployment/${APP_NAME} --to-revision=${REVISION}
else
    kubectl rollout undo deployment/${APP_NAME}
fi

# Step 3: 等待回滚完成
kubectl rollout status deployment/${APP_NAME} --timeout=180s

# Step 4: 验证
NEW_IMAGE=$(kubectl get deployment ${APP_NAME} -o jsonpath='{.spec.template.spec.containers[0].image}')
echo "[可用] 回滚完成,当前镜像: ${NEW_IMAGE}"

# Step 5: 检查 Pod 状态
kubectl get pods -l app=${APP_NAME} -o wide

# Step 6: 健康检查
for i in {1..5}; do
    STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://www.example.com/health || true)
    if [ "$STATUS" = "200" ]; then
        echo " 健康检查通过"
        exit 0
    fi
    echo "等待健康检查... (${i}/5)"
    sleep 5
done
echo " 健康检查失败,请人工介入"
exit 1

制品仓库

主流制品仓库对比

特性 Nexus Repository JFrog Artifactory Harbor
类型 通用仓库 通用仓库 Docker 专用
Docker 支持 [可用] [可用] [可用](核心功能)
Maven/Npm/PyPI [可用] [可用] [不适用]
免费版 OSS 版免费 CE 版免费 完全开源
高可用 付费版 付费版 [可用]
适合场景 中小团队 企业级 K8s/容器化

Nexus 配置示例

yaml14 行
# Docker Compose 部署 Nexus
services:
  nexus:
    image: sonatype/nexus3:3.68.0
    ports:
      - "8081:8081"
      - "8082:8082"  # Docker registry port
    volumes:
      - nexus-data:/nexus-data
    environment:
      - NEXUS_SECURITY_RANDOMPASSWORD=false

volumes:
  nexus-data:

Nexus Docker Registry 使用:

plaintext15 行
# 登录
docker login nexus.example.com:8082

# 推送镜像
docker tag myapp:v1.3.0 nexus.example.com:8082/myapp:v1.3.0
docker push nexus.example.com:8082/myapp:v1.3.0

# CI 中使用
# .gitlab-ci.yml
build:
  stage: build
  script:
    - docker login -u $NEXUS_USER -p $NEXUS_PASS nexus.example.com:8082
    - docker build -t nexus.example.com:8082/myapp:${CI_COMMIT_TAG} .
    - docker push nexus.example.com:8082/myapp:${CI_COMMIT_TAG}

Harbor 配置

plaintext12 行
# Harbor 安装 (docker-compose)
# 下载 Harbor 离线安装包
wget https://github.com/goharbor/harbor/releases/download/v2.11.0/harbor-offline-installer-v2.11.0.tgz
tar xzf harbor-offline-installer-v2.11.0.tgz
cd harbor

# 编辑 harbor.yml
# hostname: harbor.example.com
# harbor_admin_password: StrongPassword123!
# database.password: dbpassword

./install.sh --with-trivy  # 启用漏洞扫描

Harbor 项目与镜像策略:

plaintext17 行
# 创建项目
curl -u admin:password -X POST https://harbor.example.com/api/v2.0/projects \
  -H "Content-Type: application/json" \
  -d '{"project_name": "myteam", "public": false}'

# 配置镜像保留策略(保留最近 30 个 tag)
curl -u admin:password -X POST \
  "https://harbor.example.com/api/v2.0/projects/myteam/repositories/myapp/policies" \
  -H "Content-Type: application/json" \
  -d '{
    "rules": [{
      "template": "latestPushedK",
      "params": {"latestPushedK": 30},
      "tag_selectors": [{"kind": "wildcard", "decoration": "matches", "pattern": "**"}],
      "repo_selectors": [{"kind": "doublestar", "decoration": "matches", "pattern": "**"}]
    }]
  }'

制品版本策略与垃圾回收

plaintext18 行
# GitLab CI 制品版本管理
variables:
  IMAGE_REGISTRY: harbor.example.com/myteam
  IMAGE_TAG: ${CI_COMMIT_TAG:-${CI_COMMIT_SHORT_SHA}}

build:
  stage: build
  script:
    - docker build -t ${IMAGE_REGISTRY}/myapp:${IMAGE_TAG} .
    - docker push ${IMAGE_REGISTRY}/myapp:${IMAGE_TAG}
    # 额外推送 latest(仅 main 分支)
    - if [ "$CI_COMMIT_BRANCH" = "main" ]; then
        docker tag ${IMAGE_REGISTRY}/myapp:${IMAGE_TAG} ${IMAGE_REGISTRY}/myapp:latest;
        docker push ${IMAGE_REGISTRY}/myapp:latest;
      fi
  rules:
    - if: $CI_COMMIT_TAG
    - if: $CI_COMMIT_BRANCH == "main"

Harbor 垃圾回收:

plaintext8 行
# 手动触发垃圾回收
curl -u admin:password -X POST \
  "https://harbor.example.com/api/v2.0/system/gc/schedule" \
  -H "Content-Type: application/json" \
  -d '{
    "parameters": {"delete_untagged": true},
    "schedule": {"type": "Weekly", "weekday": 1, "offtime": 0}
  }'

数据库迁移

Flyway

Flyway 使用纯 SQL 脚本,简单直接,学习成本低。

目录结构:

plaintext5 行
db/migration/
  V1__create_users_table.sql
  V2__add_email_column.sql
  V3__create_orders_table.sql
  V4__add_index_on_email.sql

SQL 示例:

plaintext12 行
-- V1__create_users_table.sql
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    username VARCHAR(100) NOT NULL UNIQUE,
    email VARCHAR(255),
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);

-- V2__add_email_column.sql
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
CREATE INDEX idx_users_phone ON users(phone);

Flyway 集成到 CI/CD:

plaintext34 行
# GitLab CI 数据库迁移
db:migrate:
  stage: migrate
  image: flyway/flyway:10
  script:
    - flyway -url=jdbc:postgresql://${DB_HOST}:5432/${DB_NAME}
             -user=${DB_USER}
             -password=${DB_PASS}
             -locations=filesystem:./db/migration
             migrate
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      changes:
        - "db/migration/**/*"

db:info:
  stage: check
  image: flyway/flyway:10
  script:
    - flyway -url=jdbc:postgresql://${DB_HOST}:5432/${DB_NAME}
             -user=${DB_USER}
             -password=${DB_PASS}
             -locations=filesystem:./db/migration
             info

db:repair:
  stage: fix
  image: flyway/flyway:10
  script:
    - flyway -url=jdbc:postgresql://${DB_HOST}:5432/${DB_NAME}
             -user=${DB_USER}
             -password=${DB_PASS}
             repair
  when: manual

Liquibase

Liquibase 使用 XML/YAML/JSON 描述变更,支持自动回滚。

changelog.yaml:

plaintext48 行
databaseChangeLog:
  - changeSet:
      id: 1
      author: devteam
      changes:
        - createTable:
            tableName: users
            columns:
              - column:
                  name: id
                  type: bigint
                  autoIncrement: true
                  constraints:
                    primaryKey: true
                    nullable: false
              - column:
                  name: username
                  type: varchar(100)
                  constraints:
                    nullable: false
                    unique: true
      rollback:
        - dropTable:
            tableName: users

  - changeSet:
      id: 2
      author: devteam
      changes:
        - addColumn:
            tableName: users
            columns:
              - column:
                  name: phone
                  type: varchar(20)
        - createIndex:
            tableName: users
            indexName: idx_users_phone
            columns:
              - column:
                  name: phone
      rollback:
        - dropIndex:
            tableName: users
            indexName: idx_users_phone
        - dropColumn:
            tableName: users
            columnName: phone

迁移策略

Golden Rules:

  1. 只追加,不修改 — 不要 ALTER 已有列的数据类型
  2. 向后兼容 — 新代码必须能跑在旧 schema 上
  3. 小步快走 — 每次迁移只做一个变更
  4. 先扩后缩 — 新增列 → 迁移数据 → 去掉旧列(跨版本完成)

CI 集成模式:

plaintext2 行
代码合并 → CI 构建镜像 → 先跑 DB Migration → 再部署应用 → 健康检查
                          ↑ 数据库变更先于应用部署

安全与合规

SAST / DAST / SCA 集成

SAST(静态应用安全测试)— 代码层面:

plaintext12 行
# GitLab CI 集成 Semgrep
sast:
  stage: security
  image: returntocorp/semgrep
  script:
    - semgrep --config=auto --json --output=semgrep-results.json .
  artifacts:
    reports:
      sast: semgrep-results.json
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == "main"

SCA(软件成分分析)— 依赖层面:

plaintext17 行
# GitLab CI 集成 Trivy 扫描依赖漏洞
dependency-scan:
  stage: security
  image: aquasec/trivy:latest
  script:
    - trivy fs --format json --output trivy-results.json --severity HIGH,CRITICAL .
    - trivy fs --exit-code 1 --severity CRITICAL .  # 高危漏洞直接失败
  artifacts:
    reports:
      dependency_scanning: trivy-results.json

# Docker 镜像漏洞扫描
image-scan:
  stage: security
  image: aquasec/trivy:latest
  script:
    - trivy image --exit-code 1 --severity CRITICAL ${IMAGE_REGISTRY}/myapp:${CI_COMMIT_TAG}

DAST(动态应用安全测试)— 运行层面:

plaintext12 行
# 集成 OWASP ZAP
dast:
  stage: security
  image: ghcr.io/zaproxy/zaproxy:stable
  script:
    - zap-baseline.py -t https://staging.example.com -r zap-report.html
  artifacts:
    paths:
      - zap-report.html
    when: always
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

审批流程

plaintext27 行
# GitLab CI 多级审批
deploy:staging:
  stage: deploy
  environment:
    name: staging
  script:
    - ./deploy.sh staging
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

deploy:prod:
  stage: deploy
  environment:
    name: production
    deployment_tier: production
  script:
    - ./deploy.sh production
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
  when: manual
  allow_failure: false

# GitLab Protected Environment 审批
# Settings → CI/CD → Protected Environments
# 添加 production 环境保护
# 允许部署的角色: Maintainer
# 所需审批人数: 2

审计日志

plaintext25 行
# 部署审计记录
deploy:prod:
  stage: deploy
  script:
    - |
      # 记录部署审计日志
      AUDIT_LOG=$(cat <<EOF
      {
        "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
        "action": "deploy",
        "environment": "production",
        "version": "${CI_COMMIT_TAG}",
        "deployer": "${GITLAB_USER_LOGIN}",
        "pipeline_id": "${CI_PIPELINE_ID}",
        "commit_sha": "${CI_COMMIT_SHA}",
        "commit_message": "$(git log -1 --pretty=%B)"
      }
      EOF
      )
      echo "$AUDIT_LOG" >> /var/log/deploy-audit.jsonl
      # 推送到集中式日志
      curl -X POST "https://elasticsearch:9200/deploy-audit/_doc" \
        -H "Content-Type: application/json" \
        -d "$AUDIT_LOG"
    - ./deploy.sh production

完整流水线模板

前端项目(React/Vue + Docker)

plaintext145 行
# .gitlab-ci.yml - 前端项目完整流水线
variables:
  IMAGE_REGISTRY: harbor.example.com/frontend
  IMAGE_TAG: ${CI_COMMIT_TAG:-${CI_COMMIT_SHORT_SHA}}
  NODE_VERSION: "24"

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

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

# ── 安装依赖 ──
install:
  stage: install
  image: node:${NODE_VERSION}-alpine
  script:
    - npm ci --prefer-offline
  artifacts:
    paths:
      - node_modules/
    expire_in: 1 hour

# ── 代码检查 ──
lint:
  stage: lint
  image: node:${NODE_VERSION}-alpine
  needs: [install]
  script:
    - npm run lint
    - npm run type-check

# ── 单元测试 ──
test:unit:
  stage: test
  image: node:${NODE_VERSION}-alpine
  needs: [install]
  script:
    - npm run test:unit -- --coverage
  coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml
      junit: junit.xml

# ── 构建 ──
build:
  stage: build
  image: node:${NODE_VERSION}-alpine
  needs: [install]
  script:
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 7 days

# ── 安全扫描 ──
sast:
  stage: security
  image: returntocorp/semgrep
  script:
    - semgrep --config=auto --json --output=sast.json .
  artifacts:
    reports:
      sast: sast.json
  allow_failure: true

dependency-scan:
  stage: security
  image: aquasec/trivy:latest
  script:
    - trivy fs --exit-code 1 --severity CRITICAL .

# ── Docker 构建推送 ──
docker:build:
  stage: docker
  image: docker:24
  services:
    - docker:24-dind
  needs: [build, test:unit]
  script:
    - docker login -u $HARBOR_USER -p $HARBOR_PASS $IMAGE_REGISTRY
    - docker build -t ${IMAGE_REGISTRY}/myapp:${IMAGE_TAG} .
    - docker push ${IMAGE_REGISTRY}/myapp:${IMAGE_TAG}
    - |
      if [ "$CI_COMMIT_BRANCH" = "main" ]; then
        docker tag ${IMAGE_REGISTRY}/myapp:${IMAGE_TAG} ${IMAGE_REGISTRY}/myapp:latest
        docker push ${IMAGE_REGISTRY}/myapp:latest
      fi
  rules:
    - if: $CI_COMMIT_TAG
    - if: $CI_COMMIT_BRANCH == "main"

# ── 部署 ──
deploy:dev:
  stage: deploy
  image: bitnami/kubectl:latest
  needs: [docker:build]
  script:
    - kubectl set image deployment/myapp myapp=${IMAGE_REGISTRY}/myapp:${IMAGE_TAG} -n dev
    - kubectl rollout status deployment/myapp -n dev --timeout=180s
  environment:
    name: development
    url: https://dev.example.com
  rules:
    - if: $CI_COMMIT_BRANCH == "develop"

deploy:staging:
  stage: deploy
  image: bitnami/kubectl:latest
  needs: [docker:build]
  script:
    - kubectl set image deployment/myapp myapp=${IMAGE_REGISTRY}/myapp:${IMAGE_TAG} -n staging
    - kubectl rollout status deployment/myapp -n staging --timeout=300s
  environment:
    name: staging
    url: https://staging.example.com
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

deploy:prod:
  stage: deploy
  image: bitnami/kubectl:latest
  needs: [docker:build]
  script:
    - kubectl set image deployment/myapp myapp=${IMAGE_REGISTRY}/myapp:${IMAGE_TAG} -n production
    - kubectl rollout status deployment/myapp -n production --timeout=300s
  environment:
    name: production
    url: https://www.example.com
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
  when: manual

后端项目(Java Spring Boot)

plaintext133 行
# .gitlab-ci.yml - Java 后端完整流水线
variables:
  IMAGE_REGISTRY: harbor.example.com/backend
  IMAGE_TAG: ${CI_COMMIT_TAG:-${CI_COMMIT_SHORT_SHA}}
  MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"

stages:
  - build
  - test
  - security
  - docker
  - migrate
  - deploy

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

# ── 编译 ──
build:
  stage: build
  image: maven:3.9-eclipse-temurin-21
  script:
    - mvn clean compile -DskipTests
  artifacts:
    paths:
      - target/

# ── 测试 ──
test:unit:
  stage: test
  image: maven:3.9-eclipse-temurin-21
  needs: [build]
  script:
    - mvn test
  artifacts:
    reports:
      junit: target/surefire-reports/TEST-*.xml
    when: always

test:integration:
  stage: test
  image: maven:3.9-eclipse-temurin-21
  services:
    - postgres:15
    - redis:7
  variables:
    POSTGRES_DB: testdb
    POSTGRES_USER: test
    POSTGRES_PASSWORD: test
    SPRING_DATASOURCE_URL: "jdbc:postgresql://postgres:5432/testdb"
  needs: [build]
  script:
    - mvn verify -P integration-test
  artifacts:
    reports:
      junit: target/failsafe-reports/TEST-*.xml

# ── 安全扫描 ──
sast:
  stage: security
  image: returntocorp/semgrep
  script:
    - semgrep --config=p/java --json --output=sast.json .
  artifacts:
    reports:
      sast: sast.json
  allow_failure: true

dependency-scan:
  stage: security
  image: aquasec/trivy:latest
  script:
    - trivy fs --exit-code 1 --severity CRITICAL --scanners vuln .

# ── Docker 构建 ──
docker:build:
  stage: docker
  image: docker:24
  services:
    - docker:24-dind
  needs: [build, test:unit, test:integration]
  script:
    - mvn package -DskipTests
    - docker login -u $HARBOR_USER -p $HARBOR_PASS $IMAGE_REGISTRY
    - docker build -t ${IMAGE_REGISTRY}/myapi:${IMAGE_TAG} .
    - docker push ${IMAGE_REGISTRY}/myapi:${IMAGE_TAG}
  rules:
    - if: $CI_COMMIT_TAG
    - if: $CI_COMMIT_BRANCH == "main"

# ── 数据库迁移 ──
db:migrate:
  stage: migrate
  image: flyway/flyway:10
  needs: [docker:build]
  script:
    - flyway -url=jdbc:postgresql://${PROD_DB_HOST}:5432/${PROD_DB_NAME}
             -user=${PROD_DB_USER}
             -password=${PROD_DB_PASS}
             -locations=filesystem:./db/migration
             migrate
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
  when: manual

# ── 部署 ──
deploy:staging:
  stage: deploy
  image: bitnami/kubectl:latest
  needs: [docker:build]
  script:
    - kubectl set image deployment/myapi myapi=${IMAGE_REGISTRY}/myapi:${IMAGE_TAG} -n staging
    - kubectl rollout status deployment/myapi -n staging --timeout=300s
  environment:
    name: staging
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

deploy:prod:
  stage: deploy
  image: bitnami/kubectl:latest
  needs: [docker:build, db:migrate]
  script:
    - kubectl set image deployment/myapi myapi=${IMAGE_REGISTRY}/myapi:${IMAGE_TAG} -n production
    - kubectl rollout status deployment/myapi -n production --timeout=300s
  environment:
    name: production
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
  when: manual

Docker 多阶段构建

plaintext32 行
# Dockerfile - 前端多阶段构建
FROM node:24-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --prefer-offline
COPY . .
RUN npm run build

FROM nginx:1.25-alpine AS production
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD wget -qO- http://localhost/health || exit 1

# Dockerfile - Java 后端多阶段构建
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests

FROM eclipse-temurin:21-jre-alpine AS production
WORKDIR /app
RUN addgroup -S app && adduser -S app -G app
COPY --from=builder /app/target/*.jar app.jar
USER app
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD wget -qO- http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]

总结

CI/CD 流水线按验证、制品和发布阶段逐步建立:

plaintext5 行
阶段 1: 自动化构建 + 单元测试(CI 基础)
阶段 2: 多环境自动部署 + 集成测试
阶段 3: 安全扫描 + 制品管理
阶段 4: 蓝绿/金丝雀发布 + 自动回滚
阶段 5: 全链路监控 + 完善审计

关键心法:

  • 代码不变,配置外置 — 所有环境差异通过配置管理
  • 制品不可变 — 同一个镜像从测试跑到生产
  • 向前兼容 — 数据库变更永远向前兼容
  • 小步快跑 — 频繁小发布优于偶尔大发布
  • 快速回滚 — 回滚能力比发布能力更重要

发布的成功条件

流水线成功应同时包含测试结果、制品生成、部署状态和代表性业务请求。脚本使用管道或子进程时,要检查实际失败是否传播到最终退出码,避免 grep、日志上传或清理命令掩盖失败。

数据库迁移采用与新旧应用兼容的阶段划分。应用镜像可回滚时,已执行的破坏性数据变更仍可能无法回滚。发布前为每一步写出失败后的恢复动作和必须保留的证据。

← 返回文章