CI/CD流水线集成 #

CI/CD概述 #

流程图 #

text
┌─────────────────────────────────────────────────────┐
│                   CI/CD 流程                        │
├─────────────────────────────────────────────────────┤
│                                                     │
│  代码提交 ──→ 构建 ──→ 测试 ──→ 镜像构建 ──→ 部署  │
│                                                     │
│  ┌─────┐    ┌─────┐   ┌─────┐   ┌─────┐   ┌─────┐ │
│  │ Git │───→│Build│──→│Test │──→│Image│──→│Deploy│ │
│  └─────┘    └─────┘   └─────┘   └─────┘   └─────┘ │
│                                                     │
│  持续集成(CI)              持续部署(CD)            │
│                                                     │
└─────────────────────────────────────────────────────┘

GitLab CI/CD #

.gitlab-ci.yml #

yaml
stages:
  - build
  - test
  - package
  - deploy

variables:
  IMAGE_NAME: registry.example.com/myapp
  IMAGE_TAG: $CI_COMMIT_SHA

# 构建阶段
build:
  stage: build
  image: node:18-alpine
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 hour

# 测试阶段
test:
  stage: test
  image: node:18-alpine
  script:
    - npm ci
    - npm test
  coverage: '/Coverage: \d+\.\d+/'

# 安全扫描
security-scan:
  stage: test
  image: docker:latest
  services:
    - docker:dind
  script:
    - docker build -t $IMAGE_NAME:$IMAGE_TAG .
    - docker run --rm -v /var/run/docker.sock:/var/run/docker.sock
      aquasec/trivy:latest image --exit-code 1 --severity HIGH,CRITICAL $IMAGE_NAME:$IMAGE_TAG

# 镜像构建
docker-build:
  stage: package
  image: docker:latest
  services:
    - docker:dind
  before_script:
    - docker login -u $REGISTRY_USER -p $REGISTRY_PASSWORD registry.example.com
  script:
    - docker build -t $IMAGE_NAME:$IMAGE_TAG .
    - docker push $IMAGE_NAME:$IMAGE_TAG
    - |
      if [ "$CI_COMMIT_BRANCH" == "main" ]; then
        docker tag $IMAGE_NAME:$IMAGE_TAG $IMAGE_NAME:latest
        docker push $IMAGE_NAME:latest
      fi

# 部署到开发环境
deploy-dev:
  stage: deploy
  image: docker:latest
  services:
    - docker:dind
  script:
    - docker login -u $REGISTRY_USER -p $REGISTRY_PASSWORD registry.example.com
    - docker pull $IMAGE_NAME:$IMAGE_TAG
    - docker tag $IMAGE_NAME:$IMAGE_TAG $IMAGE_NAME:dev
    - docker push $IMAGE_NAME:dev
    - ssh deploy@dev.example.com "cd /app && docker-compose pull && docker-compose up -d"
  environment:
    name: development
    url: https://dev.example.com
  only:
    - develop

# 部署到生产环境
deploy-prod:
  stage: deploy
  image: docker:latest
  services:
    - docker:dind
  script:
    - docker login -u $REGISTRY_USER -p $REGISTRY_PASSWORD registry.example.com
    - docker pull $IMAGE_NAME:$IMAGE_TAG
    - docker tag $IMAGE_NAME:$IMAGE_TAG $IMAGE_NAME:prod
    - docker push $IMAGE_NAME:prod
    - ssh deploy@prod.example.com "cd /app && docker-compose pull && docker-compose up -d"
  environment:
    name: production
    url: https://www.example.com
  only:
    - main
  when: manual

GitHub Actions #

.github/workflows/docker.yml #

yaml
name: Docker CI/CD

on:
  push:
    branches: [main, develop]
    tags: ['v*']
  pull_request:
    branches: [main]

