CI/CD 集成指南
自动化构建、测试和部署 API 服务的最佳实践
CI/CD 流程概览
GitHub Actions 配置
主工作流
yaml
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
release:
types: [ created ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# 代码质量检查
quality-check:
name: Code Quality Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run type check
run: npm run type-check
- name: Check code formatting
run: npm run format:check
- name: Run security audit
run: npm audit --audit-level=moderate
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
# 单元测试
unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16, 18, 20]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:unit -- --coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
flags: unittests
name: codecov-umbrella
# 集成测试
integration-tests:
name: Integration Tests
runs-on: ubuntu-latest
needs: [quality-check, unit-tests]
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:7
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
steps:
- uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run database migrations
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb
run: npm run db:migrate
- name: Run integration tests
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb
REDIS_URL: redis://localhost:6379
run: npm run test:integration
- name: Run API tests
run: npm run test:api
# 构建和发布 Docker 镜像
build-and-push:
name: Build and Push Docker Image
runs-on: ubuntu-latest
needs: [integration-tests]
if: github.event_name != 'pull_request'
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Container Registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix={{branch}}-
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
BUILD_VERSION=${{ github.sha }}
BUILD_TIME=${{ steps.meta.outputs.created }}
# 安全扫描
security-scan:
name: Security Scan
runs-on: ubuntu-latest
needs: [build-and-push]
steps:
- uses: actions/checkout@v3
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
- name: OWASP Dependency Check
uses: dependency-check/Dependency-Check_Action@main
with:
project: 'api-service'
path: '.'
format: 'HTML'
args: >
--enableRetired
# 部署到测试环境
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
needs: [security-scan]
if: github.ref == 'refs/heads/develop'
environment:
name: staging
url: https://staging.api.example.com
steps:
- uses: actions/checkout@v3
- name: Deploy to Kubernetes
uses: azure/k8s-deploy@v4
with:
namespace: staging
manifests: |
k8s/staging/deployment.yaml
k8s/staging/service.yaml
k8s/staging/ingress.yaml
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
# E2E 测试
e2e-tests:
name: E2E Tests
runs-on: ubuntu-latest
needs: [deploy-staging]
if: github.ref == 'refs/heads/develop'
steps:
- uses: actions/checkout@v3
- name: Run E2E tests
uses: cypress-io/github-action@v5
with:
config: baseUrl=https://staging.api.example.com
record: true
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# 部署到生产环境
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
needs: [e2e-tests]
if: github.ref == 'refs/heads/main'
environment:
name: production
url: https://api.example.com
steps:
- uses: actions/checkout@v3
- name: Deploy to Production
uses: azure/k8s-deploy@v4
with:
namespace: production
manifests: |
k8s/production/deployment.yaml
k8s/production/service.yaml
k8s/production/ingress.yaml
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
- name: Create Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ github.run_number }}
release_name: Release v${{ github.run_number }}
body: |
Changes in this Release
- Automated release from commit ${{ github.sha }}
draft: false
prerelease: falseGitLab CI/CD 配置
yaml
# .gitlab-ci.yml
stages:
- build
- test
- security
- package
- deploy
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version"
# 缓存配置
.maven-cache:
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- .m2/repository/
- target/
# 构建阶段
build:
stage: build
image: maven:3.8-openjdk-17
extends: .maven-cache
script:
- mvn $MAVEN_CLI_OPTS clean compile
artifacts:
paths:
- target/
expire_in: 1 week
# 测试阶段
unit-test:
stage: test
image: maven:3.8-openjdk-17
extends: .maven-cache
script:
- mvn $MAVEN_CLI_OPTS test
- mvn jacoco:report
coverage: '/Total.*?([0-9]{1,3})%/'
artifacts:
reports:
junit:
- target/surefire-reports/TEST-*.xml
paths:
- target/site/jacoco/
expire_in: 1 week
integration-test:
stage: test
image: maven:3.8-openjdk-17
extends: .maven-cache
services:
- postgres:15
- redis:7
variables:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
SPRING_DATASOURCE_URL: "jdbc:postgresql://postgres:5432/testdb"
SPRING_REDIS_HOST: redis
script:
- mvn $MAVEN_CLI_OPTS verify -DskipUnitTests=true
# 代码质量检查
code-quality:
stage: test
image: maven:3.8-openjdk-17
extends: .maven-cache
script:
- mvn $MAVEN_CLI_OPTS sonar:sonar
-Dsonar.projectKey=$CI_PROJECT_NAME
-Dsonar.host.url=$SONAR_HOST_URL
-Dsonar.login=$SONAR_TOKEN
only:
- merge_requests
- main
- develop
# 安全扫描
security-scan:
stage: security
image:
name: aquasec/trivy:latest
entrypoint: [""]
script:
- trivy fs --no-progress --security-checks vuln,secret,config .
allow_failure: true
dependency-check:
stage: security
image: owasp/dependency-check:latest
script:
- /usr/share/dependency-check/bin/dependency-check.sh
--project "$CI_PROJECT_NAME"
--scan .
--format "ALL"
--enableExperimental
artifacts:
paths:
- dependency-check-report.*
expire_in: 1 week
# Docker 镜像构建
docker-build:
stage: package
image: docker:latest
services:
- docker:dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $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
only:
- main
- develop
# 部署阶段
deploy-staging:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl config use-context $K8S_CONTEXT_STAGING
- kubectl set image deployment/api-service
api-service=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
--namespace=staging
- kubectl rollout status deployment/api-service --namespace=staging
environment:
name: staging
url: https://staging.api.example.com
only:
- develop
deploy-production:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl config use-context $K8S_CONTEXT_PRODUCTION
- kubectl set image deployment/api-service
api-service=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
--namespace=production
- kubectl rollout status deployment/api-service --namespace=production
environment:
name: production
url: https://api.example.com
only:
- main
when: manualJenkins Pipeline
groovy
// Jenkinsfile
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: maven
image: maven:3.8-openjdk-17
command: ['sleep', '99999']
- name: docker
image: docker:latest
command: ['sleep', '99999']
volumeMounts:
- name: docker-sock
mountPath: /var/run/docker.sock
- name: kubectl
image: bitnami/kubectl:latest
command: ['sleep', '99999']
volumes:
- name: docker-sock
hostPath:
path: /var/run/docker.sock
'''
}
}
environment {
REGISTRY = 'registry.example.com'
IMAGE_NAME = 'api-service'
SONAR_HOST = credentials('sonar-host')
SONAR_TOKEN = credentials('sonar-token')
}
stages {
stage('Checkout') {
steps {
checkout scm
script {
env.GIT_COMMIT = sh(
script: 'git rev-parse HEAD',
returnStdout: true
).trim()
env.GIT_BRANCH = sh(
script: 'git rev-parse --abbrev-ref HEAD',
returnStdout: true
).trim()
}
}
}
stage('Build & Test') {
parallel {
stage('Java Build') {
steps {
container('maven') {
sh 'mvn clean package'
}
}
}
stage('Frontend Build') {
steps {
container('node') {
sh '''
npm ci
npm run build
npm run test
'''
}
}
}
}
}
stage('Code Analysis') {
steps {
container('maven') {
withSonarQubeEnv('SonarQube') {
sh """
mvn sonar:sonar \
-Dsonar.projectKey=${env.JOB_NAME} \
-Dsonar.host.url=${env.SONAR_HOST} \
-Dsonar.login=${env.SONAR_TOKEN}
"""
}
}
}
}
stage('Quality Gate') {
steps {
timeout(time: 1, unit: 'HOURS') {
waitForQualityGate abortPipeline: true
}
}
}
stage('Build Docker Image') {
steps {
container('docker') {
script {
docker.build("${REGISTRY}/${IMAGE_NAME}:${GIT_COMMIT}")
docker.build("${REGISTRY}/${IMAGE_NAME}:latest")
}
}
}
}
stage('Push Docker Image') {
when {
branch pattern: "(main|develop)", comparator: "REGEXP"
}
steps {
container('docker') {
script {
docker.withRegistry("https://${REGISTRY}", 'docker-registry-creds') {
docker.image("${REGISTRY}/${IMAGE_NAME}:${GIT_COMMIT}").push()
docker.image("${REGISTRY}/${IMAGE_NAME}:latest").push()
}
}
}
}
}
stage('Deploy to Staging') {
when {
branch 'develop'
}
steps {
container('kubectl') {
withKubeConfig([credentialsId: 'kubeconfig-staging']) {
sh """
kubectl set image deployment/${IMAGE_NAME} \
${IMAGE_NAME}=${REGISTRY}/${IMAGE_NAME}:${GIT_COMMIT} \
--namespace=staging
kubectl rollout status deployment/${IMAGE_NAME} \
--namespace=staging
"""
}
}
}
}
stage('Deploy to Production') {
when {
branch 'main'
}
input {
message "Deploy to production?"
ok "Deploy"
}
steps {
container('kubectl') {
withKubeConfig([credentialsId: 'kubeconfig-production']) {
sh """
kubectl set image deployment/${IMAGE_NAME} \
${IMAGE_NAME}=${REGISTRY}/${IMAGE_NAME}:${GIT_COMMIT} \
--namespace=production
kubectl rollout status deployment/${IMAGE_NAME} \
--namespace=production
"""
}
}
}
}
}
post {
always {
junit '**/target/surefire-reports/*.xml'
archiveArtifacts artifacts: '**/target/*.jar', fingerprint: true
}
success {
slackSend(
color: 'good',
message: "Build Successful: ${env.JOB_NAME} - ${env.BUILD_NUMBER}"
)
}
failure {
slackSend(
color: 'danger',
message: "Build Failed: ${env.JOB_NAME} - ${env.BUILD_NUMBER}"
)
}
}
}自动化测试集成
测试策略
yaml
# test-strategy.yml
test-pyramid:
unit-tests:
coverage: 80%
tools:
- Jest (JavaScript/TypeScript)
- JUnit (Java)
- pytest (Python)
integration-tests:
coverage: 60%
tools:
- TestContainers
- REST Assured
- Supertest
api-tests:
tools:
- Postman/Newman
- Pact (契约测试)
- Dredd (OpenAPI 验证)
e2e-tests:
tools:
- Cypress
- Playwright
- Selenium
performance-tests:
tools:
- K6
- JMeter
- Gatling契约测试
javascript
// Pact 契约测试示例
const { Pact } = require('@pact-foundation/pact');
const { like, term } = require('@pact-foundation/pact/dsl/matchers');
describe('Product API Contract', () => {
const provider = new Pact({
consumer: 'Frontend',
provider: 'ProductAPI',
port: 8080,
log: path.resolve(process.cwd(), 'logs', 'pact.log'),
dir: path.resolve(process.cwd(), 'pacts'),
logLevel: 'warn',
});
beforeAll(() => provider.setup());
afterEach(() => provider.verify());
afterAll(() => provider.finalize());
describe('get product', () => {
test('should return a product', async () => {
// 定义期望的交互
await provider.addInteraction({
state: 'a product with ID 123 exists',
uponReceiving: 'a request to get a product',
withRequest: {
method: 'GET',
path: '/api/v1/products/123',
headers: {
Accept: 'application/json',
},
},
willRespondWith: {
status: 200,
headers: {
'Content-Type': 'application/json',
},
body: like({
id: '123',
name: 'iPhone 15',
price: 999.99,
category: like({
id: '456',
name: 'Electronics',
}),
}),
},
});
// 执行测试
const response = await fetch('http://localhost:8080/api/v1/products/123');
const product = await response.json();
expect(product.id).toBe('123');
expect(product.name).toBe('iPhone 15');
});
});
});性能测试
javascript
// k6 性能测试脚本
import http from 'k6/http';
import { check, group } from 'k6';
import { Rate } from 'k6/metrics';
export const errorRate = new Rate('errors');
export const options = {
stages: [
{ duration: '2m', target: 10 }, // 预热
{ duration: '5m', target: 50 }, // 爬升
{ duration: '10m', target: 100 }, // 压力测试
{ duration: '5m', target: 50 }, // 下降
{ duration: '2m', target: 0 }, // 冷却
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% 请求在 500ms 内
errors: ['rate<0.1'], // 错误率低于 10%
},
};
const BASE_URL = 'https://api.example.com';
export default function () {
group('API Performance Test', () => {
// 测试获取产品列表
group('GET /products', () => {
const response = http.get(`${BASE_URL}/api/v1/products`);
check(response, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
'has products': (r) => JSON.parse(r.body).data.length > 0,
}) || errorRate.add(1);
});
// 测试创建产品
group('POST /products', () => {
const payload = JSON.stringify({
name: `Product ${Date.now()}`,
price: 99.99,
category: 'test',
});
const params = {
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + __ENV.API_TOKEN,
},
};
const response = http.post(`${BASE_URL}/api/v1/products`, payload, params);
check(response, {
'status is 201': (r) => r.status === 201,
'has product id': (r) => JSON.parse(r.body).id !== undefined,
}) || errorRate.add(1);
});
});
}部署策略
蓝绿部署
yaml
# kubernetes/blue-green-deployment.yaml
apiVersion: v1
kind: Service
metadata:
name: api-service
spec:
selector:
app: api-service
version: green # 切换到 blue 或 green
ports:
- port: 80
targetPort: 8080
---
# Blue 部署
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-service-blue
spec:
replicas: 3
selector:
matchLabels:
app: api-service
version: blue
template:
metadata:
labels:
app: api-service
version: blue
spec:
containers:
- name: api-service
image: registry.example.com/api-service:v1.0.0
ports:
- containerPort: 8080
---
# Green 部署
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-service-green
spec:
replicas: 3
selector:
matchLabels:
app: api-service
version: green
template:
metadata:
labels:
app: api-service
version: green
spec:
containers:
- name: api-service
image: registry.example.com/api-service:v1.1.0
ports:
- containerPort: 8080金丝雀部署
yaml
# kubernetes/canary-deployment.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-service-canary
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "10" # 10% 流量
spec:
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service-canary
port:
number: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-service-canary
spec:
replicas: 1
selector:
matchLabels:
app: api-service
version: canary
template:
metadata:
labels:
app: api-service
version: canary
spec:
containers:
- name: api-service
image: registry.example.com/api-service:v1.1.0-canary
ports:
- containerPort: 8080滚动更新
bash
#!/bin/bash
# rolling-update.sh
# 设置变量
DEPLOYMENT="api-service"
NAMESPACE="production"
NEW_IMAGE="registry.example.com/api-service:v1.1.0"
# 更新镜像
kubectl set image deployment/${DEPLOYMENT} \
${DEPLOYMENT}=${NEW_IMAGE} \
--namespace=${NAMESPACE} \
--record
# 监控更新状态
kubectl rollout status deployment/${DEPLOYMENT} \
--namespace=${NAMESPACE}
# 检查部署历史
kubectl rollout history deployment/${DEPLOYMENT} \
--namespace=${NAMESPACE}
# 如果需要回滚
# kubectl rollout undo deployment/${DEPLOYMENT} --namespace=${NAMESPACE}监控和通知
部署监控
yaml
# prometheus-rules.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: deployment-alerts
spec:
groups:
- name: deployment
interval: 30s
rules:
- alert: DeploymentFailed
expr: |
kube_deployment_status_replicas_available{deployment="api-service"}
< kube_deployment_spec_replicas{deployment="api-service"}
for: 5m
labels:
severity: critical
annotations:
summary: "Deployment {{ $labels.deployment }} has insufficient replicas"
description: "Deployment {{ $labels.deployment }} has {{ $value }} available replicas, expected {{ $labels.spec_replicas }}"
- alert: HighErrorRate
expr: |
rate(http_requests_total{status=~"5.."}[5m]) > 0.05
for: 5m
labels:
severity: warning
annotations:
summary: "High error rate detected"
description: "Error rate is {{ $value }} errors per second"Slack 通知
javascript
// slack-notification.js
const { WebClient } = require('@slack/web-api');
const slack = new WebClient(process.env.SLACK_TOKEN);
async function notifyDeployment(environment, version, status) {
const color = status === 'success' ? 'good' : 'danger';
const emoji = status === 'success' ? ':rocket:' : ':x:';
await slack.chat.postMessage({
channel: '#deployments',
attachments: [{
color: color,
blocks: [
{
type: 'header',
text: {
type: 'plain_text',
text: `${emoji} Deployment ${status.toUpperCase()}`
}
},
{
type: 'section',
fields: [
{
type: 'mrkdwn',
text: `*Environment:*\n${environment}`
},
{
type: 'mrkdwn',
text: `*Version:*\n${version}`
},
{
type: 'mrkdwn',
text: `*Time:*\n${new Date().toISOString()}`
},
{
type: 'mrkdwn',
text: `*Triggered by:*\n${process.env.GITHUB_ACTOR || 'System'}`
}
]
},
{
type: 'actions',
elements: [
{
type: 'button',
text: {
type: 'plain_text',
text: 'View Deployment'
},
url: `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`
},
{
type: 'button',
text: {
type: 'plain_text',
text: 'View Metrics'
},
url: `https://grafana.example.com/d/deployment-${environment}`
}
]
}
]
}]
});
}回滚策略
bash
#!/bin/bash
# rollback-strategy.sh
# 自动回滚脚本
rollback_deployment() {
local deployment=$1
local namespace=$2
local health_check_url=$3
echo "Starting rollback for $deployment in $namespace..."
# 回滚到上一个版本
kubectl rollout undo deployment/$deployment -n $namespace
# 等待回滚完成
kubectl rollout status deployment/$deployment -n $namespace
# 健康检查
for i in {1..30}; do
if curl -f $health_check_url > /dev/null 2>&1; then
echo "Rollback successful - service is healthy"
return 0
fi
echo "Waiting for service to be healthy... ($i/30)"
sleep 10
done
echo "Rollback completed but service health check failed"
return 1
}
# 使用示例
rollback_deployment "api-service" "production" "https://api.example.com/health"最佳实践
- 自动化一切: 从代码提交到生产部署全流程自动化
- 快速反馈: 尽早发现问题,快速失败
- 环境一致性: 使用容器确保各环境一致
- 版本管理: 所有制品都要有版本标记
- 回滚能力: 确保能快速回滚到稳定版本
- 监控告警: 部署后持续监控服务状态
- 安全扫描: 在流程中集成安全检查
- 文档更新: 自动更新 API 文档和变更日志