第四章:持续集成与持续部署 (CI/CD)

最后更新: 2024-01-01 作者: DevOps Team
页面目录

第四章:持续集成与持续部署 (CI/CD)

CI/CD 是 DevOps 的核心实践,通过自动化构建、测试和部署流程,实现快速、可靠的软件交付。本章将深入讲解 CI/CD 的概念、工具和最佳实践。


4.1 CI/CD 核心概念

4.1.1 什么是 CI/CD

┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│    持续集成 (Continuous Integration)                             │
│    ┌─────────┐    ┌─────────┐    ┌─────────┐    ┌─────────┐   │
│    │  编写   │───→│  提交   │───→│  构建   │───→│  测试   │   │
│    │  代码   │    │  代码   │    │  项目   │    │  自动化 │   │
│    └─────────┘    └─────────┘    └─────────┘    └─────────┘   │
│                                                                 │
│    持续交付 (Continuous Delivery)                                │
│    ┌─────────┐    ┌─────────┐    ┌─────────┐    ┌─────────┐   │
│    │  构建   │───→│  部署   │───→│  预生产 │───→│  人工   │   │
│    │  验证   │    │  测试   │    │  环境   │    │  审批   │   │
│    └─────────┘    └─────────┘    └─────────┘    └─────────┘   │
│                                                                 │
│    持续部署 (Continuous Deployment)                              │
│    ┌─────────┐    ┌─────────┐    ┌─────────┐    ┌─────────┐   │
│    │  构建   │───→│  测试   │───→│  生产   │───→│  监控   │   │
│    │  验证   │    │  环境   │    │  环境   │    │  反馈   │   │
│    └─────────┘    └─────────┘    └─────────┘    └─────────┘   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

4.1.2 CI/CD 的价值

价值 说明 量化指标
快速交付 缩短从代码提交到生产部署的时间 部署频率提升 30x
高质量 自动化测试减少人为错误 缺陷率降低 50%
低风险 小步快跑,降低每次变更的风险 故障恢复时间缩短 50%
透明 每个人都能看到构建和部署状态 团队协作效率提升

4.2 Jenkins 实战

4.2.1 Jenkinsfile 语法

Jenkins Pipeline 使用 Groovy DSL 定义流水线:

// Jenkinsfile
pipeline {
    agent any
    
    environment {
        DOCKER_IMAGE = 'myapp'
        DOCKER_TAG = "${env.BUILD_NUMBER}"
    }
    
    options {
        timestamps()
        timeout(time: 30, unit: 'MINUTES')
        buildDiscarder(logRotator(numToKeepStr: '10'))
    }
    
    stages {
        stage('Checkout') {
            steps {
                echo 'Checking out source code...'
                checkout scm
            }
        }
        
        stage('Build') {
            steps {
                echo 'Building application...'
                sh '''
                    npm install
                    npm run build
                '''
            }
        }
        
        stage('Test') {
            steps {
                echo 'Running tests...'
                sh 'npm test'
                junit '**/test-results/*.xml'
                cobertura coberturaReportFile: 'coverage/cobertura-coverage.xml'
            }
        }
        
        stage('Security Scan') {
            steps {
                echo 'Running security scans...'
                sh '''
                    npm audit --audit-level=moderate
                    docker run --rm -v $(pwd):/src aquasec/trivy:latest /src
                '''
            }
        }
        
        stage('Docker Build & Push') {
            when {
                branch 'main'
            }
            steps {
                echo 'Building Docker image...'
                sh '''
                    docker build -t ${DOCKER_IMAGE}:${DOCKER_TAG} .
                    docker tag ${DOCKER_IMAGE}:${DOCKER_TAG} ${DOCKER_IMAGE}:latest
                    docker login -u ${DOCKER_USER} -p ${DOCKER_PASS}
                    docker push ${DOCKER_IMAGE}:${DOCKER_TAG}
                    docker push ${DOCKER_IMAGE}:latest
                '''
            }
        }
        
        stage('Deploy to Staging') {
            when {
                branch 'main'
            }
            steps {
                echo 'Deploying to staging...'
                sh '''
                    kubectl set image deployment/myapp \
                        app=${DOCKER_IMAGE}:${DOCKER_TAG} \
                        --namespace=staging
                    kubectl rollout status deployment/myapp \
                        --namespace=staging
                '''
            }
        }
        
        stage('Deploy to Production') {
            when {
                branch 'main'
            }
            steps {
                input message: 'Deploy to production?', 
                      ok: 'Deploy'
                echo 'Deploying to production...'
                sh '''
                    kubectl set image deployment/myapp \
                        app=${DOCKER_IMAGE}:${DOCKER_TAG} \
                        --namespace=production
                    kubectl rollout status deployment/myapp \
                        --namespace=production
                '''
            }
        }
    }
    
    post {
        always {
            echo 'Cleaning up...'
            cleanWs()
        }
        success {
            echo 'Pipeline succeeded!'
            slackSend channel: '#devops',
                      message: "Build #${env.BUILD_NUMBER} succeeded!"
        }
        failure {
            echo 'Pipeline failed!'
            slackSend channel: '#devops',
                      message: "Build #${env.BUILD_NUMBER} failed!",
                      color: 'danger'
        }
    }
}

