第 79 章:CI/CD 流水线
学习目标
- 掌握 GitHub Actions / GitLab CI / Jenkins 流水线
- 实现自动化构建、测试、镜像推送、部署
- 学会环境隔离与发布策略
一、什么是 CI/CD?
| 缩写 | 含义 | 工作内容 |
|---|---|---|
| CI | Continuous Integration | 每次 push 自动编译、测试 |
| CD | Continuous 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_USERNAMEALIYUN_PASSWORDKUBE_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/04. 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-prodyaml
# 不同环境不同配置
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 开发规约 →