Skip to content
第 79 / 250 章后端⏱ 10 分钟阅读

第 79 章:CI/CD 流水线

学习目标

  • 掌握 GitHub Actions / GitLab CI / Jenkins 流水线
  • 实现自动化构建、测试、镜像推送、部署
  • 学会环境隔离与发布策略

一、什么是 CI/CD?

缩写含义工作内容
CIContinuous Integration每次 push 自动编译、测试
CDContinuous Deployment / Delivery自动化部署到环境

二、GitHub Actions 实战

基础流水线

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

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

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: maven

      - name: Run tests
        run: mvn clean test

      - name: Generate coverage report
        run: mvn jacoco:report

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./target/site/jacoco/jacoco.xml

  build:
    needs: test                                    # ① 依赖 test 任务
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'            # ② 只在 main 分支执行

    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: maven

      - name: Build with Maven
        run: mvn clean package -DskipTests

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Aliyun Container Registry
        uses: docker/login-action@v3
        with:
          registry: registry.cn-hangzhou.aliyuncs.com
          username: ${{ secrets.ALIYUN_USERNAME }}
          password: ${{ secrets.ALIYUN_PASSWORD }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            registry.cn-hangzhou.aliyuncs.com/taskflow/app:latest
            registry.cn-hangzhou.aliyuncs.com/taskflow/app:${{ github.sha }}
          cache-from: type=gha                    # ③ GitHub Actions 缓存
          cache-to: type=gha,mode=max

多环境部署

yaml
# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]
    tags: ['v*']

jobs:
  deploy-dev:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: dev                              # ① GitHub Environment
    steps:
      - name: Deploy to Dev
        run: |
          echo "Deploying to dev..."
          kubectl set image deployment/taskflow \
            app=registry.cn-hangzhou.aliyuncs.com/taskflow/app:${{ github.sha }} \
            -n taskflow-dev

  deploy-prod:
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    environment: production                        # ② 需要手动审批
    steps:
      - name: Deploy to Production
        run: |
          echo "Deploying to production with tag ${{ github.ref_name }}"

配置密钥

GitHub 仓库 → Settings → Secrets → 添加:

  • ALIYUN_USERNAME
  • ALIYUN_PASSWORD
  • KUBE_CONFIG(K8s 配置文件内容)

三、GitLab CI 实战

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

variables:
  MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
  DOCKER_REGISTRY: registry.cn-hangzhou.aliyuncs.com

cache:
  paths:
    - .m2/repository
    - target/

maven-test:
  stage: test
  image: maven:3.9-eclipse-temurin-17
  script:
    - mvn clean test
  artifacts:
    paths:
      - target/site/jacoco/
    expire_in: 7 days

docker-build:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  before_script:
    - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin $DOCKER_REGISTRY
  script:
    - mvn clean package -DskipTests
    - docker build -t $DOCKER_REGISTRY/taskflow/app:$CI_COMMIT_SHA .
    - docker push $DOCKER_REGISTRY/taskflow/app:$CI_COMMIT_SHA
  only:
    - main
    - tags

deploy-dev:
  stage: deploy
  image: bitnami/kubectl:latest
  script:
    - kubectl config set-cluster k8s --server="$KUBE_SERVER" --certificate-authority="$KUBE_CA" --embed-certs=true
    - kubectl config set-credentials gitlab --token="$KUBE_TOKEN"
    - kubectl config set-context default --cluster=k8s --user=gitlab --namespace=taskflow-dev
    - kubectl config use-context default
    - kubectl set image deployment/taskflow app=$DOCKER_REGISTRY/taskflow/app:$CI_COMMIT_SHA -n taskflow-dev
  environment:
    name: dev
    url: https://dev.taskflow.com
  only:
    - main

四、Jenkins Pipeline(自建 CI)

groovy
// Jenkinsfile
pipeline {
    agent any

    options {
        timestamps()
        timeout(time: 30, unit: 'MINUTES')
        buildDiscarder(logRotator(numToKeepStr: '10'))
    }

    environment {
        DOCKER_REGISTRY = 'registry.cn-hangzhou.aliyuncs.com'
        IMAGE_NAME = 'taskflow/app'
        IMAGE_TAG = "${BUILD_NUMBER}"
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Test') {
            steps {
                sh 'mvn clean test'
            }
            post {
                always {
                    junit 'target/surefire-reports/*.xml'
                    jacoco(execPattern: 'target/jacoco.exec')
                }
            }
        }

        stage('Build') {
            steps {
                sh 'mvn clean package -DskipTests'
            }
        }

        stage('Build & Push Image') {
            steps {
                script {
                    docker.withRegistry("https://${DOCKER_REGISTRY}", 'aliyun-creds') {
                        def image = docker.build("${DOCKER_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}")
                        image.push()
                        image.push('latest')
                    }
                }
            }
        }

        stage('Deploy to Dev') {
            when {
                branch 'main'
            }
            steps {
                script {
                    sh """
                        kubectl set image deployment/taskflow \
                            app=${DOCKER_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} \
                            -n taskflow-dev
                    """
                }
            }
        }

        stage('Deploy to Prod') {
            when {
                buildingTag()
            }
            steps {
                input 'Deploy to Production?'           // 手动审批
                script {
                    sh """
                        kubectl set image deployment/taskflow \
                            app=${DOCKER_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} \
                            -n taskflow-prod
                    """
                }
            }
        }
    }

    post {
        success {
            echo 'Pipeline succeeded!'
        }
        failure {
            echo 'Pipeline failed!'
        }
    }
}

