CI/CD 实践
Java 项目的持续集成与持续部署。
目录
CI/CD 基础
什么是 CI/CD
CI/CD 是持续集成(Continuous Integration)、持续交付(Continuous Delivery)和持续部署(Continuous Deployment)的合称,是现代软件开发中最重要的工程实践之一。
持续集成(CI) 指开发人员频繁地将代码变更合并到主干分支,每次合并都会触发自动化构建和测试流程。其核心理念是让问题尽早暴露,避免"集成地狱"。
持续交付(CD) 在 CI 的基础上,确保代码随时可以部署到生产环境。每次通过 CI 的代码变更都会自动部署到类生产环境,经过验收测试后,可以一键发布到生产。
持续部署(CD) 是持续交付的延伸,将通过自动化测试的代码变更自动部署到生产环境,无需人工干预。
CI/CD 的核心价值
- 快速反馈:提交代码后几分钟内就能知道是否引入了问题
- 降低风险:小批量变更频繁集成,问题定位范围小
- 减少手工操作:自动化构建、测试、部署,减少人为失误
- 提高交付频率:从数月一次发布到一天多次发布
- 可重复的构建过程:构建环境标准化,消除"在我机器上能运行"的问题
Java 项目 CI/CD 全流程
一个典型的 Java 项目 CI/CD 流程包含以下阶段:
阶段详解:
| 阶段 | 工具/技术 | 说明 |
|---|---|---|
| 代码提交 | Git | 开发者推送代码到远程仓库 |
| 代码检查 | SonarQube / Checkstyle / PMD | 静态代码分析,检查代码质量和规范 |
| 单元测试 | JUnit / TestNG / Mockito | 运行单元测试,验证代码逻辑 |
| 编译打包 | Maven / Gradle | 编译源码并打包为 JAR/WAR |
| 构建镜像 | Docker / Jib | 将应用打包为 Docker 镜像 |
| 部署测试环境 | kubectl / Helm / Ansible | 部署到测试或预发布环境 |
| 集成测试 | REST Assured / Selenium / Testcontainers | 运行集成测试和端到端测试 |
| 部署生产 | Spinnaker / ArgoCD / Jenkins | 发布到生产环境 |
CI/CD 工具生态
CI/CD 平台:
- Jenkins:老牌 CI/CD 工具,插件丰富,Pipeline as Code
- GitHub Actions:与 GitHub 深度集成,YAML 配置,生态活跃
- GitLab CI:与 GitLab 深度集成,功能强大
- CircleCI:云端 CI 服务,速度较快
- Azure DevOps:微软提供的 DevOps 全流程工具
- Drone:轻量级容器化 CI/CD 工具
构建工具:
- Maven:约定优于配置,XML 配置,生态成熟
- Gradle:灵活性高,Groovy/Kotlin DSL,性能优于 Maven
质量工具:
- SonarQube:代码质量与安全分析
- JaCoCo:代码覆盖率统计
- Checkstyle:代码风格检查
- PMD:潜在缺陷检测
- SpotBugs:字节码级别缺陷检测
GitHub Actions
基本概念
GitHub Actions 是 GitHub 内置的 CI/CD 引擎,使用 YAML 文件定义工作流。
核心概念:
| 概念 | 说明 |
|---|---|
| Workflow | 一个完整的自动化流程,定义在 .github/workflows/ 目录下的 YAML 文件中 |
| Event | 触发 Workflow 的事件,如 push、pull_request、schedule |
| Job | Workflow 中的一个任务单元,包含多个 Step,默认并行执行 |
| Step | Job 中的一个步骤,可以是运行命令或使用 Action |
| Action | 可复用的工作单元,可以从 Marketplace 获取或自定义 |
| Runner | 运行 Workflow 的服务器,GitHub 托管或自托管 |
Workflow 基本结构
name: Java CI
# 触发条件
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
# 环境变量
env:
JAVA_VERSION: '17'
# 任务定义
jobs:
build:
# 运行环境
runs-on: ubuntu-latest
# 步骤
steps:
- uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
- name: Build with Maven
run: mvn clean verifyname: Java CI
# 触发条件
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
# 环境变量
env:
JAVA_VERSION: '17'
# 任务定义
jobs:
build:
# 运行环境
runs-on: ubuntu-latest
# 步骤
steps:
- uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
- name: Build with Maven
run: mvn clean verifyMaven 构建工作流
一个完整的 Maven 项目 CI 工作流示例:
name: Maven CI
on:
push:
branches: [main, develop, feature/**]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Build and test
run: mvn clean verify -B
- name: Upload test reports
if: always()
uses: actions/upload-artifact@v4
with:
name: test-reports
path: '**/target/surefire-reports/*.xml'
- name: Upload JAR artifact
uses: actions/upload-artifact@v4
with:
name: application-jar
path: target/*.jarname: Maven CI
on:
push:
branches: [main, develop, feature/**]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Build and test
run: mvn clean verify -B
- name: Upload test reports
if: always()
uses: actions/upload-artifact@v4
with:
name: test-reports
path: '**/target/surefire-reports/*.xml'
- name: Upload JAR artifact
uses: actions/upload-artifact@v4
with:
name: application-jar
path: target/*.jarMaven 缓存优化:
GitHub Actions 的内置 setup-java Action 已经支持 Maven 依赖缓存。设置 cache: maven 后会自动缓存 ~/.m2/repository 目录,将后续构建的时间从几分钟缩短到几十秒。
缓存策略说明:
- 缓存键基于
pom.xml的哈希值生成 - 如果
pom.xml发生变化,缓存会失效并重建 - 命中缓存时跳过依赖下载,大幅加速构建
Gradle 构建工作流
name: Gradle CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: gradle
- name: Grant execute permission for Gradle wrapper
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew clean build
- name: Run tests
run: ./gradlew test
- name: Publish Test Report
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: build/reports/tests/name: Gradle CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: gradle
- name: Grant execute permission for Gradle wrapper
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew clean build
- name: Run tests
run: ./gradlew test
- name: Publish Test Report
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: build/reports/tests/Gradle Wrapper 最佳实践:
- 将
gradle-wrapper.jar和gradlew提交到代码仓库 - 这样 CI 环境无需预先安装 Gradle
- 确保所有开发者使用相同的 Gradle 版本
- GitHub Actions 中通过
cache: gradle缓存~/.gradle/caches目录
多 JDK 版本矩阵测试
为了确保应用在不同 JDK 版本上的兼容性,可以使用矩阵策略并行测试:
name: Multi-JDK Test
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
java: [11, 17, 21]
distribution: [temurin, zulu]
steps:
- uses: actions/checkout@v4
- name: Set up JDK ${{ matrix.java }} (${{ matrix.distribution }})
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java }}
distribution: ${{ matrix.distribution }}
cache: maven
- name: Run tests
run: mvn clean test
- name: Check Java version
run: java -versionname: Multi-JDK Test
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
java: [11, 17, 21]
distribution: [temurin, zulu]
steps:
- uses: actions/checkout@v4
- name: Set up JDK ${{ matrix.java }} (${{ matrix.distribution }})
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java }}
distribution: ${{ matrix.distribution }}
cache: maven
- name: Run tests
run: mvn clean test
- name: Check Java version
run: java -version矩阵策略高级用法:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
java: [17, 21]
spring-boot: [3.1.x, 3.2.x, 3.3.x]
exclude:
# 排除不需要的组合
- java: 17
spring-boot: 3.3.x
steps:
- uses: actions/checkout@v4
- name: Set up JDK ${{ matrix.java }}
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java }}
distribution: 'temurin'
cache: maven
- name: Test with Spring Boot ${{ matrix.spring-boot }}
run: mvn test -Dspring-boot.version=${{ matrix.spring-boot }}jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
java: [17, 21]
spring-boot: [3.1.x, 3.2.x, 3.3.x]
exclude:
# 排除不需要的组合
- java: 17
spring-boot: 3.3.x
steps:
- uses: actions/checkout@v4
- name: Set up JDK ${{ matrix.java }}
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java }}
distribution: 'temurin'
cache: maven
- name: Test with Spring Boot ${{ matrix.spring-boot }}
run: mvn test -Dspring-boot.version=${{ matrix.spring-boot }}SonarQube 代码分析
集成 SonarQube 进行代码质量和安全分析:
name: SonarQube Analysis
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
sonar:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # SonarQube 需要完整 git 历史
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Cache SonarQube packages
uses: actions/cache@v4
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: SonarQube Analysis
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
run: mvn verify sonar:sonar \
-Dsonar.token=${{ secrets.SONAR_TOKEN }} \
-Dsonar.host.url=${{ secrets.SONAR_HOST_URL }} \
-Dsonar.qualitygate.wait=truename: SonarQube Analysis
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
sonar:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # SonarQube 需要完整 git 历史
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Cache SonarQube packages
uses: actions/cache@v4
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: SonarQube Analysis
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
run: mvn verify sonar:sonar \
-Dsonar.token=${{ secrets.SONAR_TOKEN }} \
-Dsonar.host.url=${{ secrets.SONAR_HOST_URL }} \
-Dsonar.qualitygate.wait=trueSonarQube 质量门禁:
质量门禁(Quality Gate)是 SonarQube 的核心功能,在 CI 管道中用于判断代码质量是否达标。常见门禁指标:
| 指标 | 阈值 | 说明 |
|---|---|---|
| 可靠性评级 | A | 没有 Bug |
| 安全评级 | A | 没有安全漏洞 |
| 可维护性评级 | A | 代码异味在可接受范围 |
| 覆盖率 | >= 80% | 单元测试覆盖率 |
| 重复率 | <= 3% | 代码重复率 |
设置 sonar.qualitygate.wait=true 会让构建等待 SonarQube 质量门禁结果,未通过则构建失败。
Docker 构建与推送
name: Build and Push Docker Image
on:
push:
branches: [main]
tags: ['v*']
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Build JAR
run: mvn clean package -DskipTests
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: your-dockerhub-user/app-name
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=ref,event=branch
type=sha,format=long
- name: Build and push
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=maxname: Build and Push Docker Image
on:
push:
branches: [main]
tags: ['v*']
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Build JAR
run: mvn clean package -DskipTests
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: your-dockerhub-user/app-name
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=ref,event=branch
type=sha,format=long
- name: Build and push
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镜像标签策略最佳实践:
v1.2.3— 语义化版本标签,用于正式发布v1.2— 次要版本标签,指向最新补丁版本main— 分支标签,用于开发环境sha-xxxxxx— 唯一标识,用于回滚定位latest— 谨慎使用,容易造成混淆
部署到服务器
通过 SSH 将应用部署到远程服务器:
name: Deploy to Server
on:
workflow_run:
workflows: ['Build and Push Docker Image']
types:
- completed
workflow_dispatch: # 支持手动触发
jobs:
deploy:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.DEPLOY_HOST }}
port: ${{ secrets.DEPLOY_PORT }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
script: |
cd /opt/app
docker-compose pull
docker-compose up -d --remove-orphans
docker system prune -f
- name: Health check
run: |
sleep 10
curl -f http://${{ secrets.DEPLOY_HOST }}:8080/actuator/healthname: Deploy to Server
on:
workflow_run:
workflows: ['Build and Push Docker Image']
types:
- completed
workflow_dispatch: # 支持手动触发
jobs:
deploy:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.DEPLOY_HOST }}
port: ${{ secrets.DEPLOY_PORT }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
script: |
cd /opt/app
docker-compose pull
docker-compose up -d --remove-orphans
docker system prune -f
- name: Health check
run: |
sleep 10
curl -f http://${{ secrets.DEPLOY_HOST }}:8080/actuator/healthSSH 部署安全注意事项:
- 使用 SSH 私钥认证,而非密码
- 私钥存储为 GitHub Secrets
- 使用专用的部署用户,权限最小化
- 配置 SSH 免密 sudo 执行特定命令
- 考虑使用 Bastion 主机作为跳板机
GitHub Actions 进阶技巧
工作流复用:
# .github/workflows/maven-build.yml (可复用的工作流)
on:
workflow_call:
inputs:
java-version:
required: true
type: string
secrets:
SONAR_TOKEN:
required: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: ${{ inputs.java-version }}
distribution: 'temurin'
- run: mvn clean verify# .github/workflows/maven-build.yml (可复用的工作流)
on:
workflow_call:
inputs:
java-version:
required: true
type: string
secrets:
SONAR_TOKEN:
required: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: ${{ inputs.java-version }}
distribution: 'temurin'
- run: mvn clean verify# 在另一个工作流中调用
jobs:
call-build:
uses: ./.github/workflows/maven-build.yml
with:
java-version: '17'
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}# 在另一个工作流中调用
jobs:
call-build:
uses: ./.github/workflows/maven-build.yml
with:
java-version: '17'
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}条件执行:
steps:
- name: Run on main branch only
if: github.ref == 'refs/heads/main'
run: echo "Deploying to production"
- name: Run on PRs
if: github.event_name == 'pull_request'
run: echo "Running PR checks"
- name: Run on tag push
if: startsWith(github.ref, 'refs/tags/v')
run: echo "Releasing version"
- name: Always run, even if previous steps fail
if: always()
run: echo "Cleanup"
- name: Run only on failure
if: failure()
run: echo "Send notification"steps:
- name: Run on main branch only
if: github.ref == 'refs/heads/main'
run: echo "Deploying to production"
- name: Run on PRs
if: github.event_name == 'pull_request'
run: echo "Running PR checks"
- name: Run on tag push
if: startsWith(github.ref, 'refs/tags/v')
run: echo "Releasing version"
- name: Always run, even if previous steps fail
if: always()
run: echo "Cleanup"
- name: Run only on failure
if: failure()
run: echo "Send notification"手动审批环境:
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment:
name: staging
steps:
- run: echo "Deploy to staging"
deploy-prod:
needs: [deploy-staging]
runs-on: ubuntu-latest
environment:
name: production
url: https://your-app.com
steps:
- run: echo "Deploy to production"jobs:
deploy-staging:
runs-on: ubuntu-latest
environment:
name: staging
steps:
- run: echo "Deploy to staging"
deploy-prod:
needs: [deploy-staging]
runs-on: ubuntu-latest
environment:
name: production
url: https://your-app.com
steps:
- run: echo "Deploy to production"通过在 GitHub 仓库 Settings > Environments 中配置 production 环境的 Required reviewers,可以在部署生产前添加人工审批环节。
Docker 多阶段构建
为什么需要多阶段构建
传统的 Java 应用 Docker 构建面临两个问题:
- 镜像体积过大:Maven 镜像通常超过 200MB,JDK 镜像超过 300MB,构建产物中包含了编译工具和依赖,这些在生产运行时完全不需要
- 构建产物不一致:本地环境和 CI 环境使用不同版本的构建工具,可能导致构建结果不同
多阶段构建(Multi-stage Build)通过在 Dockerfile 中使用多个 FROM 语句,将构建环境和运行环境分离,有效解决以上问题。
基础多阶段构建
# Stage 1: 构建阶段
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests
# Stage 2: 运行阶段
FROM eclipse-temurin:17-jre-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]# Stage 1: 构建阶段
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests
# Stage 2: 运行阶段
FROM eclipse-temurin:17-jre-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]构建结果对比:
| 方案 | 镜像大小 | 包含内容 |
|---|---|---|
| 单阶段构建(Maven) | ~700MB | JDK + Maven + 依赖 + 源码 + 编译产物 |
| 多阶段构建(JRE) | ~180MB | JRE + 编译产物 |
| 多阶段 + Alpine | ~120MB | JRE 精简版 + 编译产物 |
| 多阶段 + Distroless | ~100MB | 运行时所需的最小文件 |
瘦 JAR + 外部依赖层
Spring Boot 打包的 fat JAR 包含所有依赖,导致每次代码变更都需要重新下载层缓存。可以将依赖分层,利用 Docker 的层缓存加速构建:
# Stage 1: 依赖下载阶段
FROM maven:3.9-eclipse-temurin-17 AS deps
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
# Stage 2: 构建阶段
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /app
COPY --from=deps /root/.m2 /root/.m2
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests
# Stage 3: 运行阶段 - 利用 Spring Boot 分层
FROM eclipse-temurin:17-jre-alpine AS extractor
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract
# Stage 4: 最终运行镜像
FROM eclipse-temurin:17-jre-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
WORKDIR /app
COPY --from=extractor /app/dependencies/ ./
COPY --from=extractor /app/spring-boot-loader/ ./
COPY --from=extractor /app/snapshot-dependencies/ ./
COPY --from=extractor /app/application/ ./
EXPOSE 8080
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]# Stage 1: 依赖下载阶段
FROM maven:3.9-eclipse-temurin-17 AS deps
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
# Stage 2: 构建阶段
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /app
COPY --from=deps /root/.m2 /root/.m2
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests
# Stage 3: 运行阶段 - 利用 Spring Boot 分层
FROM eclipse-temurin:17-jre-alpine AS extractor
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract
# Stage 4: 最终运行镜像
FROM eclipse-temurin:17-jre-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
WORKDIR /app
COPY --from=extractor /app/dependencies/ ./
COPY --from=extractor /app/spring-boot-loader/ ./
COPY --from=extractor /app/snapshot-dependencies/ ./
COPY --from=extractor /app/application/ ./
EXPOSE 8080
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]Spring Boot 分层说明:
Spring Boot 2.3+ 支持 JAR 分层(jarmode=layertools),将 JAR 包解压为四层:
| 层 | 内容 | 变化频率 | 缓存策略 |
|---|---|---|---|
dependencies | 项目依赖的第三方 JAR | 低(修改 pom.xml 时) | 长期缓存 |
spring-boot-loader | Spring Boot 加载器类 | 极低(升级 Boot 版本时) | 几乎不变 |
snapshot-dependencies | SNAPSHOT 版本的依赖 | 中 | 按需缓存 |
application | 应用代码和资源 | 高(每次代码变更) | 频繁更新 |
利用分层后,大部分时候只需要重新构建 application 层,前面三层可以直接使用缓存,极大加速 CI 构建。
Jib 构建
Google 的 Jib 工具无需 Dockerfile,直接通过 Maven/Gradle 插件构建优化过的 Docker 镜像:
Maven 配置:
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.4.0</version>
<configuration>
<from>
<image>eclipse-temurin:17-jre-alpine</image>
</from>
<to>
<image>your-registry/app-name</image>
<tags>
<tag>${project.version}</tag>
<tag>latest</tag>
</tags>
</to>
<container>
<jvmFlags>
<jvmFlag>-Xms512m</jvmFlag>
<jvmFlag>-Xmx512m</jvmFlag>
</jvmFlags>
<ports>
<port>8080</port>
</ports>
</container>
</configuration>
</plugin><plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.4.0</version>
<configuration>
<from>
<image>eclipse-temurin:17-jre-alpine</image>
</from>
<to>
<image>your-registry/app-name</image>
<tags>
<tag>${project.version}</tag>
<tag>latest</tag>
</tags>
</to>
<container>
<jvmFlags>
<jvmFlag>-Xms512m</jvmFlag>
<jvmFlag>-Xmx512m</jvmFlag>
</jvmFlags>
<ports>
<port>8080</port>
</ports>
</container>
</configuration>
</plugin>构建命令:
# 构建并推送到镜像仓库
mvn jib:build
# 仅构建到 Docker daemon(本地使用)
mvn jib:dockerBuild
# 构建到指定 tar 文件(离线分发)
mvn jib:buildTar# 构建并推送到镜像仓库
mvn jib:build
# 仅构建到 Docker daemon(本地使用)
mvn jib:dockerBuild
# 构建到指定 tar 文件(离线分发)
mvn jib:buildTarJib 优势:
- 无需 Docker daemon,可在任何环境中构建
- 自动分层,优化缓存
- 与 Maven/Gradle 无缝集成
- 支持多种镜像仓库认证方式
镜像优化技巧
选择更小的基础镜像:
# 标准 JDK 镜像 (~300MB)
FROM eclipse-temurin:17-jdk
# JRE 镜像 (~180MB)
FROM eclipse-temurin:17-jre
# Alpine + JRE (~120MB)
FROM eclipse-temurin:17-jre-alpine
# Distroless (~100MB)
FROM gcr.io/distroless/java17-debian12# 标准 JDK 镜像 (~300MB)
FROM eclipse-temurin:17-jdk
# JRE 镜像 (~180MB)
FROM eclipse-temurin:17-jre
# Alpine + JRE (~120MB)
FROM eclipse-temurin:17-jre-alpine
# Distroless (~100MB)
FROM gcr.io/distroless/java17-debian12Distroless 镜像特点:
- 仅包含应用程序及其运行时依赖
- 不包含包管理器、shell、或其他非必要工具
- 攻击面最小化
- 需要额外处理调试和健康检查
# 使用 Distroless 镜像
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn clean package -DskipTests
FROM gcr.io/distroless/java17-debian12
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]# 使用 Distroless 镜像
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn clean package -DskipTests
FROM gcr.io/distroless/java17-debian12
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]调试 Distroless 容器:
由于 Distroless 镜像没有 shell,调试需要使用:
# 使用 debug 镜像
FROM gcr.io/distroless/java17-debian12:debug
# 或者在运行时附加调试容器
kubectl debug -it pod-name --image=busybox --target=app-container# 使用 debug 镜像
FROM gcr.io/distroless/java17-debian12:debug
# 或者在运行时附加调试容器
kubectl debug -it pod-name --image=busybox --target=app-container使用 Spring Boot 的 AOT 编译优化:
Spring Boot 3.0+ 支持 AOT(Ahead-of-Time)编译,配合 GraalVM Native Image 可以显著减小镜像大小和启动时间:
FROM ghcr.io/graalvm/jdk:ol8-java17 AS builder
WORKDIR /app
COPY . .
RUN ./mvnw native:compile -Pnative
FROM gcr.io/distroless/java-base-debian12:latest
COPY --from=builder /app/target/app /app/app
EXPOSE 8080
ENTRYPOINT ["/app/app"]FROM ghcr.io/graalvm/jdk:ol8-java17 AS builder
WORKDIR /app
COPY . .
RUN ./mvnw native:compile -Pnative
FROM gcr.io/distroless/java-base-debian12:latest
COPY --from=builder /app/target/app /app/app
EXPOSE 8080
ENTRYPOINT ["/app/app"]Native Image 的优势:
- 启动时间从秒级降到毫秒级
- 内存占用降低 50-80%
- 镜像大小可以压缩到 50MB 以内
- 无需 JVM,直接作为系统可执行文件运行
环境管理
多环境配置
Java 项目通常需要管理多套环境配置,常用的方案:
方案一:Spring Profile
# application.yml
spring:
profiles:
active: ${SPRING_PROFILES_ACTIVE:dev}
---
# application-dev.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/dev_db
username: dev_user
password: dev_pass
---
# application-prod.yml
spring:
datasource:
url: jdbc:mysql://prod-db-host:3306/prod_db
username: ${DB_USERNAME}
password: ${DB_PASSWORD}# application.yml
spring:
profiles:
active: ${SPRING_PROFILES_ACTIVE:dev}
---
# application-dev.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/dev_db
username: dev_user
password: dev_pass
---
# application-prod.yml
spring:
datasource:
url: jdbc:mysql://prod-db-host:3306/prod_db
username: ${DB_USERNAME}
password: ${DB_PASSWORD}启动时通过环境变量指定 Profile:
java -jar app.jar --spring.profiles.active=prod
# 或
SPRING_PROFILES_ACTIVE=prod java -jar app.jarjava -jar app.jar --spring.profiles.active=prod
# 或
SPRING_PROFILES_ACTIVE=prod java -jar app.jar方案二:Maven Profile + 资源过滤
<profiles>
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<build.profile.id>dev</build.profile.id>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<build.profile.id>prod</build.profile.id>
</properties>
</profile>
</profiles><profiles>
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<build.profile.id>dev</build.profile.id>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<build.profile.id>prod</build.profile.id>
</properties>
</profile>
</profiles>配合 Maven 资源过滤,在构建时替换占位符:
# application.yml (模板)
database:
url: jdbc:mysql://${db.host}:3306/app_db
username: ${db.username}# application.yml (模板)
database:
url: jdbc:mysql://${db.host}:3306/app_db
username: ${db.username}# 构建不同环境的包
mvn clean package -Pdev
mvn clean package -Pprod# 构建不同环境的包
mvn clean package -Pdev
mvn clean package -Pprod方案三:外部配置中心
生产环境推荐使用配置中心统一管理配置:
| 配置中心 | 说明 |
|---|---|
| Spring Cloud Config | 基于 Git 的配置中心 |
| Alibaba Nacos | 配置注册一体化 |
| HashiCorp Consul | 服务发现 + 配置管理 |
| Apache Apollo | 携程开源的配置中心 |
CI/CD 中的配置管理
GitHub Secrets 安全管理:
敏感信息不应硬编码在代码中,应通过 CI/CD 平台提供的 Secrets 功能注入:
steps:
- name: Build with secrets
env:
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
API_KEY: ${{ secrets.API_KEY }}
MAVEN_SETTINGS: ${{ secrets.MAVEN_SETTINGS_XML }}
run: |
# 将 Maven settings 写入文件
echo "$MAVEN_SETTINGS" > ~/.m2/settings.xml
mvn clean packagesteps:
- name: Build with secrets
env:
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
API_KEY: ${{ secrets.API_KEY }}
MAVEN_SETTINGS: ${{ secrets.MAVEN_SETTINGS_XML }}
run: |
# 将 Maven settings 写入文件
echo "$MAVEN_SETTINGS" > ~/.m2/settings.xml
mvn clean package常见需要保密的配置:
- 数据库连接串和密码
- API 密钥和 Token
- JWT 签名密钥
- TLS/SSL 证书私钥
- 第三方服务的认证凭据
环境变量管理策略:
开发环境 (dev)
├─ 默认配置写在 application-dev.yml 中
├─ 敏感信息使用本地 .env 文件(不提交到 Git)
└─ CI 中通过组织级别的 Secrets 注入
测试环境 (staging)
├─ 使用与生产环境尽可能一致的配置
├─ 数据使用脱敏后的生产数据副本
└─ 所有秘密通过 GitHub Environments Secrets 管理
生产环境 (prod)
├─ 所有配置通过环境变量或配置中心注入
├─ 不同实例使用不同的密钥
└─ 开启自动轮转机制开发环境 (dev)
├─ 默认配置写在 application-dev.yml 中
├─ 敏感信息使用本地 .env 文件(不提交到 Git)
└─ CI 中通过组织级别的 Secrets 注入
测试环境 (staging)
├─ 使用与生产环境尽可能一致的配置
├─ 数据使用脱敏后的生产数据副本
└─ 所有秘密通过 GitHub Environments Secrets 管理
生产环境 (prod)
├─ 所有配置通过环境变量或配置中心注入
├─ 不同实例使用不同的密钥
└─ 开启自动轮转机制数据库迁移的 CI/CD 处理
Flyway 是 Java 生态中最流行的数据库迁移工具,它使用版本化的 SQL 脚本管理数据库 schema 变更。
Flyway 集成 Maven:
<plugin>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-maven-plugin</artifactId>
<version>9.22.0</version>
<configuration>
<url>${flyway.url}</url>
<user>${flyway.user}</user>
<password>${flyway.password}</password>
</configuration>
</plugin><plugin>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-maven-plugin</artifactId>
<version>9.22.0</version>
<configuration>
<url>${flyway.url}</url>
<user>${flyway.user}</user>
<password>${flyway.password}</password>
</configuration>
</plugin>迁移脚本命名规范:
src/main/resources/db/migration/
├── V1__create_user_table.sql
├── V1.1__add_email_to_user.sql
├── V2__create_order_table.sql
├── V2.1__add_index_to_order.sql
├── V3__add_order_status.sql
└── R__daily_cleanup.sql -- 可重复执行的脚本src/main/resources/db/migration/
├── V1__create_user_table.sql
├── V1.1__add_email_to_user.sql
├── V2__create_order_table.sql
├── V2.1__add_index_to_order.sql
├── V3__add_order_status.sql
└── R__daily_cleanup.sql -- 可重复执行的脚本CI 中的数据库迁移流程:
name: Database Migration CI
on:
push:
branches: [main]
paths:
- 'src/main/resources/db/migration/**'
jobs:
validate-migration:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: testpass
MYSQL_DATABASE: testdb
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=5
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Validate migrations
run: mvn flyway:validate flyway:migrate
env:
flyway.url: jdbc:mysql://localhost:3306/testdb
flyway.user: root
flyway.password: testpass
- name: Run integration tests
run: mvn test -Pintegration-test
env:
SPRING_DATASOURCE_URL: jdbc:mysql://localhost:3306/testdb
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: testpassname: Database Migration CI
on:
push:
branches: [main]
paths:
- 'src/main/resources/db/migration/**'
jobs:
validate-migration:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: testpass
MYSQL_DATABASE: testdb
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=5
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Validate migrations
run: mvn flyway:validate flyway:migrate
env:
flyway.url: jdbc:mysql://localhost:3306/testdb
flyway.user: root
flyway.password: testpass
- name: Run integration tests
run: mvn test -Pintegration-test
env:
SPRING_DATASOURCE_URL: jdbc:mysql://localhost:3306/testdb
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: testpassCI/CD 中数据库迁移的最佳实践:
- 迁移脚本向前兼容:新的迁移不应破坏正在运行的旧版本应用,确保零停机部署
- 不可逆迁移需谨慎:破坏性变更(如删除列)应分多个版本逐步进行
- 在 CI 中验证迁移:每次提交都使用独立的数据库实例验证迁移脚本
- 迁移即代码:所有数据库变更都通过迁移脚本管理,禁止手动修改数据库
- 回滚策略:虽然 Flyway 不支持自动回滚,但应准备逆向迁移脚本
- PR 审核:数据库迁移脚本应和代码一起经过 Code Review
CI/CD 安全最佳实践
Secrets 管理:
- 使用 CI/CD 平台提供的 Secrets 管理功能
- 定期轮换密钥
- 不要将 Secrets 输出到构建日志中
- 使用专门的 CI/CD 服务账号,权限最小化
依赖安全扫描:
steps:
- name: Check for vulnerable dependencies
run: mvn org.owasp:dependency-check-maven:check
- name: Run Trivy scan on Docker image
uses: aquasecurity/trivy-action@master
with:
image-ref: 'your-app:latest'
format: 'sarif'
output: 'trivy-results.sarif'steps:
- name: Check for vulnerable dependencies
run: mvn org.owasp:dependency-check-maven:check
- name: Run Trivy scan on Docker image
uses: aquasecurity/trivy-action@master
with:
image-ref: 'your-app:latest'
format: 'sarif'
output: 'trivy-results.sarif'供应链安全:
- 锁定依赖版本,使用 Maven Enforcer 插件禁止 SNAPSHOT 依赖
- 验证依赖的签名和校验和
- 定期扫描已知漏洞
- 使用 SBOM(Software Bill of Materials)追踪依赖
部署方式
部署到虚拟机(SCP + SSH)
传统的部署方式,将打包好的应用通过 SCP 传输到服务器,然后通过 SSH 启动:
name: Deploy to VM
on:
push:
tags: ['v*']
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Build application
run: mvn clean package -DskipTests
- name: SCP to server
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
source: "target/app.jar"
target: "/opt/app/tmp/"
strip_components: 1
- name: SSH restart application
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
script: |
# 备份旧版本
TIMESTAMP=$(date +%Y%m%d%H%M%S)
cp /opt/app/app.jar /opt/app/backups/app-$TIMESTAMP.jar
# 替换新版本
mv /opt/app/tmp/app.jar /opt/app/app.jar
# 重启应用
sudo systemctl restart myapp
# 健康检查
sleep 15
STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/actuator/health)
if [ "$STATUS" != "200" ]; then
echo "Health check failed!"
exit 1
fi
# 清理
find /opt/app/backups -name "*.jar" -mtime +30 -deletename: Deploy to VM
on:
push:
tags: ['v*']
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Build application
run: mvn clean package -DskipTests
- name: SCP to server
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
source: "target/app.jar"
target: "/opt/app/tmp/"
strip_components: 1
- name: SSH restart application
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
script: |
# 备份旧版本
TIMESTAMP=$(date +%Y%m%d%H%M%S)
cp /opt/app/app.jar /opt/app/backups/app-$TIMESTAMP.jar
# 替换新版本
mv /opt/app/tmp/app.jar /opt/app/app.jar
# 重启应用
sudo systemctl restart myapp
# 健康检查
sleep 15
STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/actuator/health)
if [ "$STATUS" != "200" ]; then
echo "Health check failed!"
exit 1
fi
# 清理
find /opt/app/backups -name "*.jar" -mtime +30 -delete使用 systemd 管理 Java 应用:
[Unit]
Description=My Java Application
After=network.target
[Service]
Type=simple
User=appuser
WorkingDirectory=/opt/app
ExecStart=/usr/bin/java \
-Xms512m -Xmx1024m \
-jar /opt/app/app.jar \
--spring.profiles.active=prod
Restart=always
RestartSec=10
Environment=DB_URL=jdbc:mysql://localhost:3306/prod_db
Environment=SPRING_PROFILES_ACTIVE=prod
[Install]
WantedBy=multi-user.target[Unit]
Description=My Java Application
After=network.target
[Service]
Type=simple
User=appuser
WorkingDirectory=/opt/app
ExecStart=/usr/bin/java \
-Xms512m -Xmx1024m \
-jar /opt/app/app.jar \
--spring.profiles.active=prod
Restart=always
RestartSec=10
Environment=DB_URL=jdbc:mysql://localhost:3306/prod_db
Environment=SPRING_PROFILES_ACTIVE=prod
[Install]
WantedBy=multi-user.target部署到 Kubernetes
使用 kubectl 直接部署:
name: Deploy to Kubernetes
on:
workflow_run:
workflows: ['Docker Build']
types: [completed]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up kubectl
uses: azure/setup-kubectl@v4
with:
version: 'latest'
- name: Configure K8s context
run: |
mkdir -p $HOME/.kube
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > $HOME/.kube/config
- name: Deploy to Kubernetes
run: |
# 使用环境变量替换模板中的占位符
export IMAGE_TAG=${{ github.sha }}
envsubst < k8s/deployment.yaml | kubectl apply -f -
kubectl apply -f k8s/service.yaml
kubectl rollout status deployment/myapp --timeout=300sname: Deploy to Kubernetes
on:
workflow_run:
workflows: ['Docker Build']
types: [completed]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up kubectl
uses: azure/setup-kubectl@v4
with:
version: 'latest'
- name: Configure K8s context
run: |
mkdir -p $HOME/.kube
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > $HOME/.kube/config
- name: Deploy to Kubernetes
run: |
# 使用环境变量替换模板中的占位符
export IMAGE_TAG=${{ github.sha }}
envsubst < k8s/deployment.yaml | kubectl apply -f -
kubectl apply -f k8s/service.yaml
kubectl rollout status deployment/myapp --timeout=300sKubernetes 部署清单示例:
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: production
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: your-registry/myapp:${IMAGE_TAG}
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: DB_URL
valueFrom:
secretKeyRef:
name: db-secret
key: url
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 30
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: production
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: your-registry/myapp:${IMAGE_TAG}
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: DB_URL
valueFrom:
secretKeyRef:
name: db-secret
key: url
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 30
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"使用 Helm 部署:
- name: Deploy with Helm
run: |
helm upgrade --install myapp ./charts/myapp \
--namespace production \
--set image.tag=${{ github.sha }} \
--set replicaCount=3 \
--set resources.requests.memory=512Mi \
--wait --timeout 5m- name: Deploy with Helm
run: |
helm upgrade --install myapp ./charts/myapp \
--namespace production \
--set image.tag=${{ github.sha }} \
--set replicaCount=3 \
--set resources.requests.memory=512Mi \
--wait --timeout 5mHelm 的 Chart 结构示例:
charts/myapp/
├── Chart.yaml # Chart 元数据
├── values.yaml # 默认配置值
├── values-prod.yaml # 生产环境覆盖配置
└── templates/
├── deployment.yaml
├── service.yaml
├── ingress.yaml
├── configmap.yaml
└── _helpers.tpl # 模板辅助函数charts/myapp/
├── Chart.yaml # Chart 元数据
├── values.yaml # 默认配置值
├── values-prod.yaml # 生产环境覆盖配置
└── templates/
├── deployment.yaml
├── service.yaml
├── ingress.yaml
├── configmap.yaml
└── _helpers.tpl # 模板辅助函数蓝绿部署
蓝绿部署(Blue-Green Deployment)维护两套完全相同的生产环境(蓝环境和绿环境),通过切换流量来实现零停机部署。
蓝绿部署流程:
用户流量 → 负载均衡器
↓
┌───────┴───────┐
↓ ↓
蓝环境(v2) 绿环境(v1)
[新版本] [旧版本]用户流量 → 负载均衡器
↓
┌───────┴───────┐
↓ ↓
蓝环境(v2) 绿环境(v1)
[新版本] [旧版本]部署步骤:
- 所有流量当前指向绿环境(旧版本)
- 在蓝环境部署新版本应用
- 对蓝环境进行健康检查和集成测试
- 将负载均衡器从绿环境切换到蓝环境
- 观察一段时间,如果出现问题则立即切回绿环境
- 确认无误后,释放绿环境资源
name: Blue-Green Deployment
on:
workflow_dispatch:
inputs:
version:
required: true
description: 'Version to deploy'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Determine target environment
id: env
run: |
# 检查当前活跃环境
ACTIVE=$(kubectl get svc myapp -n production -o jsonpath='{.spec.selector.env}')
if [ "$ACTIVE" == "blue" ]; then
echo "target=green" >> $GITHUB_OUTPUT
echo "inactive=green" >> $GITHUB_OUTPUT
else
echo "target=blue" >> $GITHUB_OUTPUT
echo "inactive=blue" >> $GITHUB_OUTPUT
fi
- name: Deploy to ${{ steps.env.outputs.target }}
run: |
kubectl apply -f k8s/deployment-${{ steps.env.outputs.target }}.yaml
kubectl rollout status deployment/myapp-${{ steps.env.outputs.target }} \
--namespace production --timeout=300s
- name: Run smoke tests
run: |
TARGET_URL="http://${{ steps.env.outputs.target }}.myapp.internal/actuator/health"
curl -f --retry 3 --retry-delay 5 $TARGET_URL
- name: Switch traffic
run: |
kubectl patch svc myapp -n production \
-p "{\"spec\":{\"selector\":{\"env\":\"${{ steps.env.outputs.target }}\"}}}"
- name: Verify all traffic healthy
run: |
sleep 10
curl -f https://myapp.com/actuator/health --retry 5 --retry-delay 10
- name: Scale down old environment
if: success()
run: |
kubectl scale deployment myapp-${{ steps.env.outputs.inactive }} \
--namespace production --replicas=0name: Blue-Green Deployment
on:
workflow_dispatch:
inputs:
version:
required: true
description: 'Version to deploy'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Determine target environment
id: env
run: |
# 检查当前活跃环境
ACTIVE=$(kubectl get svc myapp -n production -o jsonpath='{.spec.selector.env}')
if [ "$ACTIVE" == "blue" ]; then
echo "target=green" >> $GITHUB_OUTPUT
echo "inactive=green" >> $GITHUB_OUTPUT
else
echo "target=blue" >> $GITHUB_OUTPUT
echo "inactive=blue" >> $GITHUB_OUTPUT
fi
- name: Deploy to ${{ steps.env.outputs.target }}
run: |
kubectl apply -f k8s/deployment-${{ steps.env.outputs.target }}.yaml
kubectl rollout status deployment/myapp-${{ steps.env.outputs.target }} \
--namespace production --timeout=300s
- name: Run smoke tests
run: |
TARGET_URL="http://${{ steps.env.outputs.target }}.myapp.internal/actuator/health"
curl -f --retry 3 --retry-delay 5 $TARGET_URL
- name: Switch traffic
run: |
kubectl patch svc myapp -n production \
-p "{\"spec\":{\"selector\":{\"env\":\"${{ steps.env.outputs.target }}\"}}}"
- name: Verify all traffic healthy
run: |
sleep 10
curl -f https://myapp.com/actuator/health --retry 5 --retry-delay 10
- name: Scale down old environment
if: success()
run: |
kubectl scale deployment myapp-${{ steps.env.outputs.inactive }} \
--namespace production --replicas=0滚动升级
滚动升级(Rolling Update)是 Kubernetes 默认的部署策略,逐步用新版本 Pod 替换旧版本 Pod,过程中保持服务可用。
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 最多超出期望副本数 1 个
maxUnavailable: 0 # 部署期间必须保持所有 Pod 可用
minReadySeconds: 30 # Pod 就绪后等待 30 秒再继续apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 最多超出期望副本数 1 个
maxUnavailable: 0 # 部署期间必须保持所有 Pod 可用
minReadySeconds: 30 # Pod 就绪后等待 30 秒再继续滚动升级过程:
初始状态: 5 个旧版本 Pod
Step 1: 创建 1 个新 Pod,总数变为 6(maxSurge=1)
旧旧旧旧旧 新
Step 2: 新 Pod 就绪后,终止 1 个旧 Pod
旧旧旧旧 新
Step 3: 创建下一个新 Pod
旧旧旧旧 新新
Step 4: 新 Pod 就绪后,终止下一个旧 Pod
旧旧旧 新新
...重复直到所有 Pod 都替换为新版本...初始状态: 5 个旧版本 Pod
Step 1: 创建 1 个新 Pod,总数变为 6(maxSurge=1)
旧旧旧旧旧 新
Step 2: 新 Pod 就绪后,终止 1 个旧 Pod
旧旧旧旧 新
Step 3: 创建下一个新 Pod
旧旧旧旧 新新
Step 4: 新 Pod 就绪后,终止下一个旧 Pod
旧旧旧 新新
...重复直到所有 Pod 都替换为新版本...部署策略对比
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 滚动升级 | 渐进式替换,资源消耗低 | 回滚速度慢,兼容性要求高 | 常规发布 |
| 蓝绿部署 | 即时切换,回滚快 | 需要双倍资源 | 关键业务系统 |
| 金丝雀发布 | 风险可控,逐步放量 | 流量管理复杂 | 大版本变更 |
| A/B 测试 | 支持业务验证 | 需要业务指标配合 | 功能验证 |
| 重建部署 | 简单直接 | 有停机时间 | 开发/测试环境 |
金丝雀发布
金丝雀发布(Canary Release)是逐步将新版本流量从小比例增加到全量的策略:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-canary
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "10" # 10% 流量
spec:
rules:
- host: myapp.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp-canary
port:
number: 8080apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-canary
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "10" # 10% 流量
spec:
rules:
- host: myapp.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp-canary
port:
number: 8080金丝雀发布的 CI/CD 集成:
- name: Deploy canary
run: |
kubectl apply -f k8s/deployment-canary.yaml
- name: Monitor canary metrics
run: |
# 监控错误率和延迟
ERROR_RATE=$(curl -s http://prometheus:9090/api/v1/query \
--data "query=rate(http_requests_total{app='myapp-canary',status=~'5..'}[5m])")
echo "Canary error rate: $ERROR_RATE"
# 如果错误率低于阈值,逐步增加流量
if [ "$ERROR_RATE" -lt "0.01" ]; then
echo "Canary healthy, increasing traffic to 50%"
kubectl annotate ingress myapp-canary \
nginx.ingress.kubernetes.io/canary-weight="50"
fi
- name: Promote canary to full release
if: success()
run: |
# 将所有流量切换到新版本
kubectl patch ingress myapp-canary \
--type=json \
-p='[{"op": "remove", "path": "/metadata/annotations/nginx.ingress.kubernetes.io/canary"}]'
kubectl scale deployment myapp-stable --replicas=0- name: Deploy canary
run: |
kubectl apply -f k8s/deployment-canary.yaml
- name: Monitor canary metrics
run: |
# 监控错误率和延迟
ERROR_RATE=$(curl -s http://prometheus:9090/api/v1/query \
--data "query=rate(http_requests_total{app='myapp-canary',status=~'5..'}[5m])")
echo "Canary error rate: $ERROR_RATE"
# 如果错误率低于阈值,逐步增加流量
if [ "$ERROR_RATE" -lt "0.01" ]; then
echo "Canary healthy, increasing traffic to 50%"
kubectl annotate ingress myapp-canary \
nginx.ingress.kubernetes.io/canary-weight="50"
fi
- name: Promote canary to full release
if: success()
run: |
# 将所有流量切换到新版本
kubectl patch ingress myapp-canary \
--type=json \
-p='[{"op": "remove", "path": "/metadata/annotations/nginx.ingress.kubernetes.io/canary"}]'
kubectl scale deployment myapp-stable --replicas=0部署后的监控与回滚
健康检查:
- name: Post-deployment health check
run: |
# 多维度健康检查
ENDPOINTS=(
"https://myapp.com/actuator/health"
"https://myapp.com/actuator/info"
"https://myapp.com/api/ping"
)
for endpoint in "${ENDPOINTS[@]}"; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 $endpoint)
if [ "$STATUS" != "200" ]; then
echo "Health check failed: $endpoint returned $STATUS"
exit 1
fi
done- name: Post-deployment health check
run: |
# 多维度健康检查
ENDPOINTS=(
"https://myapp.com/actuator/health"
"https://myapp.com/actuator/info"
"https://myapp.com/api/ping"
)
for endpoint in "${ENDPOINTS[@]}"; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 $endpoint)
if [ "$STATUS" != "200" ]; then
echo "Health check failed: $endpoint returned $STATUS"
exit 1
fi
done自动回滚策略:
- name: Monitor and rollback if needed
run: |
# 部署后监控 5 分钟
for i in $(seq 1 10); do
ERROR_RATE=$(curl -s "http://prometheus:9090/api/v1/query" \
--data "query=sum(rate(http_requests_total{status=~'5..'}[1m]))/sum(rate(http_requests_total[1m]))")
if (( $(echo "$ERROR_RATE > 0.01" | bc -l) )); then
echo "Error rate $ERROR_RATE exceeds threshold, triggering rollback..."
kubectl rollout undo deployment/myapp
exit 1
fi
sleep 30
done- name: Monitor and rollback if needed
run: |
# 部署后监控 5 分钟
for i in $(seq 1 10); do
ERROR_RATE=$(curl -s "http://prometheus:9090/api/v1/query" \
--data "query=sum(rate(http_requests_total{status=~'5..'}[1m]))/sum(rate(http_requests_total[1m]))")
if (( $(echo "$ERROR_RATE > 0.01" | bc -l) )); then
echo "Error rate $ERROR_RATE exceeds threshold, triggering rollback..."
kubectl rollout undo deployment/myapp
exit 1
fi
sleep 30
doneKubernetes 回滚操作:
# 回滚到上一个版本
kubectl rollout undo deployment/myapp
# 回滚到指定版本
kubectl rollout undo deployment/myapp --to-revision=3
# 查看部署历史
kubectl rollout history deployment/myapp
# 查看回滚状态
kubectl rollout status deployment/myapp# 回滚到上一个版本
kubectl rollout undo deployment/myapp
# 回滚到指定版本
kubectl rollout undo deployment/myapp --to-revision=3
# 查看部署历史
kubectl rollout history deployment/myapp
# 查看回滚状态
kubectl rollout status deployment/myapp总结
CI/CD 是 Java 项目 DevOps 实践的核心,本文涵盖了从基础概念到具体实现的全流程:
- CI/CD 基础:理解了持续集成、持续交付和持续部署的概念与价值
- GitHub Actions:掌握了工作流定义、Maven/Gradle 构建、矩阵测试、SonarQube 分析和 Docker 构建推送
- Docker 多阶段构建:学会了分层构建、镜像优化、Spring Boot 分层打包和 Jib 工具
- 环境管理:了解了多环境配置方案、Secrets 安全管理和数据库迁移处理
- 部署方式:覆盖了虚拟机部署、K8s 部署、蓝绿部署、滚动升级和金丝雀发布等策略
推荐实践路线:
入门阶段:GitHub Actions + Maven + 单阶段 Docker 构建 + 虚拟机部署
进阶阶段:增加 SonarQube + 多阶段构建 + Docker 镜像优化
高级阶段:引入 K8s + Helm + 蓝绿/金丝雀部署 + 自动化监控回滚入门阶段:GitHub Actions + Maven + 单阶段 Docker 构建 + 虚拟机部署
进阶阶段:增加 SonarQube + 多阶段构建 + Docker 镜像优化
高级阶段:引入 K8s + Helm + 蓝绿/金丝雀部署 + 自动化监控回滚CI/CD 的核心目标是让软件交付更快、更安全、更可靠。选择合适的技术栈和策略,根据团队成熟度和业务需求逐步演进,是实施 CI/CD 的关键。
Cao Note