最后更新于:2026年7月

CI/CD 是现代软件开发的标配。 根据 2025 年 DORA 报告,实施成熟 CI/CD 的团队,部署频率提高 200 倍,变更失败率降低 7 倍,问题修复速度快 65 倍。
但很多人对 CI/CD 的理解还停留在「有个 Jenkins 服务器」。什么是持续集成?什么是持续部署?流水线怎么设计?GitHub Actions 怎么配置?蓝绿部署和金丝雀发布是什么?
本文从核心概念到生产级配置,带你全面掌握 CI/CD。
一、CI/CD 核心概念
1.1 什么是 CI/CD?
PLAINTEXT
CI(Continuous Integration)持续集成:
- 开发者频繁将代码合并到主干
- 每次合并自动触发构建和测试
- 快速发现问题,快速修复
CD(Continuous Delivery)持续交付:
- 代码随时可以部署到生产环境
- 自动化测试通过后自动部署到预发布环境
- 生产部署需要手动触发
CD(Continuous Deployment)持续部署:
- 代码自动部署到生产环境
- 全流程自动化,无需人工干预
- 要求极高的测试覆盖率和自动化水平CI(Continuous Integration)持续集成:
- 开发者频繁将代码合并到主干
- 每次合并自动触发构建和测试
- 快速发现问题,快速修复
CD(Continuous Delivery)持续交付:
- 代码随时可以部署到生产环境
- 自动化测试通过后自动部署到预发布环境
- 生产部署需要手动触发
CD(Continuous Deployment)持续部署:
- 代码自动部署到生产环境
- 全流程自动化,无需人工干预
- 要求极高的测试覆盖率和自动化水平
1.2 CI/CD 流水线
PLAINTEXT
完整 CI/CD 流水线:
代码提交 → 构建 → 测试 → 安全扫描 → 部署 → 监控
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
Git Push Compile Unit Test SAST Deploy Alerts
PR Merge Lint Integration DAST Release Metrics
E2E Test SCA Rollback Logs完整 CI/CD 流水线:
代码提交 → 构建 → 测试 → 安全扫描 → 部署 → 监控
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
Git Push Compile Unit Test SAST Deploy Alerts
PR Merge Lint Integration DAST Release Metrics
E2E Test SCA Rollback Logs
1.3 CI/CD 平台对比
| 平台 | 托管方式 | 配置语言 | 特点 | 推荐度 |
|---|---|---|---|---|
| GitHub Actions | 云托管 | YAML | GitHub 深度集成,生态丰富 | ⭐⭐⭐⭐⭐ |
| GitLab CI | 云/自托管 | YAML | 功能全面,DevOps 一体化 | ⭐⭐⭐⭐⭐ |
| Jenkins | 自托管 | Groovy | 灵活强大,插件丰富 | ⭐⭐⭐⭐ |
| CircleCI | 云托管 | YAML | 简单易用,速度快 | ⭐⭐⭐⭐ |
| Bitbucket Pipelines | 云托管 | YAML | Bitbucket 集成 | ⭐⭐⭐ |
| Drone | 自托管 | YAML | 轻量,容器化 | ⭐⭐⭐⭐ |
| Tekton | 云原生 | YAML | Kubernetes 原生 | ⭐⭐⭐ |
二、GitHub Actions 完全指南
2.1 基础概念
YAML
# GitHub Actions 核心概念
Workflow(工作流):
- 一个自动化流程,定义在 .github/workflows/*.yml
- 由一个或多个 Job 组成
Job(作业):
- 工作流中的一组步骤,在同一个运行环境中执行
- 可以并行执行,也可以串行执行
Step(步骤):
- Job 中的具体任务
- 执行 Shell 命令或调用 Action
Action(动作):
- 可复用的单元
- 可以是官方提供,也可以是第三方或自己编写
Runner(运行器):
- 执行 Job 的服务器
- GitHub 提供 ubuntu-latest, windows-latest, macos-latest
- 也可以使用自托管 Runner# GitHub Actions 核心概念
Workflow(工作流):
- 一个自动化流程,定义在 .github/workflows/*.yml
- 由一个或多个 Job 组成
Job(作业):
- 工作流中的一组步骤,在同一个运行环境中执行
- 可以并行执行,也可以串行执行
Step(步骤):
- Job 中的具体任务
- 执行 Shell 命令或调用 Action
Action(动作):
- 可复用的单元
- 可以是官方提供,也可以是第三方或自己编写
Runner(运行器):
- 执行 Job 的服务器
- GitHub 提供 ubuntu-latest, windows-latest, macos-latest
- 也可以使用自托管 Runner
2.2 基础工作流
YAML
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- name: Install pnpm
run: npm install -g pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm lint
- name: Type check
run: pnpm type-check
- name: Test
run: pnpm test -- --coverage
- name: Build
run: pnpm build
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- name: Install pnpm
run: npm install -g pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm lint
- name: Type check
run: pnpm type-check
- name: Test
run: pnpm test -- --coverage
- name: Build
run: pnpm build
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
2.3 条件与矩阵
YAML
name: CI
on:
push:
branches: [main]
pull_request:
workflow_dispatch: # 手动触发
jobs:
test:
runs-on: ubuntu-latest
# 条件执行
if: github.event_name == 'push' || github.event.pull_request.draft == false
strategy:
# 矩阵构建
matrix:
node: [18, 20, 22]
os: [ubuntu-latest, macos-latest]
exclude:
- node: 18
os: macos-latest
fail-fast: false # 一个失败不影响其他
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci
- run: npm test
# 服务容器
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379
deploy:
needs: test # 依赖 test job
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Deploy
run: echo "Deploying..."name: CI
on:
push:
branches: [main]
pull_request:
workflow_dispatch: # 手动触发
jobs:
test:
runs-on: ubuntu-latest
# 条件执行
if: github.event_name == 'push' || github.event.pull_request.draft == false
strategy:
# 矩阵构建
matrix:
node: [18, 20, 22]
os: [ubuntu-latest, macos-latest]
exclude:
- node: 18
os: macos-latest
fail-fast: false # 一个失败不影响其他
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci
- run: npm test
# 服务容器
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379
deploy:
needs: test # 依赖 test job
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Deploy
run: echo "Deploying..."
2.4 Secrets 与环境变量
YAML
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
# 使用环境(可以有环境级 Secrets)
environment: production
steps:
- uses: actions/checkout@v4
- name: Deploy to server
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /app
docker-compose pull
docker-compose up -d
# 环境变量
- name: Build
run: npm run build
env:
NODE_ENV: production
DATABASE_URL: ${{ secrets.DATABASE_URL }}
API_KEY: ${{ secrets.API_KEY }}name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
# 使用环境(可以有环境级 Secrets)
environment: production
steps:
- uses: actions/checkout@v4
- name: Deploy to server
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /app
docker-compose pull
docker-compose up -d
# 环境变量
- name: Build
run: npm run build
env:
NODE_ENV: production
DATABASE_URL: ${{ secrets.DATABASE_URL }}
API_KEY: ${{ secrets.API_KEY }}
2.5 Docker 镜像构建与推送
YAML
name: Docker Build
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ github.repository }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Build and push
uses: docker/build-push-action@v5
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/arm64name: Docker Build
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ github.repository }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Build and push
uses: docker/build-push-action@v5
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
2.6 发布到 Kubernetes
YAML
name: Deploy to K8s
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up kubectl
uses: azure/setup-kubectl@v3
- name: Set up kubeconfig
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > ~/.kube/config
- name: Deploy
run: |
# 替换镜像版本
sed -i "s|image: myapp:.*|image: myapp:${{ github.sha }}|" k8s/deployment.yaml
# 应用配置
kubectl apply -f k8s/
# 等待滚动更新完成
kubectl rollout status deployment/myapp -n production --timeout=300s
- name: Rollback on failure
if: failure()
run: |
kubectl rollout undo deployment/myapp -n productionname: Deploy to K8s
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up kubectl
uses: azure/setup-kubectl@v3
- name: Set up kubeconfig
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > ~/.kube/config
- name: Deploy
run: |
# 替换镜像版本
sed -i "s|image: myapp:.*|image: myapp:${{ github.sha }}|" k8s/deployment.yaml
# 应用配置
kubectl apply -f k8s/
# 等待滚动更新完成
kubectl rollout status deployment/myapp -n production --timeout=300s
- name: Rollback on failure
if: failure()
run: |
kubectl rollout undo deployment/myapp -n production
三、GitLab CI/CD 完全指南
3.1 基础配置
YAML
# .gitlab-ci.yml
stages:
- build
- test
- security
- deploy
variables:
NODE_VERSION: "20"
DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
# 定义缓存
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
- .pnpm-store/
# 定义作业
build:
stage: build
image: node:${NODE_VERSION}-alpine
before_script:
- npm install -g pnpm
- pnpm install --frozen-lockfile
script:
- pnpm build
artifacts:
paths:
- dist/
expire_in: 1 hour
test:
stage: test
image: node:${NODE_VERSION}-alpine
before_script:
- npm install -g pnpm
- pnpm install --frozen-lockfile
script:
- pnpm lint
- pnpm test -- --coverage
coverage: '/Lines\s*:\s*(\d+\.\d+)%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
security_scan:
stage: security
image: aquasec/trivy:latest
script:
- trivy fs --exit-code 1 --severity HIGH,CRITICAL .
deploy_staging:
stage: deploy
image: alpine:latest
environment:
name: staging
url: https://staging.example.com
before_script:
- apk add --no-cache openssh-client
script:
- ssh $SSH_USER@$STAGING_SERVER "cd /app && docker-compose pull && docker-compose up -d"
only:
- develop
deploy_production:
stage: deploy
image: alpine:latest
environment:
name: production
url: https://example.com
before_script:
- apk add --no-cache openssh-client
script:
- ssh $SSH_USER@$PRODUCTION_SERVER "cd /app && docker-compose pull && docker-compose up -d"
only:
- main
when: manual # 手动触发# .gitlab-ci.yml
stages:
- build
- test
- security
- deploy
variables:
NODE_VERSION: "20"
DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
# 定义缓存
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
- .pnpm-store/
# 定义作业
build:
stage: build
image: node:${NODE_VERSION}-alpine
before_script:
- npm install -g pnpm
- pnpm install --frozen-lockfile
script:
- pnpm build
artifacts:
paths:
- dist/
expire_in: 1 hour
test:
stage: test
image: node:${NODE_VERSION}-alpine
before_script:
- npm install -g pnpm
- pnpm install --frozen-lockfile
script:
- pnpm lint
- pnpm test -- --coverage
coverage: '/Lines\s*:\s*(\d+\.\d+)%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
security_scan:
stage: security
image: aquasec/trivy:latest
script:
- trivy fs --exit-code 1 --severity HIGH,CRITICAL .
deploy_staging:
stage: deploy
image: alpine:latest
environment:
name: staging
url: https://staging.example.com
before_script:
- apk add --no-cache openssh-client
script:
- ssh $SSH_USER@$STAGING_SERVER "cd /app && docker-compose pull && docker-compose up -d"
only:
- develop
deploy_production:
stage: deploy
image: alpine:latest
environment:
name: production
url: https://example.com
before_script:
- apk add --no-cache openssh-client
script:
- ssh $SSH_USER@$PRODUCTION_SERVER "cd /app && docker-compose pull && docker-compose up -d"
only:
- main
when: manual # 手动触发
3.2 Docker 镜像构建
YAML
docker_build:
stage: build
image: docker:latest
services:
- docker:dind
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $DOCKER_IMAGE .
- docker push $DOCKER_IMAGE
only:
- main
- tagsdocker_build:
stage: build
image: docker:latest
services:
- docker:dind
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $DOCKER_IMAGE .
- docker push $DOCKER_IMAGE
only:
- main
- tags
3.3 自动部署到 Kubernetes
YAML
deploy_k8s:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl config set-cluster k8s --server="$KUBE_URL" --insecure-skip-tls-verify=true
- kubectl config set-credentials admin --token="$KUBE_TOKEN"
- kubectl config set-context default --cluster=k8s --user=admin
- kubectl config use-context default
# 替换变量
- envsubst < k8s/deployment.yaml | kubectl apply -f -
# 等待部署完成
- kubectl rollout status deployment/myapp -n production --timeout=300s
environment:
name: production
only:
- maindeploy_k8s:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl config set-cluster k8s --server="$KUBE_URL" --insecure-skip-tls-verify=true
- kubectl config set-credentials admin --token="$KUBE_TOKEN"
- kubectl config set-context default --cluster=k8s --user=admin
- kubectl config use-context default
# 替换变量
- envsubst < k8s/deployment.yaml | kubectl apply -f -
# 等待部署完成
- kubectl rollout status deployment/myapp -n production --timeout=300s
environment:
name: production
only:
- main
四、Jenkins Pipeline
4.1 Declarative Pipeline
GROOVY
// Jenkinsfile
pipeline {
agent any
tools {
nodejs 'NodeJS-20'
}
environment {
DOCKER_IMAGE = 'myapp'
DOCKER_TAG = "${env.BUILD_NUMBER}"
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Install') {
steps {
sh 'npm ci'
}
}
stage('Lint') {
steps {
sh 'npm run lint'
}
}
stage('Test') {
steps {
sh 'npm test -- --coverage'
}
post {
always {
junit 'coverage/junit.xml'
publishCoverage adapter: 'istanbul'
}
}
}
stage('Build') {
steps {
sh 'npm run build'
}
}
stage('Docker Build') {
steps {
script {
docker.withRegistry('https://registry.example.com', 'docker-credentials') {
def app = docker.build("${DOCKER_IMAGE}:${DOCKER_TAG}")
app.push()
}
}
}
}
stage('Deploy') {
when {
branch 'main'
}
steps {
sh """
kubectl set image deployment/myapp \\
myapp=${DOCKER_IMAGE}:${DOCKER_TAG} \\
-n production
"""
}
}
}
post {
success {
slackSend(
channel: '#deployments',
color: 'good',
message: "部署成功: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
)
}
failure {
slackSend(
channel: '#deployments',
color: 'danger',
message: "部署失败: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
)
}
}
}// Jenkinsfile
pipeline {
agent any
tools {
nodejs 'NodeJS-20'
}
environment {
DOCKER_IMAGE = 'myapp'
DOCKER_TAG = "${env.BUILD_NUMBER}"
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Install') {
steps {
sh 'npm ci'
}
}
stage('Lint') {
steps {
sh 'npm run lint'
}
}
stage('Test') {
steps {
sh 'npm test -- --coverage'
}
post {
always {
junit 'coverage/junit.xml'
publishCoverage adapter: 'istanbul'
}
}
}
stage('Build') {
steps {
sh 'npm run build'
}
}
stage('Docker Build') {
steps {
script {
docker.withRegistry('https://registry.example.com', 'docker-credentials') {
def app = docker.build("${DOCKER_IMAGE}:${DOCKER_TAG}")
app.push()
}
}
}
}
stage('Deploy') {
when {
branch 'main'
}
steps {
sh """
kubectl set image deployment/myapp \\
myapp=${DOCKER_IMAGE}:${DOCKER_TAG} \\
-n production
"""
}
}
}
post {
success {
slackSend(
channel: '#deployments',
color: 'good',
message: "部署成功: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
)
}
failure {
slackSend(
channel: '#deployments',
color: 'danger',
message: "部署失败: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
)
}
}
}
4.2 Scripted Pipeline
GROOVY
// Jenkinsfile (Scripted Pipeline)
node {
try {
stage('Checkout') {
checkout scm
}
stage('Build') {
sh 'npm ci'
sh 'npm run build'
}
stage('Test') {
sh 'npm test'
}
stage('Deploy') {
if (env.BRANCH_NAME == 'main') {
sh './deploy.sh production'
} else if (env.BRANCH_NAME == 'develop') {
sh './deploy.sh staging'
}
}
} catch (e) {
currentBuild.result = 'FAILED'
throw e
} finally {
// 清理
deleteDir()
// 通知
if (currentBuild.result == 'FAILED') {
emailext(
subject: "构建失败: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: "请检查构建日志: ${env.BUILD_URL}",
to: 'team@example.com'
)
}
}
}// Jenkinsfile (Scripted Pipeline)
node {
try {
stage('Checkout') {
checkout scm
}
stage('Build') {
sh 'npm ci'
sh 'npm run build'
}
stage('Test') {
sh 'npm test'
}
stage('Deploy') {
if (env.BRANCH_NAME == 'main') {
sh './deploy.sh production'
} else if (env.BRANCH_NAME == 'develop') {
sh './deploy.sh staging'
}
}
} catch (e) {
currentBuild.result = 'FAILED'
throw e
} finally {
// 清理
deleteDir()
// 通知
if (currentBuild.result == 'FAILED') {
emailext(
subject: "构建失败: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: "请检查构建日志: ${env.BUILD_URL}",
to: 'team@example.com'
)
}
}
}
五、部署策略
5.1 滚动更新(Rolling Update)
YAML
# Kubernetes 滚动更新配置
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 4
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 最多可以多 1 个 Pod
maxUnavailable: 1 # 最多可以有 1 个 Pod 不可用
template:
spec:
containers:
- name: myapp
image: myapp:v2
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10# Kubernetes 滚动更新配置
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 4
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 最多可以多 1 个 Pod
maxUnavailable: 1 # 最多可以有 1 个 Pod 不可用
template:
spec:
containers:
- name: myapp
image: myapp:v2
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
BASH
# 手动触发滚动更新
kubectl set image deployment/myapp myapp=myapp:v2 -n production
# 查看滚动更新状态
kubectl rollout status deployment/myapp -n production
# 回滚
kubectl rollout undo deployment/myapp -n production
# 查看历史版本
kubectl rollout history deployment/myapp -n production# 手动触发滚动更新
kubectl set image deployment/myapp myapp=myapp:v2 -n production
# 查看滚动更新状态
kubectl rollout status deployment/myapp -n production
# 回滚
kubectl rollout undo deployment/myapp -n production
# 查看历史版本
kubectl rollout history deployment/myapp -n production
5.2 蓝绿部署(Blue-Green Deployment)
PLAINTEXT
蓝绿部署架构:
当前版本(蓝) 新版本(绿)
│ │
┌───▼───┐ ┌───▼───┐
│ Pod 1 │ │ Pod 1 │
│ Pod 2 │ │ Pod 2 │
│ Pod 3 │ │ Pod 3 │
└───┬───┘ └───┬───┘
│ │
└───────┬───────────────┘
│
┌─────▼─────┐
│ Service │ ← 切换 Service 选择器
│ │ 从 blue 切换到 green
└───────────┘
│
┌─────▼─────┐
│ 用户 │
└───────────┘蓝绿部署架构:
当前版本(蓝) 新版本(绿)
│ │
┌───▼───┐ ┌───▼───┐
│ Pod 1 │ │ Pod 1 │
│ Pod 2 │ │ Pod 2 │
│ Pod 3 │ │ Pod 3 │
└───┬───┘ └───┬───┘
│ │
└───────┬───────────────┘
│
┌─────▼─────┐
│ Service │ ← 切换 Service 选择器
│ │ 从 blue 切换到 green
└───────────┘
│
┌─────▼─────┐
│ 用户 │
└───────────┘
YAML
# Kubernetes 蓝绿部署示例
# 蓝色版本(当前)
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-blue
spec:
replicas: 3
selector:
matchLabels:
app: myapp
version: blue
template:
metadata:
labels:
app: myapp
version: blue
spec:
containers:
- name: myapp
image: myapp:v1
---
# 绿色版本(新)
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-green
spec:
replicas: 3
selector:
matchLabels:
app: myapp
version: green
template:
metadata:
labels:
app: myapp
version: green
spec:
containers:
- name: myapp
image: myapp:v2
---
# Service(指向蓝色)
apiVersion: v1
kind: Service
metadata:
name: myapp
spec:
selector:
app: myapp
version: blue # 切换到 green 完成蓝绿部署
ports:
- port: 80
targetPort: 3000# Kubernetes 蓝绿部署示例
# 蓝色版本(当前)
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-blue
spec:
replicas: 3
selector:
matchLabels:
app: myapp
version: blue
template:
metadata:
labels:
app: myapp
version: blue
spec:
containers:
- name: myapp
image: myapp:v1
---
# 绿色版本(新)
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-green
spec:
replicas: 3
selector:
matchLabels:
app: myapp
version: green
template:
metadata:
labels:
app: myapp
version: green
spec:
containers:
- name: myapp
image: myapp:v2
---
# Service(指向蓝色)
apiVersion: v1
kind: Service
metadata:
name: myapp
spec:
selector:
app: myapp
version: blue # 切换到 green 完成蓝绿部署
ports:
- port: 80
targetPort: 3000
BASH
# 切换到绿色版本
kubectl patch service myapp -p '{"spec":{"selector":{"version":"green"}}}'
# 验证
kubectl get endpoints myapp# 切换到绿色版本
kubectl patch service myapp -p '{"spec":{"selector":{"version":"green"}}}'
# 验证
kubectl get endpoints myapp
5.3 金丝雀发布(Canary Deployment)
PLAINTEXT
金丝雀发布架构:
用户流量
│
▼
┌───────┐
│Router/│
│ Ingress│
└───┬───┘
│
┌─────┴─────┐
│ │
90%│ │10%
▼ ▼
┌──────────┐ ┌──────────┐
│ 稳定版本 │ │ 金丝雀 │
│ (v1) │ │ (v2) │
│ Pod x 9 │ │ Pod x 1 │
└──────────┘ └──────────┘
│ │
└─────┬─────┘
▼
监控指标
- 错误率
- 响应时间
- 业务指标金丝雀发布架构:
用户流量
│
▼
┌───────┐
│Router/│
│ Ingress│
└───┬───┘
│
┌─────┴─────┐
│ │
90%│ │10%
▼ ▼
┌──────────┐ ┌──────────┐
│ 稳定版本 │ │ 金丝雀 │
│ (v1) │ │ (v2) │
│ Pod x 9 │ │ Pod x 1 │
└──────────┘ └──────────┘
│ │
└─────┬─────┘
▼
监控指标
- 错误率
- 响应时间
- 业务指标
YAML
# Kubernetes 金丝雀发布
# 使用 Istio 或 Flagger 实现自动化
# Istio VirtualService 示例
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: myapp
spec:
hosts:
- myapp.example.com
http:
- route:
- destination:
host: myapp
subset: stable
weight: 90
- destination:
host: myapp
subset: canary
weight: 10# Kubernetes 金丝雀发布
# 使用 Istio 或 Flagger 实现自动化
# Istio VirtualService 示例
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: myapp
spec:
hosts:
- myapp.example.com
http:
- route:
- destination:
host: myapp
subset: stable
weight: 90
- destination:
host: myapp
subset: canary
weight: 10
YAML
# Flagger 金丝雀配置
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
name: myapp
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: myapp
progressDeadlineSeconds: 600
service:
port: 3000
analysis:
interval: 1m
threshold: 5
maxWeight: 50
stepWeight: 10
metrics:
- name: request-success-rate
thresholdRange:
min: 99
interval: 1m
- name: request-duration
thresholdRange:
max: 500
interval: 1m# Flagger 金丝雀配置
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
name: myapp
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: myapp
progressDeadlineSeconds: 600
service:
port: 3000
analysis:
interval: 1m
threshold: 5
maxWeight: 50
stepWeight: 10
metrics:
- name: request-success-rate
thresholdRange:
min: 99
interval: 1m
- name: request-duration
thresholdRange:
max: 500
interval: 1m
六、自动化测试集成
6.1 单元测试
YAML
# GitHub Actions 单元测试
- name: Run unit tests
run: npm test -- --coverage --watchAll=false
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/lcov.info
fail_ci_if_error: true# GitHub Actions 单元测试
- name: Run unit tests
run: npm test -- --coverage --watchAll=false
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/lcov.info
fail_ci_if_error: true
6.2 端到端测试(E2E)
YAML
# Playwright E2E 测试
name: E2E Tests
on:
push:
branches: [main]
pull_request:
jobs:
e2e:
runs-on: ubuntu-latest
services:
# 启动测试用的后端服务
api:
image: myapp-api:test
ports:
- 3000:3000
env:
NODE_ENV: test
DATABASE_URL: postgresql://test:test@db:5432/test
db:
image: postgres:16-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install Playwright
run: |
npm ci
npx playwright install --with-deps
- name: Run E2E tests
run: npx playwright test
env:
BASE_URL: http://localhost:3000
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 30# Playwright E2E 测试
name: E2E Tests
on:
push:
branches: [main]
pull_request:
jobs:
e2e:
runs-on: ubuntu-latest
services:
# 启动测试用的后端服务
api:
image: myapp-api:test
ports:
- 3000:3000
env:
NODE_ENV: test
DATABASE_URL: postgresql://test:test@db:5432/test
db:
image: postgres:16-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install Playwright
run: |
npm ci
npx playwright install --with-deps
- name: Run E2E tests
run: npx playwright test
env:
BASE_URL: http://localhost:3000
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 30
6.3 性能测试
YAML
# k6 性能测试
name: Performance Tests
on:
schedule:
- cron: '0 2 * * *' # 每天凌晨2点
workflow_dispatch:
jobs:
k6:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run k6
uses: grafana/k6-action@v0.3.0
with:
filename: tests/performance/script.js
env:
K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN }}
- name: Upload results
uses: actions/upload-artifact@v4
with:
name: k6-results
path: summary.json# k6 性能测试
name: Performance Tests
on:
schedule:
- cron: '0 2 * * *' # 每天凌晨2点
workflow_dispatch:
jobs:
k6:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run k6
uses: grafana/k6-action@v0.3.0
with:
filename: tests/performance/script.js
env:
K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN }}
- name: Upload results
uses: actions/upload-artifact@v4
with:
name: k6-results
path: summary.json
JAVASCRIPT
// tests/performance/script.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 20 }, // 30秒内增加到 20 VUs
{ duration: '1m', target: 20 }, // 维持 1 分钟
{ duration: '30s', target: 0 }, // 30秒内减少到 0
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% 的请求 < 500ms
http_req_failed: ['rate<0.01'], // 错误率 < 1%
},
};
export default function () {
const res = http.get('https://example.com/api/users');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
}// tests/performance/script.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 20 }, // 30秒内增加到 20 VUs
{ duration: '1m', target: 20 }, // 维持 1 分钟
{ duration: '30s', target: 0 }, // 30秒内减少到 0
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% 的请求 < 500ms
http_req_failed: ['rate<0.01'], // 错误率 < 1%
},
};
export default function () {
const res = http.get('https://example.com/api/users');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
}
七、安全扫描集成
7.1 SAST(静态应用安全测试)
YAML
# GitHub Actions - CodeQL
name: CodeQL
on:
push:
branches: [main]
pull_request:
schedule:
- cron: '0 0 * * 1' # 每周一
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: javascript, typescript
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3# GitHub Actions - CodeQL
name: CodeQL
on:
push:
branches: [main]
pull_request:
schedule:
- cron: '0 0 * * 1' # 每周一
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: javascript, typescript
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
7.2 SCA(软件成分分析)
YAML
# Dependabot 自动依赖更新
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "09:00"
open-pull-requests-limit: 10
reviewers:
- "team-lead"
labels:
- "dependencies"
- "security"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"# Dependabot 自动依赖更新
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "09:00"
open-pull-requests-limit: 10
reviewers:
- "team-lead"
labels:
- "dependencies"
- "security"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
YAML
# Trivy 漏洞扫描
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:latest'
format: 'table'
exit-code: '1'
ignore-unfixed: true
vuln-type: 'os,library'
severity: 'CRITICAL,HIGH'# Trivy 漏洞扫描
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:latest'
format: 'table'
exit-code: '1'
ignore-unfixed: true
vuln-type: 'os,library'
severity: 'CRITICAL,HIGH'
7.3 DAST(动态应用安全测试)
YAML
# OWASP ZAP 扫描
name: DAST
on:
workflow_dispatch:
jobs:
zap_scan:
runs-on: ubuntu-latest
name: ZAP Scan
steps:
- name: Checkout
uses: actions/checkout@v4
- name: ZAP Full Scan
uses: zaproxy/action-full-scan@v0.7.0
with:
target: 'https://staging.example.com'
rules_file_name: '.zap/rules.tsv'
cmd_options: '-a'# OWASP ZAP 扫描
name: DAST
on:
workflow_dispatch:
jobs:
zap_scan:
runs-on: ubuntu-latest
name: ZAP Scan
steps:
- name: Checkout
uses: actions/checkout@v4
- name: ZAP Full Scan
uses: zaproxy/action-full-scan@v0.7.0
with:
target: 'https://staging.example.com'
rules_file_name: '.zap/rules.tsv'
cmd_options: '-a'
八、通知与告警
8.1 Slack 通知
YAML
# GitHub Actions Slack 通知
- name: Notify Slack
if: always()
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,message,commit,author,action,eventName,ref,workflow
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}# GitHub Actions Slack 通知
- name: Notify Slack
if: always()
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,message,commit,author,action,eventName,ref,workflow
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
8.2 部署状态徽章
MARKDOWN
<!-- README.md -->


<!-- README.md -->



九、最佳实践
9.1 CI/CD 流水线设计原则
PLAINTEXT
1. 快速反馈
- 构建时间 < 10 分钟
- 失败立即通知
- 明确的错误信息
2. 失败安全
- 任何步骤失败都要停止
- 自动回滚机制
- 保留日志和现场
3. 幂等性
- 流水线可以多次运行
- 不会因为重复运行出问题
4. 可追溯
- 每次部署都有记录
- 可以回溯到具体代码版本
- 部署历史可查询
5. 渐进式发布
- 先测试环境,后生产环境
- 小流量验证,逐步放量
- 监控指标驱动
6. 安全优先
- Secrets 加密存储
- 镜像签名验证
- 安全扫描集成1. 快速反馈
- 构建时间 < 10 分钟
- 失败立即通知
- 明确的错误信息
2. 失败安全
- 任何步骤失败都要停止
- 自动回滚机制
- 保留日志和现场
3. 幂等性
- 流水线可以多次运行
- 不会因为重复运行出问题
4. 可追溯
- 每次部署都有记录
- 可以回溯到具体代码版本
- 部署历史可查询
5. 渐进式发布
- 先测试环境,后生产环境
- 小流量验证,逐步放量
- 监控指标驱动
6. 安全优先
- Secrets 加密存储
- 镜像签名验证
- 安全扫描集成
9.2 流水线优化技巧
YAML
# 优化技巧
# 1. 缓存依赖
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm' # 启用缓存
# 2. 并行执行
jobs:
lint:
...
test:
...
build:
...
# 以上三个 job 并行执行
# 3. 条件执行
- name: Deploy
if: github.ref == 'refs/heads/main'
# 4. 使用矩阵减少配置
strategy:
matrix:
node: [18, 20, 22]
# 5. 复用工作流
# .github/workflows/deploy.yml 可被其他工作流调用
on:
workflow_call:
inputs:
environment:
required: true
type: string# 优化技巧
# 1. 缓存依赖
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm' # 启用缓存
# 2. 并行执行
jobs:
lint:
...
test:
...
build:
...
# 以上三个 job 并行执行
# 3. 条件执行
- name: Deploy
if: github.ref == 'refs/heads/main'
# 4. 使用矩阵减少配置
strategy:
matrix:
node: [18, 20, 22]
# 5. 复用工作流
# .github/workflows/deploy.yml 可被其他工作流调用
on:
workflow_call:
inputs:
environment:
required: true
type: string
9.3 Git 分支策略
PLAINTEXT
Git Flow(适合有明确发布周期的项目):
main ────●──────────────●────────●──── (生产)
\ ↑
\ release
\ ↑
develop ─────────●──●──●──●──●──●──●──── (开发)
\ /
feature/ ●──●──● (功能分支)
---
GitHub Flow(适合持续部署):
main ────●──●──●──●──●──●──●──── (始终可部署)
\ \ \ \ \ \
feature/ ● ● ● ● ● ●── (通过 PR 合并)
---
Trunk-Based Development(适合高频部署):
main ────●──●──●──●──●──●──●──●──── (主干,频繁集成)
\ / \ | / \ /
short/ ● ● ● ● ● (短生命周期分支)Git Flow(适合有明确发布周期的项目):
main ────●──────────────●────────●──── (生产)
\ ↑
\ release
\ ↑
develop ─────────●──●──●──●──●──●──●──── (开发)
\ /
feature/ ●──●──● (功能分支)
---
GitHub Flow(适合持续部署):
main ────●──●──●──●──●──●──●──── (始终可部署)
\ \ \ \ \ \
feature/ ● ● ● ● ● ●── (通过 PR 合并)
---
Trunk-Based Development(适合高频部署):
main ────●──●──●──●──●──●──●──●──── (主干,频繁集成)
\ / \ | / \ /
short/ ● ● ● ● ● (短生命周期分支)
十、故障排查
Q1:构建很慢怎么办?
YAML
# 优化方案:
# 1. 启用缓存
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
# 2. 使用更快的安装工具
- run: npm install -g pnpm
- run: pnpm install --frozen-lockfile
# 3. 减少不必要的步骤
# 只在 main 分支运行 lint
- name: Lint
if: github.ref == 'refs/heads/main'
run: pnpm lint
# 4. 拆分 Job 并行执行
jobs:
lint: ...
test: ...
build: ...
# 5. 使用自托管 Runner(更快、更稳定)
runs-on: self-hosted# 优化方案:
# 1. 启用缓存
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
# 2. 使用更快的安装工具
- run: npm install -g pnpm
- run: pnpm install --frozen-lockfile
# 3. 减少不必要的步骤
# 只在 main 分支运行 lint
- name: Lint
if: github.ref == 'refs/heads/main'
run: pnpm lint
# 4. 拆分 Job 并行执行
jobs:
lint: ...
test: ...
build: ...
# 5. 使用自托管 Runner(更快、更稳定)
runs-on: self-hosted
Q2:部署失败怎么回滚?
BASH
# Kubernetes 回滚
kubectl rollout undo deployment/myapp -n production
# 回滚到指定版本
kubectl rollout undo deployment/myapp --to-revision=2 -n production
# Docker Compose 回滚
docker-compose -f docker-compose.prev.yml up -d# Kubernetes 回滚
kubectl rollout undo deployment/myapp -n production
# 回滚到指定版本
kubectl rollout undo deployment/myapp --to-revision=2 -n production
# Docker Compose 回滚
docker-compose -f docker-compose.prev.yml up -d
Q3:如何实现零停机部署?
YAML
# 关键配置:
# 1. 健康检查
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
# 2. 优雅关闭
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]
# 3. 滚动更新策略
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 0# 关键配置:
# 1. 健康检查
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
# 2. 优雅关闭
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]
# 3. 滚动更新策略
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 0
十一、总结
CI/CD 实施清单
- 选择 CI/CD 平台
- 配置代码仓库集成
- 编写构建脚本
- 配置自动化测试
- 添加安全扫描
- 配置部署流水线
- 实现回滚机制
- 配置通知告警
- 监控部署指标
- 定期审查和优化
学习路径
PLAINTEXT
入门(第1周):
→ CI/CD 概念理解
→ GitHub Actions 基础
→ 简单流水线配置
进阶(第2-3周):
→ 多环境部署
→ Docker 镜像构建
→ 测试集成
高级(第1-2月):
→ Kubernetes 部署
→ 部署策略
→ 安全扫描集成
专家(第3月+):
→ GitOps(ArgoCD)
→ 平台工程
→ 企业级实践入门(第1周):
→ CI/CD 概念理解
→ GitHub Actions 基础
→ 简单流水线配置
进阶(第2-3周):
→ 多环境部署
→ Docker 镜像构建
→ 测试集成
高级(第1-2月):
→ Kubernetes 部署
→ 部署策略
→ 安全扫描集成
专家(第3月+):
→ GitOps(ArgoCD)
→ 平台工程
→ 企业级实践
【相关推荐】
- Docker 进阶与容器编排实战 - 容器化
- Nginx 反向代理与负载均衡 - Web 服务器
- Git 版本控制与团队协作 - 版本控制
CI/CD 是现代软件交付的基石。 掌握 CI/CD,让代码从提交到生产全流程自动化,告别手工部署的混乱与风险。🚀