五、部署策略

1. 蓝绿部署(Blue-Green)

bash
# K8s 实现
kubectl apply -f deployment-v2-green.yaml
# 等新版本就绪
kubectl wait --for=condition=ready pod -l app=taskflow,version=v2
# 切换 Service 指向
kubectl patch service taskflow -p '{"spec":{"selector":{"version":"v2"}}}'
# 保留 Blue 环境,验证后删除

2. 滚动更新(Rolling Update,默认)

yaml
apiVersion: apps/v1
kind: Deployment
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 25%             # 最多比期望多 25%
      maxUnavailable: 25%       # 最多不可用 25%

3. 金丝雀发布(Canary)

yaml
# 旧版本:9 个副本
# 新版本:1 个副本
# 验证后逐步调比例 1/9 → 3/7 → 5/5 → 7/3 → 10/0

4. A/B 测试

yaml
# 根据 Header / Cookie 路由不同版本
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: taskflow
spec:
  http:
    - match:
        - headers:
            x-canary:
              exact: "true"
      route:
        - destination:
            host: taskflow
            subset: v2
    - route:
        - destination:
            host: taskflow
            subset: v1

六、环境隔离

yaml
# K8s 命名空间
apiVersion: v1
kind: Namespace
metadata:
  name: taskflow-dev

---
apiVersion: v1
kind: Namespace
metadata:
  name: taskflow-staging

---
apiVersion: v1
kind: Namespace
metadata:
  name: taskflow-prod
yaml
# 不同环境不同配置
apiVersion: v1
kind: ConfigMap
metadata:
  name: taskflow-config
  namespace: taskflow-prod
data:
  SPRING_PROFILES_ACTIVE: prod
  DB_HOST: mysql-prod
  LOG_LEVEL: WARN

七、流水线优化

1. 缓存依赖

yaml
# GitHub Actions
- uses: actions/setup-java@v3
  with:
    java-version: '17'
    cache: maven              # 自动缓存 ~/.m2

# GitLab CI
cache:
  paths:
    - .m2/repository

# Jenkins
options {
    // 缓存目录
}

2. 并行执行

yaml
jobs:
  test:
    strategy:
      matrix:
        java-version: [17, 21]
        test-type: [unit, integration]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v3
        with:
          java-version: ${{ matrix.java-version }}
      - run: mvn test -P${{ matrix.test-type }}

3. 失败快速反馈

yaml
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - run: mvn checkstyle:check
  test:
    needs: lint               # ① lint 不过不跑测试

八、安全扫描

yaml
# 在流水线中加入安全检查
- name: OWASP Dependency Check
  uses: dependency-check/dependency-check-action@main
  with:
    projectName: taskflow
    path: '.'
    format: 'HTML'
    args: >
      --failOnCVSS 7
      --enableRetired

- name: Trivy Image Scan
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: ${{ env.IMAGE }}
    format: 'table'
    exit-code: '1'
    ignore-unfixed: true
    vuln-type: 'os,library'
    severity: 'CRITICAL,HIGH'

九、最佳实践

实践说明
每个 PR 跑流水线防止合并坏代码
主分支保护必须 Code Review + 流水线通过
构建一次,多处部署一个镜像推多环境
镜像标签用 Git SHA可追溯、可回滚
生产部署手动审批重要操作需人工确认
失败快速反馈先 lint、单元测试,慢测试后面
并行执行利用矩阵并发
密钥用 Secret绝不硬编码
流水线即代码Jenkinsfile / yml 进 Git

十、本章小结

要点关键
CI每次 push 自动编译 + 测试
CD自动化部署到环境
GitHub Actions云端、轻量、YAML 配置
GitLab CI内置、与 GitLab 深度集成
Jenkins自建、灵活、插件丰富
部署策略蓝绿 / 滚动 / 金丝雀 / A/B
环境隔离命名空间 / 配置文件
流水线优化缓存 + 并行 + 失败快速

动手练习

练习 1:基础题

为你的项目配置 GitHub Actions:每次 push 自动跑单元测试,main 分支自动构建并推送 Docker 镜像。

练习 2:进阶题

实现完整的 CI/CD:

  • PR:跑测试 + 镜像构建
  • main merge:自动部署到 dev 环境
  • 打 tag v*..:部署到生产(手动审批)

练习 3:思考题

你的服务从开发到生产有 3 个环境。如何设计流水线,使镜像构建一次、多处部署,避免环境差异?


下一章第 80 章:阿里 Java 开发规约

本站基于 VitePress 构建 · 由 Codebook 团队维护