4.2.2 共享库 (Shared Libraries)

// vars/buildDockerImage.groovy
def call(Map config) {
    def imageName = config.imageName ?: env.DOCKER_IMAGE
    def tag = config.tag ?: env.BUILD_NUMBER
    def registry = config.registry ?: 'docker.io'
    
    pipeline {
        stages {
            stage('Build Docker Image') {
                steps {
                    script {
                        def fullImageName = "${registry}/${imageName}:${tag}"
                        sh """
                            docker build -t ${fullImageName} .
                            docker push ${fullImageName}
                        """
                    }
                }
            }
        }
    }
}

// 使用共享库
@Library('my-shared-library') _

buildDockerImage imageName: 'myapp', tag: 'v1.0.0'

4.3 GitLab CI 实战

4.3.1 .gitlab-ci.yml 配置

# .gitlab-ci.yml
stages:
  - build
  - test
  - security
  - deploy

variables:
  DOCKER_IMAGE: registry.gitlab.com/username/myapp
  DOCKER_DRIVER: overlay2

# 缓存配置
cache:
  key: ${CI_COMMIT_REF_SLUG}
  paths:
    - node_modules/
    - .npm/

# Docker 镜像
docker:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  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
  only:
    - main
    - develop

# 测试阶段
unit-test:
  stage: test
  image: node:18-alpine
  script:
    - npm ci
    - npm run test:unit
    - npm run test:e2e
  coverage: '/Coverage: \d+\.\d+%/'
  artifacts:
    reports:
      junit: junit.xml
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml

lint:
  stage: test
  image: node:18-alpine
  script:
    - npm ci
    - npm run lint
    - npm run type-check

# 安全扫描
security-scan:
  stage: security
  image: aquasec/trivy:latest
  script:
    - trivy image --exit-code 1 --severity HIGH $DOCKER_IMAGE:$CI_COMMIT_SHA
  allow_failure: true
  only:
    - main

# 部署到开发环境
deploy:dev:
  stage: deploy
  image: bitnami/kubectl:latest
  script:
    - kubectl config set-cluster dev --server=${K8S_DEV_SERVER} --insecure-skip-tls-verify=true
    - kubectl config set-credentials dev --token=${K8S_DEV_TOKEN}
    - kubectl config set-context dev --cluster=dev --user=dev
    - kubectl config use-context dev
    - kubectl set image deployment/myapp app=$DOCKER_IMAGE:$CI_COMMIT_SHA -n default
    - kubectl rollout status deployment/myapp -n default
  environment:
    name: development
    url: https://dev.example.com
  only:
    - develop

# 部署到生产环境
deploy:prod:
  stage: deploy
  image: bitnami/kubectl:latest
  script:
    - kubectl config set-cluster prod --server=${K8S_PROD_SERVER}
    - kubectl config set-credentials prod --token=${K8S_PROD_TOKEN}
    - kubectl config set-context prod --cluster=prod --user=prod
    - kubectl config use-context prod
    - kubectl set image deployment/myapp app=$DOCKER_IMAGE:$CI_COMMIT_SHA -n production
    - kubectl rollout status deployment/myapp -n production
  environment:
    name: production
    url: https://example.com
  when: manual
  only:
    - main

4.3.2 GitLab CI 高级特性

# 矩阵式构建
build:matrix:
  stage: build
  parallel:
    matrix:
      - OS: ubuntu
        VERSION: [18.04, 20.04, 22.04]
      - OS: alpine
        VERSION: [3.15, 3.16, 3.17]
  script:
    - echo "Building for $OS $VERSION"

# DAG 调度
build:
  stage: build
  script: echo "Building..."

test:unit:
  stage: test
  script: echo "Unit tests..."
  needs: [build]

test:integration:
  stage: test
  script: echo "Integration tests..."
  needs: [build]

deploy:
  stage: deploy
  script: echo "Deploying..."
  needs:
    - test:unit
    - test:integration

4.4 GitHub Actions 实战

4.4.1 工作流配置

