CI/CD 完整指南
CI/CD 完整指南
1. CI/CD 基础概念
1.1 核心流程
# CI/CD 流水线示例
Pipeline:
Stages:
- 代码检查 (Lint)
- 单元测试 (Test)
- 构建打包 (Build)
- 安全扫描 (Security)
- 部署测试 (Deploy to Staging)
- 集成测试 (Integration Test)
- 生产部署 (Deploy to Production)
2. GitHub Actions CI/CD 实现
2.1 基础工作流配置
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env:
NODE_VERSION: '18.x'
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# 代码质量检查
lint-and-test:
name: Lint & 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 ESLint
run: npm run lint
- name: Run TypeScript check
run: npm run type-check
- name: Run unit tests
run: npm run test:unit
- name: Run coverage
run: npm run test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
# 构建阶段
build:
name: Build
runs-on: ubuntu-latest
needs: lint-and-test
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: Build application
run: npm run build
- name: Run bundle analyzer
run: npm run build:analyze
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 7
# 安全扫描
security:
name: Security Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run Snyk to check for vulnerabilities
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'
2.2 Docker 构建和推送
# .github/workflows/docker.yml
name: Build and Push Docker Image
on:
push:
branches: [ main ]
tags: [ 'v*.*.*' ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
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=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@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
2.3 完整部署流水线
# .github/workflows/deploy.yml
name: Deploy to Environments
on:
workflow_dispatch:
inputs:
environment:
description: '部署环境'
required: true
default: 'staging'
type: choice
options:
- staging
- production
jobs:
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
environment: staging
if: github.event.inputs.environment == 'staging' || github.ref == 'refs/heads/develop'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Deploy to Staging
run: |
echo "部署到测试环境..."
# 这里可以是 kubectl, helm, ssh 等部署命令
./scripts/deploy.sh staging
- name: Run smoke tests
run: |
echo "运行冒烟测试..."
npm run test:smoke
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
environment: production
needs: deploy-staging
if: github.event.inputs.environment == 'production' || github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Deploy to Production
run: |
echo "部署到生产环境..."
./scripts/deploy.sh production
- name: Run health checks
run: |
echo "运行健康检查..."
./scripts/health-check.sh
- name: Notify deployment
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: 生产环境部署完成
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
3. GitLab CI/CD 配置
3.1 .gitlab-ci.yml 完整配置
# .gitlab-ci.yml
stages:
- test
- build
- security
- deploy
variables:
NODE_VERSION: "18.19.0"
DOCKER_REGISTRY: registry.gitlab.com
DOCKER_IMAGE: $CI_REGISTRY_IMAGE
# 缓存配置
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
- .npm/
before_script:
- npm ci --cache .npm --prefer-offline
lint:
stage: test
script:
- npm run lint
- npm run type-check
only:
- merge_requests
- develop
- main
test:
stage: test
script:
- npm run test:unit
- npm run test:coverage
coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
artifacts:
reports:
cobertura: coverage/cobertura-coverage.xml
paths:
- coverage/
expire_in: 1 week
build:
stage: build
script:
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 week
only:
- main
- develop
- tags
security-scan:
stage: security
image: node:$NODE_VERSION
script:
- npm audit --audit-level high
- npx snyk test --severity-threshold=high
allow_failure: true
only:
- main
- develop
docker-build:
stage: build
image: docker:20.10
services:
- docker:20.10-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $DOCKER_IMAGE:$CI_COMMIT_SHA .
- docker push $DOCKER_IMAGE:$CI_COMMIT_SHA
- docker tag $DOCKER_IMAGE:$CI_COMMIT_SHA $DOCKER_IMAGE:latest
- docker push $DOCKER_IMAGE:latest
only:
- main
- tags
deploy-staging:
stage: deploy
environment:
name: staging
url: https://staging.example.com
script:
- echo "部署到测试环境"
- kubectl set image deployment/my-app app=$DOCKER_IMAGE:$CI_COMMIT_SHA -n staging
only:
- develop
deploy-production:
stage: deploy
environment:
name: production
url: https://example.com
script:
- echo "部署到生产环境"
- kubectl set image deployment/my-app app=$DOCKER_IMAGE:$CI_COMMIT_SHA -n production
when: manual
only:
- main
4. Jenkins Pipeline 配置
4.1 Jenkinsfile 完整配置
// Jenkinsfile
pipeline {
agent {
docker {
image 'node:18-alpine'
args '-p 3000:3000'
}
}
environment {
NODE_ENV = 'production'
DOCKER_REGISTRY = 'registry.example.com'
DOCKER_IMAGE = "${DOCKER_REGISTRY}/my-app"
}
options {
timeout(time: 30, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '10'))
disableConcurrentBuilds()
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Install') {
steps {
sh 'npm ci'
}
}
stage('Lint') {
steps {
sh 'npm run lint'
}
}
stage('Test') {
parallel {
stage('Unit Tests') {
steps {
sh 'npm run test:unit'
}
post {
always {
junit 'test-results/**/*.xml'
}
}
}
stage('Integration Tests') {
steps {
sh 'npm run test:integration'
}
}
}
}
stage('Build') {
steps {
sh 'npm run build'
}
post {
success {
archiveArtifacts artifacts: 'dist/**/*', fingerprint: true
}
}
}
stage('Docker Build') {
agent {
docker {
image 'docker:20.10'
args '--privileged -v /var/run/docker.sock:/var/run/docker.sock'
}
}
steps {
script {
docker.build("${DOCKER_IMAGE}:${env.BUILD_ID}")
}
}
}
stage('Security Scan') {
steps {
sh 'npm audit --audit-level high'
sh 'trivy image ${DOCKER_IMAGE}:${env.BUILD_ID}'
}
}
stage('Deploy to Staging') {
when {
branch 'develop'
}
steps {
script {
sh "kubectl set image deployment/my-app app=${DOCKER_IMAGE}:${env.BUILD_ID} -n staging"
}
}
}
stage('Deploy to Production') {
when {
branch 'main'
}
steps {
input message: '部署到生产环境?', ok: 'Deploy'
script {
sh "kubectl set image deployment/my-app app=${DOCKER_IMAGE}:${env.BUILD_ID} -n production"
}
}
}
}
post {
always {
emailext (
subject: "构建结果: ${currentBuild.fullDisplayName}",
body: "构建结果: ${currentBuild.result}\n\n查看详情: ${env.BUILD_URL}",
to: 'dev-team@example.com'
)
}
success {
slackSend(
channel: '#builds',
message: "构建成功: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
)
}
failure {
slackSend(
channel: '#builds',
message: "构建失败: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
)
}
}
}
5. 部署脚本和配置
5.1 Kubernetes 部署配置
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend-app
namespace: production
labels:
app: frontend
spec:
replicas: 3
selector:
matchLabels:
app: frontend
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
image: registry.example.com/my-app:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
- name: API_URL
valueFrom:
configMapKeyRef:
name: app-config
key: api.url
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: frontend-service
namespace: production
spec:
selector:
app: frontend
ports:
- port: 80
targetPort: 3000
type: LoadBalancer
5.2 部署脚本
#!/bin/bash
# scripts/deploy.sh
set -e
ENVIRONMENT=$1
VERSION=${2:-latest}
deploy_staging() {
echo "🚀 部署到测试环境..."
# 更新 Kubernetes 部署
kubectl set image deployment/frontend-app frontend=registry.example.com/my-app:$VERSION -n staging
# 等待部署完成
kubectl rollout status deployment/frontend-app -n staging --timeout=300s
# 运行冒烟测试
echo "运行冒烟测试..."
npm run test:smoke -- --base-url https://staging.example.com
echo "✅ 测试环境部署完成"
}
deploy_production() {
echo "🚀 部署到生产环境..."
# 蓝绿部署策略
CURRENT_VERSION=$(kubectl get deployment frontend-app -n production -o=jsonpath='{.spec.template.spec.containers[0].image}')
# 创建新版本部署
kubectl apply -f k8s/production-deployment-v2.yaml
# 等待新版本就绪
kubectl rollout status deployment/frontend-app-v2 -n production --timeout=300s
# 切换流量
kubectl patch service frontend-service -n production -p '{"spec":{"selector":{"version":"v2"}}}'
# 健康检查
./scripts/health-check.sh production
# 清理旧版本
kubectl delete deployment frontend-app-v1 -n production
echo "✅ 生产环境部署完成"
}
case $ENVIRONMENT in
"staging")
deploy_staging
;;
"production")
deploy_production
;;
*)
echo "❌ 未知环境: $ENVIRONMENT"
echo "用法: $0 {staging|production} [version]"
exit 1
;;
esac
5.3 健康检查脚本
#!/bin/bash
# scripts/health-check.sh
set -e
ENVIRONMENT=$1
MAX_ATTEMPTS=30
ATTEMPT=0
check_health() {
local url=$1
local response=$(curl -s -o /dev/null -w "%{http_code}" $url/health)
if [ "$response" = "200" ]; then
return 0
else
return 1
fi
}
case $ENVIRONMENT in
"staging")
URL="https://staging.example.com"
;;
"production")
URL="https://example.com"
;;
*)
echo "❌ 未知环境: $ENVIRONMENT"
exit 1
;;
esac
echo "🔍 检查 $ENVIRONMENT 环境健康状态..."
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
if check_health $URL; then
echo "✅ 健康检查通过"
exit 0
else
ATTEMPT=$((ATTEMPT + 1))
echo "⏳ 等待服务就绪... ($ATTEMPT/$MAX_ATTEMPTS)"
sleep 10
fi
done
echo "❌ 健康检查失败: 服务在指定时间内未就绪"
exit 1
6. 监控和告警
6.1 监控配置
# monitoring/prometheus.yml
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'frontend-app'
static_configs:
- targets: ['frontend-service:3000']
metrics_path: '/metrics'
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter:9100']
6.2 告警规则
# monitoring/alerts.yml
groups:
- name: frontend-app
rules:
- alert: HighErrorRate
expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.1
for: 5m
labels:
severity: critical
annotations:
summary: "高错误率检测"
description: "错误率超过 10%"
- alert: HighResponseTime
expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 1
for: 5m
labels:
severity: warning
annotations:
summary: "高响应时间"
description: "95% 分位响应时间超过 1 秒"
7. 最佳实践
7.1 安全最佳实践
# 安全扫描集成
security-scan:
stage: security
script:
# 依赖漏洞扫描
- npm audit --audit-level high
# 容器漏洞扫描
- docker scan $IMAGE_NAME
# 静态代码安全分析
- npx semgrep --config=auto .
allow_failure: false
7.2 性能优化
# 缓存优化
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
- .npm/
- .cache/
policy: pull-push
# 并行执行
test:
parallel:
matrix:
- SUITE: [unit, integration, e2e]
script:
- npm run test:$SUITE
这个完整的 CI/CD 配置涵盖了从代码提交到生产部署的全流程,包括代码检查、测试、安全扫描、容器化、部署和监控等关键环节。
挣钱养家