前言
CI/CD(持续集成/持续部署)是现代软件开发的核心实践。Spring Boot 应用可以通过 Jenkins、GitLab CI、GitHub Actions 等工具实现自动化构建和部署。本文将介绍 Spring Boot CI/CD 的完整方案。
CI/CD 流程
1. 典型流程
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Code │────▶│ Build │────▶│ Test │────▶│ Package │────▶│ Deploy │
│ Push │ │ Maven │ │ Unit │ │ Docker │ │ K8s │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
Git Push Compile Run Tests Build Image kubectl apply
2. 阶段说明
| 阶段 | 说明 | 工具 |
|---|---|---|
| Code | 代码提交 | Git |
| Build | 编译构建 | Maven/Gradle |
| Test | 单元测试 | JUnit/Mockito |
| Package | 打包镜像 | Docker/Jib |
| Deploy | 部署应用 | K8s/Helm |
GitHub Actions
1. 基础配置
# .github/workflows/ci.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
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: maven
- name: Build with Maven
run: mvn -B clean package -DskipTests
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: demo-jar
path: target/*.jar
2. 完整流水线
# .github/workflows/deploy.yml
name: Deploy Pipeline
on:
push:
branches: [main]
tags: ['v*']
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
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: maven
- name: Run tests
run: mvn -B test
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: target/site/jacoco/jacoco.xml
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
deploy-dev:
needs: build
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v4
- name: Set up kubectl
uses: azure/setup-kubectl@v3
with:
version: 'v1.28.0'
- name: Configure kubectl
run: |
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > kubeconfig
export KUBECONFIG=kubeconfig
- name: Deploy to K8s
run: |
kubectl set image deployment/demo demo=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
kubectl rollout status deployment/demo
deploy-prod:
needs: deploy-dev
runs-on: ubuntu-latest
environment: production
if: startsWith(github.ref, 'refs/tags/v')
steps:
- uses: actions/checkout@v4
- name: Set up Helm
uses: azure/setup-helm@v3
with:
version: 'v3.13.0'
- name: Configure kubectl
run: |
echo "${{ secrets.KUBE_CONFIG_PROD }}" | base64 -d > kubeconfig
export KUBECONFIG=kubeconfig
- name: Deploy with Helm
run: |
helm upgrade demo ./helm/demo \
--install \
--namespace production \
--set image.tag=${{ github.ref_name }} \
--wait --timeout 5m
3. 环境变量和密钥
# 在 GitHub Settings → Secrets and variables 中配置
# Secrets:
# - KUBE_CONFIG: K8s 配置文件(base64 编码)
# - DOCKER_USERNAME: Docker 用户名
# - DOCKER_PASSWORD: Docker 密码
# - SONAR_TOKEN: SonarQube Token
# Variables:
# - SONAR_HOST_URL: SonarQube 地址
# - DEPLOY_ENV: 部署环境
GitLab CI
1. 基础配置
# .gitlab-ci.yml
stages:
- build
- test
- package
- deploy
variables:
MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"
DOCKER_TLS_CERTDIR: ""
cache:
paths:
- .m2/repository/
- target/
build:
stage: build
image: maven:3.9-eclipse-temurin-21
script:
- mvn clean package -DskipTests
artifacts:
paths:
- target/*.jar
expire_in: 1 week
test:
stage: test
image: maven:3.9-eclipse-temurin-21
script:
- mvn test
artifacts:
reports:
junit: target/surefire-reports/*.xml
coverage: '/Total.*?([0-9]{1,3})%/'
package:
stage: package
image: docker:24
services:
- docker:24-dind
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
only:
- main
- develop
deploy-dev:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl set image deployment/demo demo=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA -n dev
environment:
name: development
url: https://dev.example.com
only:
- develop
deploy-prod:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl set image deployment/demo demo=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA -n prod
environment:
name: production
url: https://example.com
when: manual
only:
- main
2. 多环境配置
# .gitlab-ci.yml
stages:
- build
- test
- deploy
.deploy_template: &deploy_template
image: bitnami/kubectl:latest
before_script:
- mkdir -p ~/.kube
- echo "$KUBE_CONFIG" | base64 -d > ~/.kube/config
deploy-dev:
<<: *deploy_template
stage: deploy
script:
- kubectl apply -f k8s/dev/ -n dev
environment:
name: development
url: https://dev.example.com
only:
- develop
deploy-test:
<<: *deploy_template
stage: deploy
script:
- kubectl apply -f k8s/test/ -n test
environment:
name: test
url: https://test.example.com
only:
- test
deploy-prod:
<<: *deploy_template
stage: deploy
script:
- kubectl apply -f k8s/prod/ -n prod
environment:
name: production
url: https://example.com
when: manual
only:
- main
3. GitLab Runner
# 安装 Runner
curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | bash
apt-get install gitlab-runner
# 注册 Runner
gitlab-runner register \
--url https://gitlab.com/ \
--registration-token YOUR_TOKEN \
--executor docker \
--description "My Docker Runner" \
--docker-image "docker:24" \
--docker-volumes /var/run/docker.sock:/var/run/docker.sock
Jenkins
1. Jenkinsfile(声明式)
// Jenkinsfile
pipeline {
agent any
tools {
maven 'Maven 3.9'
jdk 'JDK 21'
}
environment {
REGISTRY = 'registry.example.com'
IMAGE_NAME = 'demo'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
sh 'mvn clean package -DskipTests'
}
post {
success {
archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
}
}
}
stage('Test') {
steps {
sh 'mvn test'
}
post {
always {
junit 'target/surefire-reports/*.xml'
publishCoverage adapters: [jacocoAdapter('target/site/jacoco/jacoco.xml')]
}
}
}
stage('Build Docker Image') {
steps {
script {
docker.build("${REGISTRY}/${IMAGE_NAME}:${env.BUILD_ID}")
}
}
}
stage('Push Docker Image') {
steps {
script {
docker.withRegistry("https://${REGISTRY}", 'docker-credentials') {
docker.image("${REGISTRY}/${IMAGE_NAME}:${env.BUILD_ID}").push()
docker.image("${REGISTRY}/${IMAGE_NAME}:${env.BUILD_ID}").push('latest')
}
}
}
}
stage('Deploy to Dev') {
when {
branch 'develop'
}
steps {
sh '''
kubectl set image deployment/demo demo=${REGISTRY}/${IMAGE_NAME}:${BUILD_ID} -n dev
kubectl rollout status deployment/demo -n dev
'''
}
}
stage('Deploy to Prod') {
when {
branch 'main'
}
steps {
input message: 'Deploy to production?', ok: 'Deploy'
sh '''
kubectl set image deployment/demo demo=${REGISTRY}/${IMAGE_NAME}:${BUILD_ID} -n prod
kubectl rollout status deployment/demo -n prod
'''
}
}
}
post {
always {
cleanWs()
}
failure {
mail to: 'team@example.com',
subject: "Build Failed: ${env.JOB_NAME} ${env.BUILD_ID}",
body: "Check console output at ${BUILD_URL}"
}
}
}
2. 多分支流水线
// Jenkinsfile
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'mvn clean package -DskipTests'
}
}
stage('Deploy') {
steps {
script {
if (env.BRANCH_NAME == 'main') {
deploy('prod')
} else if (env.BRANCH_NAME == 'develop') {
deploy('dev')
}
}
}
}
}
}
def deploy(String env) {
// 部署逻辑
}
3. 共享库
// vars/springBootBuild.groovy
def call(Map config = [:]) {
def mavenVersion = config.mavenVersion ?: 'Maven 3.9'
def jdkVersion = config.jdkVersion ?: 'JDK 21'
tools {
maven mavenVersion
jdk jdkVersion
}
stage('Build') {
sh 'mvn clean package -DskipTests'
}
stage('Test') {
sh 'mvn test'
junit 'target/surefire-reports/*.xml'
}
}
// Jenkinsfile
@Library('my-shared-library') _
pipeline {
agent any
stages {
stage('Build and Test') {
steps {
springBootBuild(mavenVersion: 'Maven 3.9', jdkVersion: 'JDK 21')
}
}
}
}
ArgoCD 持续部署
1. 安装 ArgoCD
# 安装
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# 访问 UI
kubectl port-forward svc/argocd-server -n argocd 8080:443
# 获取初始密码
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d
2. 创建应用
# argocd-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: demo
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/your-org/demo-k8s.git
targetRevision: HEAD
path: k8s/prod
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
# 创建应用
kubectl apply -f argocd-app.yaml
# 查看状态
argocd app get demo
# 同步应用
argocd app sync demo
3. GitOps 流程
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Code │────▶│ CI │────▶│ Git │────▶│ ArgoCD │
│ Repo │ │ Pipeline│ │ Ops │ │ Sync │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
│ │ │
▼ ▼ ▼
Build & Test Update Manifest Deploy to K8s
Push Image k8s/prod/deployment.yaml
最佳实践
1. 流水线优化
# ✅ 推荐
# 并行执行
jobs:
test:
# 单元测试
security-scan:
# 安全扫描
lint:
# 代码检查
# 缓存依赖
cache:
paths:
- .m2/repository/
# ❌ 不推荐
# 串行执行所有任务
# 不缓存依赖
2. 安全配置
# ✅ 推荐
# 使用密钥管理
secrets:
- KUBE_CONFIG
- DOCKER_PASSWORD
# 扫描镜像
- name: Scan image
uses: aquasecurity/trivy-action@master
# ❌ 不推荐
# 硬编码密钥
# 不扫描漏洞
3. 通知配置
# Slack 通知
- name: Notify Slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
# 邮件通知
post {
failure {
mail to: 'team@example.com',
subject: "Build Failed: ${env.JOB_NAME}"
}
}
4. 版本管理
# 语义化版本
tags:
- 'v1.0.0'
- 'v1.0.1'
- 'v1.1.0'
# Git 标签
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0
总结
CI/CD 要点:
- ✅ GitHub Actions - 原生集成、易于使用
- ✅ GitLab CI - 一体化平台、功能强大
- ✅ Jenkins - 灵活、插件丰富
- ✅ ArgoCD - GitOps、持续部署
- ✅ 最佳实践 - 并行、缓存、安全、通知
CI/CD 是 DevOps 实践的核心。