# .github/workflows/ci.yml
name: CI Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  workflow_dispatch:

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

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
    
    steps:
      - name: Checkout
        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=sha,prefix=
      
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  test:
    runs-on: ubuntu-latest
    needs: build
    
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
          POSTGRES_DB: test
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
      
      redis:
        image: redis:7
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        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: Run unit tests
        run: npm run test:unit -- --coverage
      
      - name: Run integration tests
        env:
          DATABASE_URL: postgresql://test:test@localhost:5432/test
          REDIS_URL: redis://localhost:6379
        run: npm run test:integration
      
      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info
          fail_ci_if_error: true

  security:
    runs-on: ubuntu-latest
    needs: build
    
    permissions:
      security-events: write
      contents: read
    
    steps:
      - uses: actions/checkout@v4
      
      - 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 results to GitHub Security tab
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: 'trivy-results.sarif'
      
      - name: Run npm audit
        run: npm audit --audit-level=moderate

  deploy:
    runs-on: ubuntu-latest
    needs: [build, test, security]
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Deploy to Kubernetes
        uses: azure/k8s-deploy@v4
        with:
          manifests: |
            manifests/deployment.yaml
            manifests/service.yaml
          images: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
          namespace: production

4.4.2 自定义 Action

# action.yml
name: 'Deploy to Kubernetes'
description: 'Deploy application to Kubernetes cluster'
inputs:
  namespace:
    description: 'Kubernetes namespace'
    required: true
    default: 'default'
  image:
    description: 'Docker image to deploy'
    required: true
  rollout-status:
    description: 'Wait for rollout to complete'
    required: false
    default: 'true'
runs:
  using: 'composite'
  steps:
    - name: Set up kubectl
      uses: azure/setup-kubectl@v3
      
    - name: Configure kubectl
      run: |
        echo "${{ inputs.kubeconfig }}" | base64 -d > kubeconfig
        export KUBECONFIG=kubeconfig
        
    - name: Deploy
      run: |
        kubectl set image deployment/myapp app=${{ inputs.image }} -n ${{ inputs.namespace }}
        
    - name: Wait for rollout
      if: inputs.rollout-status == 'true'
      run: |
        kubectl rollout status deployment/myapp -n ${{ inputs.namespace }}

4.5 ArgoCD 实战

4.5.1 Application 定义

# application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp-production
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: production
  
  source:
    repoURL: https://github.com/myorg/myapp.git
    targetRevision: main
    path: k8s/overlays/production
    kustomize:
      images:
        - myapp=myapp:v1.2.3
  
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
      allowEmpty: false
    syncOptions:
      - CreateNamespace=true
      - PrunePropagationPolicy=foreground
      - PruneLast=true
    retry:
      limit: 5
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 3m
  
  ignoreDifferences:
    - group: apps
      kind: Deployment
      jsonPointers:
        - /spec/replicas
  
  revisionHistoryLimit: 10

4.5.2 ApplicationSet 批量部署

# applicationset.yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: myapp-clusters
  namespace: argocd
spec:
  generators:
    - clusters:
        selector:
          matchLabels:
            environment: production
  
  template:
    metadata:
      name: 'myapp-{{name}}'
    spec:
      project: production
      source:
        repoURL: https://github.com/myorg/myapp.git
        targetRevision: main
        path: k8s/base
        helm:
          valueFiles:
            - values-{{name}}.yaml
          parameters:
            - name: image.tag
              value: v1.2.3
      destination:
        server: '{{server}}'
        namespace: myapp
      syncPolicy:
        automated:
          prune: true
          selfHeal: true

4.6 CI/CD 最佳实践

4.6.1 流水线设计原则

原则 说明
快速反馈 失败的构建应该在 10 分钟内通知开发者
幂等性 流水线可以被安全地重复执行
并行执行 独立的阶段应该并行运行
缓存依赖 减少重复下载依赖的时间
失败隔离 早期阶段失败应阻止后续阶段

4.6.2 安全最佳实践

# 安全配置示例
environment:
  # 使用 Secret 管理敏感信息
  DATABASE_URL: postgres://user:${DB_PASSWORD}@host/db
  API_KEY: ${{ secrets.API_KEY }}

# 不在日志中暴露敏感信息
- name: Build
  script: |
    echo "Building version ${{ github.sha }}"
    # 不要 echo 敏感变量

4.6.3 流水线性能优化

# 依赖缓存
- name: Cache node_modules
  uses: actions/cache@v3
  with:
    path: node_modules
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-npm-

# 并行矩阵
jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - name: Run tests (shard ${{ matrix.shard }})
        run: npm test -- --shard=${{ matrix.shard }}/4

4.7 本章小结

工具 适用场景 特点
Jenkins 大型企业,多项目 高度可定制,插件丰富
GitLab CI GitLab 团队 集成度高,YAML 配置
GitHub Actions GitHub 生态 免费额度高,Marketplace 丰富
ArgoCD Kubernetes 部署 GitOps 原生,声明式

📌 下一章预告

下一章我们将学习 容器化技术 Docker,包括:

  • Docker 核心概念
  • Dockerfile 编写最佳实践
  • Docker Compose 编排
  • 镜像优化与安全

💡 提示:CI/CD 流水线的设计应该遵循"快速失败"原则,尽可能早地发现并报告问题。