CI/CD 核心概念
CI、持续交付与持续部署是三个递进的阶段,分别对应提交合并、随时可发布与自动发布。
持续集成(Continuous Integration)
开发人员频繁(每天至少一次)将代码合并到主干,每次合并都触发自动化构建和测试。
核心价值: 尽早发现问题。代码冲突、编译错误、单元测试失败尽早暴露,并把失败反馈给提交者。
典型流程:
代码提交 → 触发 CI → 编译 → 单元测试 → 静态分析 → 构建产物 → 通知持续交付(Continuous Delivery)
在 CI 基础上,确保代码随时可以部署到生产环境。部署到生产需要手动审批。
核心价值: 部署能力回归业务决策。技术上随时可发,业务上选择合适时机。
持续部署(Continuous Deployment)
在持续交付基础上更进一步:代码通过所有测试后自动部署到生产,零人工干预。
核心价值: 最快的交付速度,适合成熟团队和高自动化水平的系统。
三者关系
持续集成 ⊂ 持续交付 ⊂ 持续部署
CI: 代码 → 构建 → 测试 → 制品
CD*: 制品 → 预发布环境 → [手动审批] → 生产环境(持续交付)
CD**: 制品 → 预发布环境 → 自动 → 生产环境(持续部署)选型建议:
- 初创团队 / 新项目:先做好 CI
- 中型团队:持续交付(推荐大多数场景)
- 成熟微服务 + 完善监控:持续部署
流水线设计原则
快速反馈
流水线每一秒都在消耗团队的时间。反馈越快,修复成本越低。
实践要点:
- 单元测试 < 5 分钟,集成测试 < 15 分钟
- 失败时立即通知,不要等整条流水线跑完
- 分层测试:快速检查先行,慢速检查后置
GitLab CI 示例 — 阶段化流水线:
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并行化
独立任务并行执行,缩短总耗时。
# 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幂等性
流水线多次执行结果一致,不会产生副作用。
# 幂等部署脚本示例
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可回滚
任何变更都能快速回退到上一个已知正常版本。
# 保留最近 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多环境部署
环境分层
典型的三环境模型:
dev (开发) → staging (预发布) → production (生产)
↓ ↓ ↓
自动部署 自动部署 手动审批后部署
功能验证 集成验证 线上验证
随时重置 接近生产 高可用保障环境差异管理
原则:代码不变,配置外置。
GitLab CI 多环境模板:
.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
# 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 动态密钥
# 使用 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 核心语法
# 定义流水线阶段(按顺序执行)
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 为例):
# 添加官方仓库
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:
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 在独立容器中运行,天然隔离。
# /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 镜像时使用。
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_SHAShell Executor 配置
Shell Executor 直接在宿主机上执行命令,性能最好但隔离性最差。
[[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 实例。
[[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
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 之间的制品传递,避免不必要的下载。
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 配置
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 配置
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 提供了大量内置变量,常用的包括:
# 在 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。
# 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_TYPEProtected 与 Masked 变量
在 Settings → CI/CD → Variables 中配置:
- Protected:仅在受保护分支/标签的流水线中可见
- Masked:在 Job 日志中自动隐藏(显示
[MASKED]),值必须是 Base64 编码或满足正则规则
# 使用 Protected + Masked 变量(在 Settings 中配置,不在 YAML 中暴露)
deploy-production:
stage: deploy
script:
- echo "$PROD_DB_PASSWORD" | docker secret create db_password - # 日志中显示 [MASKED]
only:
- main # 仅 main 分支触发
environment:
name: productionSecrets 管理(Vault 集成)
对于更高安全要求,可集成 HashiCorp Vault:
# 需要在项目 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"常用场景
构建→测试→部署(完整流水线)
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 # 手动触发多环境部署
.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 # 不允许跳过并行任务
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矩阵构建
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 详细配置
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过期策略
# 快速过期的中间产物
test-results:
artifacts:
expire_in: 3 days
# 永久保留的发布制品
release-package:
artifacts:
expire_in: never # 永不过期
only:
- tags # 仅标签触发依赖传递控制
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 部署
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.comDocker 部署(带 Registry)
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
EOFKubernetes 部署
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 部署:
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 5mAuto DevOps
GitLab 内置的 Auto DevOps 可以零配置完成从构建到部署的主要流程:
# 项目根目录创建 .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缓存优化
全局缓存
# 全局缓存配置,所有 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 testPer-Job 缓存
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 ./...缓存键策略
# 策略 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(静态应用安全测试)
include:
- template: Security/SAST.gitlab-ci.yml
sast:
variables:
SAST_EXCLUDED_ANALYZERS: "spotbugs" # 排除特定分析器
SAST_EXCLUDED_PATHS: "test,spec,docs" # 排除目录DAST(动态应用安全测试)
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 中配置依赖扫描
include:
- template: Security/Dependency-Scanning.gitlab-ci.yml
dependency_scanning:
variables:
DS_EXCLUDED_PATHS: "test,spec,docs"容器扫描
include:
- template: Security/Container-Scanning.gitlab-ci.yml
container_scanning:
variables:
CS_IMAGE: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
CS_SEVERITY_THRESHOLD: "high" # 只报告 high 及以上完整安全扫描流水线示例:
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 锚点复用
# 定义可复用的 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(更推荐)
.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 策略
# 使用 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"调试技巧
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其他调试方法:
# 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 日志中变量值泄露
# 错误:直接打印密码
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 中文件权限问题
# [不适用] 以 root 运行的容器中创建的文件,宿主机 runner 用户无法访问
build:
script:
- touch output.txt
# [可用] 使用 --user 指定用户,或在 script 中修改权限
build:
script:
- touch output.txt
- chmod 644 output.txt坑 3:缓存未生效
# [不适用] 不同分支使用相同缓存 key 但 Lock 文件不同
cache:
key: "deps"
# [可用] 基于 Lock 文件生成 key
cache:
key:
files:
- package-lock.json坑 4:rules 与 only/except 混用
# [不适用] 不要混用 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 超大导致上传失败
# [不适用] 打包整个 node_modules(可能上 GB)
artifacts:
paths:
- node_modules/
# [可用] 只打包构建产物,依赖用 cache 管理
artifacts:
paths:
- dist/
expire_in: 7 days附录:完整项目配置示例
以下是一个完整的 .gitlab-ci.yml 模板,涵盖了本文讨论的大部分功能:
# ============================================
# .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 文件长这样:
# .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 何时被触发,支持多种事件类型:
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 与依赖关系
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 的上下文变量和函数:
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 可以有以下属性:
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 让你用一份配置跑多组参数,非常适合多版本、多平台测试:
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
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 都需要的第一步:
- 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
- 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 scopeactions/setup-java
- 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
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
# cache: 'pipenv'
# cache: 'poetry'docker/build-push-action
构建并推送 Docker 镜像:
- 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
# 缓存任意目录
- 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 是必要选择。
安装与配置
# 创建目录
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:
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 实现权限隔离:
jobs:
deploy:
runs-on: [self-hosted, linux, production]
# 在仓库/组织设置中配置 Runner Group 的访问控制安全考虑
自托管 Runner 的安全至关重要:
# 对于公开仓库,强烈建议仅使用临时 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 自动伸缩:
# 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 分三个层级,优先级从高到低:
- Environment Secrets — 绑定到特定部署环境(如 production),可以配合审批流程
- Repository Secrets — 仓库级别,所有 Workflow 可访问
- Organization Secrets — 组织级别,可共享给多个仓库
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 中配置:
# 使用 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.shOIDC 联合身份认证(推荐)
OIDC 无需存储长期凭据,是目前最安全的方式:
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.comOIDC 配置步骤:
- 在云平台创建身份提供商(Identity Provider),指向 GitHub 的 OIDC 端点
- 创建角色并配置信任策略,仅允许特定仓库/分支的 Workflow 假设角色
- 在 Workflow 中使用对应的 Action 获取临时凭据
变量(Variables)vs Secrets
非敏感配置使用 Variables,敏感字段使用 Secrets:
# 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 完整配置
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: 3Java CI 完整配置(Gradle)
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/*.jarDocker 构建与推送
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 包
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/*多环境部署
环境定义与审批流程
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:环境级别的配置和凭据
分支策略与自动部署
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 将多个步骤打包成一个可复用的单元,存放在仓库中:
# .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:
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-project
with:
node-version: '22'
- run: npm test可重用 Workflow(Reusable Workflow)
可重用 Workflow 通过 workflow_call 触发,适合跨仓库或跨项目的标准化流程:
# .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:
# .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 }}跨仓库调用:
jobs:
ci:
uses: your-org/shared-workflows/.github/workflows/node-ci.yml@main
with:
node-version: '24'
secrets: inherit # 继承调用方的所有 secrets缓存优化
actions/cache 基础
- 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:
# 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@v2Docker 构建缓存
# 方式一:使用 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 分支读取
最佳实践
安全
# 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性能优化
# 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']调试技巧
# 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
成本优化策略:
# 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 减少重复配置维护性
# 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 时,不能在拥有高权限的上下文执行其未审阅代码。制品按提交与摘要关联,下载后核对来源;缓存用于加速构建,不提供制品可信性保证。
发布模式与回滚
原理
同时维护两套完全相同的生产环境(蓝和绿),任何时候只有一套对外服务。发布新版本时部署到闲置环境,验证通过后切换流量,出问题立即切回。
┌─────────────┐
用户请求 ───→ │ 负载均衡器 │
└──────┬──────┘
│
┌──────────┴──────────┐
▼ ▼
┌──────────┐ ┌──────────┐
│ 蓝(当前) │ │ 绿(新) │
│ v1.2.3 │ │ v1.3.0 │
└──────────┘ └──────────┘
发布前:流量 → 蓝
发布后:流量 → 绿(蓝保留作回滚)Nginx 实现
# /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;
}
}自动化切换脚本:
#!/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 实现
# 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: 3Kubernetes 实现
# 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切换命令:
# 切换到绿环境
kubectl patch service myapp -p '{"spec":{"selector":{"version":"green"}}}'
# 回滚到蓝环境
kubectl patch service myapp -p '{"spec":{"selector":{"version":"blue"}}}'金丝雀发布
原理
将新版本逐步推送给一小部分用户,观察指标无异常后再全量发布。像矿井中的金丝雀一样,先让“探路者”验证安全性。
流量分配:
┌────────────────────────────────────────┐
│ ████████████████████████████░░░░ 90% │ ← 旧版本 (stable)
│ ████████░░░░░░░░░░░░░░░░░░░░ 10% │ ← 新版本 (canary)
└────────────────────────────────────────┘
时间线:
0% → 10% → 25% → 50% → 100%
│ │ │ │ │
观察 观察 观察 观察 全量发布
15min 15min 15min 15minNginx 流量控制
# 基于权重的金丝雀
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 金丝雀
# 稳定版 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 告警规则(金丝雀指标监控):
# 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 秒"自动回滚脚本:
#!/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}"
fiFlagger 自动化金丝雀
# 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。
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 | 先删后建,不超资源配额 |
健康检查三件套
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 次失败则重启回滚机制
# 查看发布历史
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/myappCI/CD 集成回滚:
# 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;
}回滚策略
版本管理
语义化版本 + 制品不可变:
版本号: MAJOR.MINOR.PATCH (如 1.3.2)
MAJOR: 不兼容的 API 变更
MINOR: 向后兼容的功能新增
PATCH: 向后兼容的 Bug 修复
制品标签: v1.3.2 / v1.3.2-abc1234 (带 commit hash)Docker 镜像版本策略:
# [不适用] 避免使用 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}数据库回滚
数据库变更是回滚中最棘手的部分。核心原则:向前兼容,分步执行。
安全的数据库变更模式:
第 1 步:新增列(Additive only)→ 部署新代码
第 2 步:新代码同时读写新旧列 → 验证
第 3 步:新代码只读写新列 → 停止写旧列
第 4 步:删除旧列(在下个版本)回滚脚本模板:
-- 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
#!/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 配置示例
# 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 使用:
# 登录
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 配置
# 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 项目与镜像策略:
# 创建项目
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": "**"}]
}]
}'制品版本策略与垃圾回收
# 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 垃圾回收:
# 手动触发垃圾回收
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 脚本,简单直接,学习成本低。
目录结构:
db/migration/
V1__create_users_table.sql
V2__add_email_column.sql
V3__create_orders_table.sql
V4__add_index_on_email.sqlSQL 示例:
-- 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:
# 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: manualLiquibase
Liquibase 使用 XML/YAML/JSON 描述变更,支持自动回滚。
changelog.yaml:
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:
- 只追加,不修改 — 不要 ALTER 已有列的数据类型
- 向后兼容 — 新代码必须能跑在旧 schema 上
- 小步快走 — 每次迁移只做一个变更
- 先扩后缩 — 新增列 → 迁移数据 → 去掉旧列(跨版本完成)
CI 集成模式:
代码合并 → CI 构建镜像 → 先跑 DB Migration → 再部署应用 → 健康检查
↑ 数据库变更先于应用部署安全与合规
SAST / DAST / SCA 集成
SAST(静态应用安全测试)— 代码层面:
# 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(软件成分分析)— 依赖层面:
# 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(动态应用安全测试)— 运行层面:
# 集成 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"审批流程
# 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审计日志
# 部署审计记录
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)
# .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)
# .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: manualDocker 多阶段构建
# 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 流水线按验证、制品和发布阶段逐步建立:
阶段 1: 自动化构建 + 单元测试(CI 基础)
阶段 2: 多环境自动部署 + 集成测试
阶段 3: 安全扫描 + 制品管理
阶段 4: 蓝绿/金丝雀发布 + 自动回滚
阶段 5: 全链路监控 + 完善审计关键心法:
- 代码不变,配置外置 — 所有环境差异通过配置管理
- 制品不可变 — 同一个镜像从测试跑到生产
- 向前兼容 — 数据库变更永远向前兼容
- 小步快跑 — 频繁小发布优于偶尔大发布
- 快速回滚 — 回滚能力比发布能力更重要
发布的成功条件
流水线成功应同时包含测试结果、制品生成、部署状态和代表性业务请求。脚本使用管道或子进程时,要检查实际失败是否传播到最终退出码,避免 grep、日志上传或清理命令掩盖失败。
数据库迁移采用与新旧应用兼容的阶段划分。应用镜像可回滚时,已执行的破坏性数据变更仍可能无法回滚。发布前为每一步写出失败后的恢复动作和必须保留的证据。
← 返回文章