env:
  REGISTRY: registry.example.com
  IMAGE_NAME: myapp

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Build
        run: npm run build
      
      - name: Test
        run: npm test
      
      - name: Upload artifacts
        uses: actions/upload-artifact@v3
        with:
          name: dist
          path: dist/

  security-scan:
    runs-on: ubuntu-latest
    needs: build
    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: 'table'
          exit-code: '1'
          severity: 'CRITICAL,HIGH'

  docker-build:
    runs-on: ubuntu-latest
    needs: build
    steps:
      - uses: actions/checkout@v3
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2
      
      - name: Login to Registry
        uses: docker/login-action@v2
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ secrets.REGISTRY_USER }}
          password: ${{ secrets.REGISTRY_PASSWORD }}
      
      - name: Build and push
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
          cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:cache
          cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:cache

  deploy-dev:
    runs-on: ubuntu-latest
    needs: docker-build
    if: github.ref == 'refs/heads/develop'
    steps:
      - name: Deploy to dev
        run: |
          ssh deploy@dev.example.com "cd /app && docker-compose pull && docker-compose up -d"

  deploy-prod:
    runs-on: ubuntu-latest
    needs: docker-build
    if: startsWith(github.ref, 'refs/tags/v')
    steps:
      - name: Deploy to prod
        run: |
          ssh deploy@prod.example.com "cd /app && docker-compose pull && docker-compose up -d"

Jenkins Pipeline #

Jenkinsfile #

groovy
pipeline {
    agent any
    
    environment {
        IMAGE_NAME = 'registry.example.com/myapp'
        IMAGE_TAG = "${env.BUILD_ID}"
    }
    
    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }
        
        stage('Build') {
            steps {
                sh 'npm ci'
                sh 'npm run build'
            }
        }
        
        stage('Test') {
            steps {
                sh 'npm test'
            }
            post {
                always {
                    junit 'test-results/*.xml'
                    publishCoverage adapters: [coberturaAdapter('coverage/*.xml')]
                }
            }
        }
        
        stage('Security Scan') {
            steps {
                script {
                    sh 'docker build -t ${IMAGE_NAME}:${IMAGE_TAG} .'
                    sh 'docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy:latest image --exit-code 1 --severity HIGH,CRITICAL ${IMAGE_NAME}:${IMAGE_TAG}'
                }
            }
        }
        
        stage('Docker Build & Push') {
            steps {
                script {
                    docker.withRegistry('https://registry.example.com', 'registry-credentials') {
                        def app = docker.build("${IMAGE_NAME}:${IMAGE_TAG}")
                        app.push()
                        if (env.BRANCH_NAME == 'main') {
                            app.push('latest')
                        }
                    }
                }
            }
        }
        
        stage('Deploy to Dev') {
            when {
                branch 'develop'
            }
            steps {
                sh 'ssh deploy@dev.example.com "cd /app && docker-compose pull && docker-compose up -d"'
            }
        }
        
        stage('Deploy to Prod') {
            when {
                branch 'main'
            }
            steps {
                input 'Deploy to production?'
                sh 'ssh deploy@prod.example.com "cd /app && docker-compose pull && docker-compose up -d"'
            }
        }
    }
    
    post {
        always {
            cleanWs()
        }
        success {
            slackSend(color: 'good', message: "Build ${env.BUILD_ID} succeeded!")
        }
        failure {
            slackSend(color: 'danger', message: "Build ${env.BUILD_ID} failed!")
        }
    }
}

部署策略 #

滚动更新 #

yaml
# docker-compose.yml
services:
  app:
    image: myapp:v1.0
    deploy:
      update_config:
        parallelism: 2
        delay: 10s
        failure_action: rollback
      replicas: 4

蓝绿部署 #

bash
# 当前运行蓝色环境
docker-compose -f docker-compose.blue.yml up -d

# 部署绿色环境
docker-compose -f docker-compose.green.yml up -d

# 切换流量
# 更新负载均衡配置

# 停止蓝色环境
docker-compose -f docker-compose.blue.yml down

金丝雀发布 #

yaml
# docker-compose.yml
services:
  app-v1:
    image: myapp:v1.0
    deploy:
      replicas: 9
      
  app-v2:
    image: myapp:v2.0
    deploy:
      replicas: 1

小结 #

本节学习了CI/CD流水线集成:

  • GitLab CI/CD配置
  • GitHub Actions配置
  • Jenkins Pipeline配置
  • 部署策略选择

下一步 #

接下来,让我们学习 生产环境最佳实践,了解生产环境的部署经验。