Docker容器化实战:将你的SpringBoot应用一键打包部署(二)-设置CI/CD流水线实现自动化部署

在完成SpringBoot应用Docker化的基础上,我们现在将建立完整的CI/CD流水线,实现从代码提交到生产部署的全流程自动化。

选择CI/CD工具链

工具选型比较

bash 复制代码
#!/bin/bash
# ci_cd_tool_comparison.sh - CI/CD工具链选型分析

set -euo pipefail

cat << 'EOF'
🔍 CI/CD工具链选型分析
================================

1. Jenkins (推荐)
   ✅ 优点: 功能全面、插件丰富、社区活跃
   ❌ 缺点: 配置复杂、资源消耗较大

2. GitLab CI
   ✅ 优点: 与GitLab深度集成、配置简单
   ❌ 缺点: 社区版功能有限

3. GitHub Actions
   ✅ 优点: 与GitHub深度集成、配置简单
   ❌ 缺点: 私有仓库有使用限制

4. Azure DevOps
   ✅ 优点: 微软生态完善、功能全面
   ❌ 缺点: 国内访问可能较慢

根据项目需求,我们选择 Jenkins + Docker + Kubernetes 作为CI/CD工具链
EOF

# 检查现有工具
echo -e "\n📋 系统现有工具检查:"
for tool in java mvn docker git ssh; do
    if command -v $tool &> /dev/null; then
        echo "  ✅ $tool: $(which $tool)"
    else
        echo "  ❌ $tool: 未安装"
    fi
done

Jenkins自动化部署

Jenkins安装和配置

bash 复制代码
#!/bin/bash
# install_jenkins.sh - Jenkins自动化安装配置

set -euo pipefail

# 颜色定义
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
BLUE='\033[0;34m'
NC='\033[0m'

JENKINS_HOME="/var/lib/jenkins"
JENKINS_PORT="8080"
DOCKER_GROUP="docker"

log() {
    echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')] $1${NC}"
}

error_exit() {
    echo -e "${RED}错误: $1${NC}" >&2
    exit 1
}

# 检测操作系统
detect_os() {
    if [ -f /etc/os-release ]; then
        source /etc/os-release
        OS=$ID
        VERSION=$VERSION_ID
    else
        error_exit "无法检测操作系统"
    fi
}

# 安装Java
install_java() {
    log "安装Java 11..."
    
    case $OS in
        ubuntu|debian)
            sudo apt-get update
            sudo apt-get install -y openjdk-11-jdk
            ;;
        centos|rhel)
            sudo yum install -y java-11-openjdk-devel
            ;;
        *)
            error_exit "不支持的操作系统: $OS"
            ;;
    esac
    
    # 设置Java环境变量
    export JAVA_HOME=$(dirname $(dirname $(readlink -f $(which java))))
    echo "export JAVA_HOME=$JAVA_HOME" >> ~/.bashrc
    echo "export PATH=\$JAVA_HOME/bin:\$PATH" >> ~/.bashrc
    
    source ~/.bashrc
    log "✅ Java安装完成: $(java -version 2>&1 | head -1)"
}

# 安装Jenkins
install_jenkins() {
    log "安装Jenkins..."
    
    case $OS in
        ubuntu|debian)
            # 添加Jenkins仓库
            wget -q -O - https://pkg.jenkins.io/debian/jenkins.io.key | sudo apt-key add -
            sudo sh -c 'echo deb http://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list'
            
            sudo apt-get update
            sudo apt-get install -y jenkins
            ;;
        centos|rhel)
            # 添加Jenkins仓库
            sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo
            sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key
            
            sudo yum install -y jenkins
            ;;
        *)
            error_exit "不支持的操作系统: $OS"
            ;;
    esac
    
    log "✅ Jenkins安装完成"
}

# 配置Jenkins
configure_jenkins() {
    log "配置Jenkins..."
    
    # 备份原始配置
    sudo cp /etc/default/jenkins /etc/default/jenkins.backup
    
    # 配置Jenkins
    sudo tee /etc/default/jenkins > /dev/null << EOF
# Jenkins配置
JENKINS_HOME=$JENKINS_HOME
JENKINS_USER=jenkins
JENKINS_GROUP=jenkins
JENKINS_PORT=$JENKINS_PORT
JENKINS_LISTEN_ADDRESS=0.0.0.0
JENKINS_HTTP_PORT=$JENKINS_PORT
JENKINS_HTTPS_PORT=8443
JENKINS_DEBUG_LEVEL=5
JENKINS_ENABLE_ACCESS_LOG=false
JENKINS_HANDLER_MAX=100
JENKINS_HANDLER_IDLE=20
JENKINS_ARGS="--webroot=/var/cache/jenkins/war --httpPort=\$JENKINS_PORT"
EOF
    
    # 将Jenkins用户添加到docker组
    sudo usermod -aG $DOCKER_GROUP jenkins
    
    # 设置权限
    sudo chown -R jenkins:jenkins $JENKINS_HOME
    sudo chmod 755 $JENKINS_HOME
    
    log "✅ Jenkins配置完成"
}

# 安装Jenkins插件
install_jenkins_plugins() {
    log "安装Jenkins插件..."
    
    # 等待Jenkins启动
    log "等待Jenkins服务启动..."
    sleep 30
    
    # Jenkins CLI路径
    JENKINS_CLI="/var/cache/jenkins/war/WEB-INF/jenkins-cli.jar"
    
    # 获取初始管理员密码
    if [ -f /var/lib/jenkins/secrets/initialAdminPassword ]; then
        JENKINS_PASSWORD=$(sudo cat /var/lib/jenkins/secrets/initialAdminPassword)
        log "Jenkins初始管理员密码: $JENKINS_PASSWORD"
    else
        error_exit "无法获取Jenkins初始密码"
    fi
    
    # 插件列表
    PLUGINS=(
        "git"
        "docker-workflow"
        "pipeline"
        "blueocean"
        "kubernetes"
        "credentials-binding"
        "ssh-slaves"
        "matrix-auth"
        "workflow-aggregator"
        "github"
        "email-ext"
        "ws-cleanup"
        "ansible"
        "sonar"
        "jacoco"
        "htmlpublisher"
    )
    
    # 安装插件(这里只是模拟,实际需要通过Jenkins API)
    log "需要安装的插件:"
    for plugin in "${PLUGINS[@]}"; do
        echo "  - $plugin"
    done
    
    log "⚠️  请通过Jenkins网页界面安装以上插件"
    log "   访问: http://$(hostname -I | awk '{print $1}'):$JENKINS_PORT"
    log "   初始密码: $JENKINS_PASSWORD"
}

# 配置Jenkins安全
configure_jenkins_security() {
    log "配置Jenkins安全..."
    
    # 创建Jenkins配置目录
    local jenkins_config_dir="$JENKINS_HOME/init.groovy.d"
    sudo mkdir -p "$jenkins_config_dir"
    
    # 创建安全配置脚本
    sudo tee "$jenkins_config_dir/security-setup.groovy" > /dev/null << 'EOF'
import jenkins.model.*
import hudson.security.*
import jenkins.security.s2m.AdminWhitelistRule

def instance = Jenkins.getInstance()

// 启用安全
def hudsonRealm = new HudsonPrivateSecurityRealm(false)
hudsonRealm.createAccount("admin", "admin123")
instance.setSecurityRealm(hudsonRealm)

// 设置授权策略
def strategy = new FullControlOnceLoggedInAuthorizationStrategy()
strategy.setAllowAnonymousRead(false)
instance.setAuthorizationStrategy(strategy)

// 保存配置
instance.save()

// 配置代理到控制器的安全
Jenkins.instance.getInjector().getInstance(AdminWhitelistRule.class).setMasterKillSwitch(false)
EOF
    
    sudo chown jenkins:jenkins "$jenkins_config_dir/security-setup.groovy"
    
    log "✅ Jenkins安全配置完成"
    log "   默认用户名: admin"
    log "   默认密码: admin123"
}

# 启动Jenkins服务
start_jenkins() {
    log "启动Jenkins服务..."
    
    sudo systemctl daemon-reload
    sudo systemctl enable jenkins
    sudo systemctl start jenkins
    
    # 检查服务状态
    if sudo systemctl is-active --quiet jenkins; then
        log "✅ Jenkins服务启动成功"
        
        # 显示访问信息
        local server_ip=$(hostname -I | awk '{print $1}')
        echo ""
        echo "🎉 Jenkins安装完成!"
        echo "📋 访问信息:"
        echo "   地址: http://$server_ip:$JENKINS_PORT"
        echo "   初始密码: $(sudo cat /var/lib/jenkins/secrets/initialAdminPassword 2>/dev/null || echo '请检查/var/lib/jenkins/secrets/initialAdminPassword')"
        echo ""
        echo "🔧 后续步骤:"
        echo "   1. 访问上述地址完成初始设置"
        echo "   2. 安装推荐的插件"
        echo "   3. 创建管理员账户"
        echo "   4. 配置系统设置"
    else
        error_exit "Jenkins服务启动失败"
    fi
}

# 主函数
main() {
    log "开始安装和配置Jenkins..."
    
    # 检测操作系统
    detect_os
    
    # 安装Java
    install_java
    
    # 安装Jenkins
    install_jenkins
    
    # 配置Jenkins
    configure_jenkins
    
    # 配置安全
    configure_jenkins_security
    
    # 启动服务
    start_jenkins
    
    # 插件安装提示
    install_jenkins_plugins
    
    log "🎉 Jenkins安装配置全部完成!"
}

# 执行主函数
main "$@"

Jenkins Pipeline配置

groovy 复制代码
// Jenkinsfile - 完整的CI/CD流水线配置
pipeline {
    agent {
        docker {
            image 'maven:3.8.6-openjdk-11'
            args '-v /var/run/docker.sock:/var/run/docker.sock -v /usr/bin/docker:/usr/bin/docker'
        }
    }
    
    environment {
        // 应用配置
        APP_NAME = 'springboot-docker-demo'
        APP_VERSION = '1.0.0'
        DOCKER_REGISTRY = 'localhost:5000'
        
        // 环境配置
        DEPLOY_ENV = 'dev'
        KUBE_CONFIG = credentials('kubeconfig')
        DOCKER_CREDENTIALS = credentials('docker-hub')
        
        // SonarQube配置
        SONAR_HOST = 'http://sonarqube:9000'
        SONAR_TOKEN = credentials('sonar-token')
    }
    
    parameters {
        choice(
            name: 'DEPLOY_ENVIRONMENT',
            choices: ['dev', 'staging', 'prod'],
            description: '选择部署环境'
        )
        string(
            name: 'IMAGE_TAG',
            defaultValue: 'latest',
            description: 'Docker镜像标签'
        )
        booleanParam(
            name: 'RUN_INTEGRATION_TESTS',
            defaultValue: true,
            description: '是否运行集成测试'
        )
        booleanParam(
            name: 'DEPLOY_TO_K8S',
            defaultValue: false,
            description: '是否部署到Kubernetes'
        )
    }
    
    options {
        buildDiscarder(logRotator(numToKeepStr: '10'))
        timeout(time: 30, unit: 'MINUTES')
        gitLabConnection('gitlab-connection')
        disableConcurrentBuilds()
    }
    
    triggers {
        gitlab(
            triggerOnPush: true,
            triggerOnMergeRequest: true,
            branchFilterType: 'All'
        )
        pollSCM('H/5 * * * *')
    }
    
    stages {
        stage('代码检查') {
            parallel {
                stage('代码质量扫描') {
                    steps {
                        script {
                            echo "开始代码质量检查..."
                            sh '''
                                mvn sonar:sonar \
                                    -Dsonar.projectKey=springboot-docker-demo \
                                    -Dsonar.host.url=$SONAR_HOST \
                                    -Dsonar.login=$SONAR_TOKEN \
                                    -Dsonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml
                            '''
                        }
                    }
                    post {
                        success {
                            echo "✅ 代码质量检查通过"
                        }
                        failure {
                            error "❌ 代码质量检查失败"
                        }
                    }
                }
                
                stage('安全检查') {
                    steps {
                        script {
                            echo "开始安全漏洞扫描..."
                            sh '''
                                mvn org.owasp:dependency-check-maven:check \
                                    -Dformat=HTML \
                                    -DoutputDirectory=target/dependency-check
                            '''
                        }
                    }
                    post {
                        always {
                            publishHTML([
                                allowMissing: false,
                                alwaysLinkToLastBuild: true,
                                keepAll: true,
                                reportDir: 'target/dependency-check',
                                reportFiles: 'dependency-check-report.html',
                                reportName: '依赖安全检查报告'
                            ])
                        }
                    }
                }
            }
        }
        
        stage('编译和单元测试') {
            steps {
                script {
                    echo "开始编译和单元测试..."
                    sh '''
                        mvn clean compile test
                        mvn jacoco:report
                    '''
                }
            }
            post {
                always {
                    junit 'target/surefire-reports/*.xml'
                    jacoco(
                        execPattern: 'target/jacoco.exec',
                        classPattern: 'target/classes',
                        sourcePattern: 'src/main/java'
                    )
                }
                success {
                    echo "✅ 编译和单元测试通过"
                }
                failure {
                    error "❌ 编译或单元测试失败"
                }
            }
        }
        
        stage('构建Docker镜像') {
            steps {
                script {
                    echo "开始构建Docker镜像..."
                    
                    // 设置镜像标签
                    def imageTag = "${DOCKER_REGISTRY}/${APP_NAME}:${params.IMAGE_TAG}"
                    def imageTagWithCommit = "${DOCKER_REGISTRY}/${APP_NAME}:${env.GIT_COMMIT.substring(0, 8)}"
                    
                    sh """
                        docker build \
                            -t $imageTag \
                            -t $imageTagWithCommit \
                            -f Dockerfile \
                            .
                    """
                    
                    // 保存镜像信息
                    env.DOCKER_IMAGE = imageTag
                    env.DOCKER_IMAGE_WITH_COMMIT = imageTagWithCommit
                }
            }
            post {
                success {
                    echo "✅ Docker镜像构建成功: ${env.DOCKER_IMAGE}"
                    archiveArtifacts 'target/*.jar'
                }
                failure {
                    error "❌ Docker镜像构建失败"
                }
            }
        }
        
        stage('集成测试') {
            when {
                expression { params.RUN_INTEGRATION_TESTS == true }
            }
            steps {
                script {
                    echo "开始集成测试..."
                    
                    // 启动测试环境
                    sh '''
                        docker-compose -f docker-compose.test.yml up -d
                        sleep 30
                    '''
                    
                    // 运行集成测试
                    sh '''
                        mvn verify -Pintegration-test
                    '''
                    
                    // 清理测试环境
                    sh '''
                        docker-compose -f docker-compose.test.yml down
                    '''
                }
            }
            post {
                always {
                    junit 'target/failsafe-reports/*.xml'
                }
                success {
                    echo "✅ 集成测试通过"
                }
                failure {
                    error "❌ 集成测试失败"
                }
            }
        }
        
        stage('推送镜像') {
            steps {
                script {
                    echo "推送Docker镜像到仓库..."
                    
                    // 登录Docker仓库
                    sh """
                        docker login \
                            -u $DOCKER_CREDENTIALS_USR \
                            -p $DOCKER_CREDENTIALS_PSW \
                            $DOCKER_REGISTRY
                    """
                    
                    // 推送镜像
                    sh """
                        docker push ${env.DOCKER_IMAGE}
                        docker push ${env.DOCKER_IMAGE_WITH_COMMIT}
                    """
                }
            }
            post {
                success {
                    echo "✅ Docker镜像推送成功"
                }
                failure {
                    error "❌ Docker镜像推送失败"
                }
            }
        }
        
        stage('部署到环境') {
            steps {
                script {
                    echo "部署到 ${params.DEPLOY_ENVIRONMENT} 环境..."
                    
                    // 根据环境选择部署策略
                    switch(params.DEPLOY_ENVIRONMENT) {
                        case 'dev':
                            deployToDev()
                            break
                        case 'staging':
                            deployToStaging()
                            break
                        case 'prod':
                            deployToProd()
                            break
                        default:
                            error "未知环境: ${params.DEPLOY_ENVIRONMENT}"
                    }
                }
            }
        }
        
        stage('Kubernetes部署') {
            when {
                expression { params.DEPLOY_TO_K8S == true }
            }
            steps {
                script {
                    echo "部署到Kubernetes集群..."
                    deployToKubernetes()
                }
            }
        }
        
        stage('自动化测试') {
            steps {
                script {
                    echo "执行自动化测试..."
                    runAutomatedTests()
                }
            }
            post {
                always {
                    publishHTML([
                        allowMissing: false,
                        alwaysLinkToLastBuild: true,
                        keepAll: true,
                        reportDir: 'target/reports',
                        reportFiles: 'test-report.html',
                        reportName: '自动化测试报告'
                    ])
                }
            }
        }
    }
    
    post {
        always {
            echo "构建完成 - 结果: ${currentBuild.result}"
            
            // 清理工作空间
            cleanWs()
            
            // 保存构建信息
            script {
                currentBuild.description = "应用: ${APP_NAME} | 环境: ${params.DEPLOY_ENVIRONMENT} | 镜像: ${env.DOCKER_IMAGE}"
            }
        }
        success {
            script {
                // 发送成功通知
                sendBuildNotification('SUCCESS')
                
                // 更新部署看板
                updateDeploymentDashboard()
            }
        }
        failure {
            script {
                // 发送失败通知
                sendBuildNotification('FAILURE')
            }
        }
        unstable {
            script {
                // 发送不稳定通知
                sendBuildNotification('UNSTABLE')
            }
        }
    }
}

// 部署到开发环境
def deployToDev() {
    sh """
        docker-compose -f docker-compose.dev.yml down
        docker-compose -f docker-compose.dev.yml up -d
    """
    
    // 等待应用启动
    sh "sleep 30"
    
    // 运行健康检查
    sh """
        curl -f http://localhost:8080/health || exit 1
    """
    
    echo "✅ 开发环境部署完成"
}

// 部署到预生产环境
def deployToStaging() {
    sh """
        docker-compose -f docker-compose.staging.yml down
        docker-compose -f docker-compose.staging.yml up -d
    """
    
    // 等待应用启动
    sh "sleep 30"
    
    // 运行冒烟测试
    sh """
        ./smoke-tests.sh staging
    """
    
    echo "✅ 预生产环境部署完成"
}

// 部署到生产环境
def deployToProd() {
    // 生产环境需要确认
    input message: '确认部署到生产环境?', ok: '部署'
    
    // 蓝绿部署策略
    sh """
        ./blue-green-deploy.sh production ${env.DOCKER_IMAGE}
    """
    
    echo "✅ 生产环境部署完成"
}

// 部署到Kubernetes
def deployToKubernetes() {
    withCredentials([file(credentialsId: 'kubeconfig', variable: 'KUBECONFIG')]) {
        sh """
            kubectl config use-context ${params.DEPLOY_ENVIRONMENT}
            kubectl apply -f k8s/
            kubectl rollout status deployment/${APP_NAME}
        """
    }
}

// 运行自动化测试
def runAutomatedTests() {
    sh """
        mvn test -Pautomated-tests
        ./generate-test-report.sh
    """
}

// 发送构建通知
def sendBuildNotification(String status) {
    def subject = "构建 ${status}: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
    def body = """
        构建项目: ${env.JOB_NAME}
        构建编号: ${env.BUILD_NUMBER}
        构建状态: ${status}
        构建地址: ${env.BUILD_URL}
        部署环境: ${params.DEPLOY_ENVIRONMENT}
        Docker镜像: ${env.DOCKER_IMAGE}
        Git提交: ${env.GIT_COMMIT}
    """
    
    emailext (
        subject: subject,
        body: body,
        to: 'devops@company.com',
        attachLog: true
    )
}

// 更新部署看板
def updateDeploymentDashboard() {
    sh """
        curl -X POST \
            -H "Content-Type: application/json" \
            -d '{
                "application": "${APP_NAME}",
                "version": "${APP_VERSION}",
                "environment": "${params.DEPLOY_ENVIRONMENT}",
                "status": "success",
                "timestamp": "$(date -Iseconds)",
                "build_url": "${env.BUILD_URL}"
            }' \
            http://dashboard-api/update-deployment
    """
}

测试环境配置

yaml 复制代码
# docker-compose.test.yml - 测试环境配置
version: '3.8'

services:
  springboot-app-test:
    build:
      context: .
      dockerfile: Dockerfile.test
    image: springboot-docker-demo:test
    container_name: springboot-app-test
    ports:
      - "8081:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=test
      - JAVA_OPTS=-Xms256m -Xmx512m
      - LOG_LEVEL=DEBUG
    networks:
      - test-network
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 10s
      timeout: 5s
      retries: 10

  test-database:
    image: mysql:8.0
    container_name: test-mysql
    environment:
      - MYSQL_ROOT_PASSWORD=testpassword
      - MYSQL_DATABASE=test_db
      - MYSQL_USER=test_user
      - MYSQL_PASSWORD=test_password
    ports:
      - "3307:3306"
    networks:
      - test-network
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      timeout: 10s
      retries: 5

  test-redis:
    image: redis:7-alpine
    container_name: test-redis
    ports:
      - "6380:6379"
    networks:
      - test-network

  test-sonarqube:
    image: sonarqube:community
    container_name: test-sonarqube
    ports:
      - "9000:9000"
    environment:
      - SONAR_ES_BOOTSTRAP_CHECKS_DISABLE=true
    networks:
      - test-network

networks:
  test-network:
    driver: bridge

GitLab CI/CD配置

.gitlab-ci.yml配置

yaml 复制代码
# .gitlab-ci.yml - GitLab CI/CD配置
stages:
  - test
  - build
  - security-scan
  - deploy
  - monitor

variables:
  # 应用配置
  APP_NAME: "springboot-docker-demo"
  DOCKER_REGISTRY: "registry.gitlab.com/your-username/your-project"
  
  # Maven配置
  MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"
  MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version"
  
  # Docker配置
  DOCKER_TLS_CERTDIR: "/certs"

# 缓存配置
cache:
  key: "${CI_COMMIT_REF_SLUG}"
  paths:
    - .m2/repository
    - target/

# 只在特定分支触发
workflow:
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
    - if: $CI_COMMIT_BRANCH == "develop"
    - if: $CI_COMMIT_BRANCH =~ /^feature\/.*$/
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

.test_template: &test_template
  stage: test
  image: maven:3.8.6-openjdk-11
  services:
    - mysql:8.0
    - redis:7-alpine
  variables:
    MYSQL_DATABASE: "test_db"
    MYSQL_ROOT_PASSWORD: "test_password"
    REDIS_URL: "redis://redis:6379"
  before_script:
    - export DB_HOST=mysql
    - export DB_PORT=3306
  script:
    - mvn $MAVEN_CLI_OPTS clean test
  artifacts:
    when: always
    paths:
      - target/surefire-reports/
      - target/site/jacoco/
    reports:
      junit: target/surefire-reports/TEST-*.xml
  coverage: '/Total.*?([0-9]{1,3})%/'

unit-test:
  <<: *test_template
  except:
    - tags

integration-test:
  stage: test
  image: maven:3.8.6-openjdk-11
  services:
    - name: mysql:8.0
      alias: mysql
    - name: redis:7-alpine
      alias: redis
    - name: docker:20.10.16-dind
      alias: docker
  variables:
    DOCKER_HOST: "tcp://docker:2375"
    DOCKER_TLS_CERTDIR: ""
  before_script:
    - apk add --no-cache docker
    - docker --version
  script:
    - mvn $MAVEN_CLI_OPTS verify -Pintegration-test
    - docker-compose -f docker-compose.test.yml up -d
    - sleep 30
    - mvn $MAVEN_CLI_OPTS test -Pintegration-test
    - docker-compose -f docker-compose.test.yml down
  artifacts:
    when: always
    paths:
      - target/failsafe-reports/
    reports:
      junit: target/failsafe-reports/TEST-*.xml
  only:
    - main
    - develop
    - merge_requests

build:
  stage: build
  image: docker:20.10.16
  services:
    - name: docker:20.10.16-dind
      alias: docker
  variables:
    DOCKER_HOST: "tcp://docker:2375"
    DOCKER_TLS_CERTDIR: ""
  before_script:
    - apk add --no-cache maven openjdk11
    - java -version
    - mvn -version
  script:
    # 构建应用
    - mvn $MAVEN_CLI_OPTS clean package -DskipTests
    
    # 构建Docker镜像
    - docker build -t $DOCKER_REGISTRY/$APP_NAME:latest .
    - docker build -t $DOCKER_REGISTRY/$APP_NAME:$CI_COMMIT_SHORT_SHA .
    
    # 登录镜像仓库
    - echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
    
    # 推送镜像
    - docker push $DOCKER_REGISTRY/$APP_NAME:latest
    - docker push $DOCKER_REGISTRY/$APP_NAME:$CI_COMMIT_SHORT_SHA
  artifacts:
    paths:
      - target/*.jar
  only:
    - main
    - develop
    - tags

security-scan:
  stage: security-scan
  image: owasp/dependency-check:7
  variables:
    DC_DIR: "/tmp/dependency-check"
  script:
    - mkdir -p $DC_DIR
    - dependency-check.sh
        --project "$APP_NAME"
        --scan "."
        --format "HTML"
        --format "JSON"
        --out "$DC_DIR"
        --enableExperimental
        --failOnCVSS 8
  artifacts:
    when: always
    paths:
      - $DC_DIR/
    reports:
      dependency_scanning: $DC_DIR/dependency-check-report.json
  allow_failure: true
  only:
    - main
    - develop

sonarqube-check:
  stage: security-scan
  image: maven:3.8.6-openjdk-11
  variables:
    SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
    GIT_DEPTH: "0"
  cache:
    key: "${CI_JOB_NAME}"
    paths:
      - .sonar/cache
  script:
    - mvn $MAVEN_CLI_OPTS sonar:sonar
        -Dsonar.qualitygate.wait=true
        -Dsonar.projectKey="$APP_NAME"
        -Dsonar.projectName="$APP_NAME"
  allow_failure: false
  only:
    - main
    - develop
    - merge_requests

deploy-dev:
  stage: deploy
  image: docker:20.10.16
  services:
    - name: docker:20.10.16-dind
      alias: docker
  variables:
    DOCKER_HOST: "tcp://docker:2375"
    DOCKER_TLS_CERTDIR: ""
  environment:
    name: development
    url: https://dev.example.com
  before_script:
    - apk add --no-cache docker-compose
    - docker-compose --version
  script:
    - echo "部署到开发环境..."
    - docker-compose -f docker-compose.dev.yml down
    - docker-compose -f docker-compose.dev.yml up -d
    - sleep 30
    - curl -f http://localhost:8080/health || exit 1
  only:
    - develop

deploy-staging:
  stage: deploy
  image: docker:20.10.16
  services:
    - name: docker:20.10.16-dind
      alias: docker
  variables:
    DOCKER_HOST: "tcp://docker:2375"
    DOCKER_TLS_CERTDIR: ""
  environment:
    name: staging
    url: https://staging.example.com
  before_script:
    - apk add --no-cache docker-compose curl
  script:
    - echo "部署到预生产环境..."
    - docker-compose -f docker-compose.staging.yml down
    - docker-compose -f docker-compose.staging.yml up -d
    - sleep 30
    - ./smoke-tests.sh staging
  only:
    - main
  when: manual

deploy-prod:
  stage: deploy
  image: docker:20.10.16
  services:
    - name: docker:20.10.16-dind
      alias: docker
  variables:
    DOCKER_HOST: "tcp://docker:2375"
    DOCKER_TLS_CERTDIR: ""
  environment:
    name: production
    url: https://prod.example.com
  before_script:
    - apk add --no-cache docker-compose curl
  script:
    - echo "部署到生产环境..."
    - ./blue-green-deploy.sh production $DOCKER_REGISTRY/$APP_NAME:$CI_COMMIT_SHORT_SHA
  only:
    - tags
  when: manual

monitor:
  stage: monitor
  image: curlimages/curl:7.83.1
  environment:
    name: production
  script:
    - |
      echo "监控部署状态..."
      for i in {1..30}; do
        response=$(curl -s -o /dev/null -w "%{http_code}" https://prod.example.com/health || echo "000")
        if [ "$response" = "200" ]; then
          echo "✅ 应用健康检查通过"
          break
        else
          echo "⏳ 等待应用就绪... ($i/30)"
          sleep 10
        fi
      done
      
      if [ "$response" != "200" ]; then
        echo "❌ 应用健康检查失败"
        exit 1
      fi
  only:
    - tags
  when: manual

# 清理工作
cleanup:
  stage: .post
  script:
    - echo "清理工作空间..."
    - docker system prune -f
  when: always

自动化测试脚本

集成测试配置

bash 复制代码
#!/bin/bash
# run-integration-tests.sh - 集成测试脚本

set -euo pipefail

# 配置
TEST_TIMEOUT=300
SERVICE_URL="http://localhost:8081"
REPORT_DIR="target/integration-reports"

# 颜色定义
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'

log() {
    echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')] $1${NC}"
}

wait_for_service() {
    local url=$1
    local timeout=$2
    local start_time=$(date +%s)
    
    log "等待服务启动: $url"
    
    while true; do
        local current_time=$(date +%s)
        local elapsed=$((current_time - start_time))
        
        if [ $elapsed -gt $timeout ]; then
            echo -e "${RED}服务启动超时${NC}"
            return 1
        fi
        
        if curl -s -f "$url/health" > /dev/null 2>&1; then
            log "✅ 服务就绪"
            return 0
        fi
        
        echo "⏳ 等待服务... (${elapsed}s/${timeout}s)"
        sleep 5
    done
}

run_api_tests() {
    log "执行API测试..."
    
    local test_results="$REPORT_DIR/api-test-results.xml"
    
    # 使用curl进行API测试
    cat > "$REPORT_DIR/api-tests.sh" << 'EOF'
#!/bin/bash
set -e

# 健康检查测试
echo "测试健康检查端点..."
response=$(curl -s -w "%{http_code}" -o /dev/null http://localhost:8081/health)
if [ "$response" != "200" ]; then
    echo "健康检查失败: HTTP $response"
    exit 1
fi

# 信息端点测试
echo "测试信息端点..."
info_response=$(curl -s http://localhost:8081/info)
if ! echo "$info_response" | grep -q "java.version"; then
    echo "信息端点测试失败"
    exit 1
fi

echo "所有API测试通过"
EOF
    
    chmod +x "$REPORT_DIR/api-tests.sh"
    
    if "$REPORT_DIR/api-tests.sh"; then
        log "✅ API测试通过"
        return 0
    else
        echo -e "${RED}❌ API测试失败${NC}"
        return 1
    fi
}

run_performance_tests() {
    log "执行性能测试..."
    
    # 使用ab进行简单性能测试
    if command -v ab >/dev/null 2>&1; then
        local perf_results="$REPORT_DIR/performance-results.txt"
        
        ab -n 1000 -c 10 http://localhost:8081/health > "$perf_results" 2>&1 || true
        
        # 分析性能结果
        if grep -q "Failed requests:        0" "$perf_results"; then
            log "✅ 性能测试通过"
        else
            echo -e "${YELLOW}⚠️ 性能测试有失败请求${NC}"
        fi
    else
        echo -e "${YELLOW}⚠️ ab工具未安装,跳过性能测试${NC}"
    fi
}

run_load_tests() {
    log "执行负载测试..."
    
    # 使用siege进行负载测试
    if command -v siege >/dev/null 2>&1; then
        local load_results="$REPORT_DIR/load-results.txt"
        
        siege -b -c 10 -t 30s http://localhost:8081/health > "$load_results" 2>&1 || true
        
        log "负载测试完成"
    else
        echo -e "${YELLOW}⚠️ siege工具未安装,跳过负载测试${NC}"
    fi
}

generate_test_report() {
    log "生成测试报告..."
    
    local report_file="$REPORT_DIR/integration-test-report.html"
    
    cat > "$report_file" << EOF
<!DOCTYPE html>
<html>
<head>
    <title>集成测试报告</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 40px; }
        .summary { background: #f4f4f4; padding: 20px; border-radius: 5px; }
        .success { color: green; }
        .failure { color: red; }
        .warning { color: orange; }
        .test-result { margin: 10px 0; padding: 10px; border-left: 4px solid; }
    </style>
</head>
<body>
    <h1>集成测试报告</h1>
    <div class="summary">
        <h2>测试摘要</h2>
        <p><strong>应用:</strong> SpringBoot Docker Demo</p>
        <p><strong>测试时间:</strong> $(date)</p>
        <p><strong>测试环境:</strong> $SERVICE_URL</p>
    </div>
    
    <h2>详细结果</h2>
    <div class="test-result" style="border-color: green;">
        <h3 class="success">✅ API测试</h3>
        <p>所有API端点测试通过</p>
    </div>
    
    <div class="test-result" style="border-color: orange;">
        <h3 class="warning">⚠️ 性能测试</h3>
        <p>性能测试完成,请查看详细报告</p>
    </div>
    
    <div class="test-result" style="border-color: blue;">
        <h3>📊 负载测试</h3>
        <p>负载测试完成,系统表现稳定</p>
    </div>
</body>
</html>
EOF

    log "测试报告已生成: $report_file"
}

main() {
    log "开始集成测试..."
    
    # 创建报告目录
    mkdir -p "$REPORT_DIR"
    
    # 等待服务启动
    if ! wait_for_service "$SERVICE_URL" $TEST_TIMEOUT; then
        echo -e "${RED}❌ 服务启动失败,无法执行集成测试${NC}"
        exit 1
    fi
    
    # 执行各种测试
    run_api_tests
    run_performance_tests
    run_load_tests
    
    # 生成报告
    generate_test_report
    
    log "🎉 集成测试全部完成"
}

# 执行主函数
main "$@"

冒烟测试脚本

bash 复制代码
#!/bin/bash
# smoke-tests.sh - 冒烟测试脚本

set -euo pipefail

# 配置
SERVICE_URL=${1:-"http://localhost:8080"}
TIMEOUT=60
RETRY_INTERVAL=5

# 颜色定义
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'

log() {
    echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')] $1${NC}"
}

wait_for_service() {
    local url=$1
    local timeout=$2
    local start_time=$(date +%s)
    
    log "等待服务就绪: $url"
    
    while true; do
        local current_time=$(date +%s)
        local elapsed=$((current_time - start_time))
        
        if [ $elapsed -gt $timeout ]; then
            echo -e "${RED}❌ 服务等待超时${NC}"
            return 1
        fi
        
        if curl -s -f "$url/health" > /dev/null 2>&1; then
            log "✅ 服务就绪"
            return 0
        fi
        
        echo "⏳ 等待服务... (${elapsed}s/${timeout}s)"
        sleep $RETRY_INTERVAL
    done
}

test_health_endpoint() {
    log "测试健康检查端点..."
    
    local response
    response=$(curl -s -w "%{http_code}" -o /dev/null "$SERVICE_URL/health")
    
    if [ "$response" = "200" ]; then
        log "✅ 健康检查通过"
        return 0
    else
        echo -e "${RED}❌ 健康检查失败: HTTP $response${NC}"
        return 1
    fi
}

test_info_endpoint() {
    log "测试信息端点..."
    
    local response
    response=$(curl -s "$SERVICE_URL/info")
    
    if echo "$response" | grep -q "java.version"; then
        log "✅ 信息端点测试通过"
        
        # 显示应用信息
        echo "应用信息:"
        echo "$response" | jq '.' 2>/dev/null || echo "$response"
    else
        echo -e "${RED}❌ 信息端点测试失败${NC}"
        return 1
    fi
}

test_database_connectivity() {
    log "测试数据库连接..."
    
    # 这里可以添加数据库连接测试
    # 例如测试数据库迁移状态等
    
    log "✅ 数据库连接测试跳过(需要具体配置)"
    return 0
}

test_external_dependencies() {
    log "测试外部依赖..."
    
    # 测试Redis、消息队列等外部依赖
    
    log "✅ 外部依赖测试跳过(需要具体配置)"
    return 0
}

test_business_endpoints() {
    log "测试业务端点..."
    
    # 测试关键业务API端点
    
    local endpoints=("/health" "/info")
    
    for endpoint in "${endpoints[@]}"; do
        local url="$SERVICE_URL$endpoint"
        local response_code
        
        response_code=$(curl -s -o /dev/null -w "%{http_code}" "$url")
        
        if [ "$response_code" = "200" ] || [ "$response_code" = "404" ]; then
            log "✅ 端点 $endpoint 响应正常: HTTP $response_code"
        else
            echo -e "${RED}❌ 端点 $endpoint 响应异常: HTTP $response_code${NC}"
            return 1
        fi
    done
    
    return 0
}

generate_smoke_test_report() {
    local result=$1
    local report_file="smoke-test-report-$(date +%Y%m%d_%H%M%S).txt"
    
    cat > "$report_file" << EOF
冒烟测试报告
============

测试时间: $(date)
测试环境: $SERVICE_URL
测试结果: $result

测试项目:
1. 服务健康检查: $( [ $result = "SUCCESS" ] && echo "通过" || echo "失败" )
2. 应用信息端点: $( [ $result = "SUCCESS" ] && echo "通过" || echo "失败" )
3. 数据库连接: 跳过
4. 外部依赖: 跳过
5. 业务端点: $( [ $result = "SUCCESS" ] && echo "通过" || echo "失败" )

建议:
$( [ $result = "SUCCESS" ] && echo "✅ 应用运行正常,可以继续部署流程" || echo "❌ 应用存在问题,请检查部署" )
EOF

    log "测试报告已生成: $report_file"
}

main() {
    local environment=${1:-"unknown"}
    
    log "开始冒烟测试 - 环境: $environment"
    log "服务地址: $SERVICE_URL"
    
    # 等待服务就绪
    if ! wait_for_service "$SERVICE_URL" $TIMEOUT; then
        generate_smoke_test_report "FAILURE"
        exit 1
    fi
    
    # 执行测试项目
    local tests_passed=0
    local tests_failed=0
    
    if test_health_endpoint; then ((tests_passed++)); else ((tests_failed++)); fi
    if test_info_endpoint; then ((tests_passed++)); else ((tests_failed++)); fi
    if test_database_connectivity; then ((tests_passed++)); else ((tests_failed++)); fi
    if test_external_dependencies; then ((tests_passed++)); else ((tests_failed++)); fi
    if test_business_endpoints; then ((tests_passed++)); else ((tests_failed++)); fi
    
    # 生成报告
    if [ $tests_failed -eq 0 ]; then
        log "🎉 冒烟测试全部通过 ($tests_passed 个测试)"
        generate_smoke_test_report "SUCCESS"
        exit 0
    else
        echo -e "${RED}❌ 冒烟测试失败: $tests_passed 通过, $tests_failed 失败${NC}"
        generate_smoke_test_report "FAILURE"
        exit 1
    fi
}

# 执行主函数
main "$@"

CI/CD流水线架构图

以下图表展示了完整的CI/CD流水线架构:

flowchart TD A[开发者提交代码] --> B[Git仓库] B --> C{CI/CD触发器} C -->|Push/MR| D[Jenkins/GitLab CI] subgraph CI阶段 [持续集成] D --> E[代码质量检查] E --> F[单元测试] F --> G[Docker镜像构建] G --> H[镜像推送仓库] end subgraph CD阶段 [持续部署] H --> I[开发环境部署] I --> J[自动化测试] J --> K[预生产环境] K --> L[性能测试] L --> M[生产环境部署] end subgraph 监控反馈 [监控与反馈] M --> N[应用监控] N --> O[性能分析] O --> P[反馈优化] P --> A end E --> E1[SonarQube分析] E --> E2[安全扫描] J --> J1[集成测试] J --> J2[冒烟测试] N --> N1[Prometheus] N --> N2[Grafana] N --> N3[日志分析] style A fill:#3498db,color:#fff style M fill:#27ae60,color:#fff style E fill:#9b59b6,color:#fff style J fill:#e67e22,color:#fff

总结

通过本部分的配置,我们建立了完整的CI/CD流水线,实现了:

核心能力

  1. 自动化构建: 代码提交后自动触发构建流程
  2. 质量门控: 代码检查、测试覆盖率、安全扫描
  3. 环境管理: 开发、测试、预生产、生产多环境部署
  4. 部署策略: 蓝绿部署、滚动更新等高级部署策略

工具集成

  • Jenkins: 企业级CI/CD流水线
  • GitLab CI: 与GitLab深度集成的CI/CD
  • Docker: 容器化构建和部署
  • SonarQube: 代码质量分析
  • 自动化测试: 单元测试、集成测试、冒烟测试

最佳实践

  1. 流水线即代码: 使用Jenkinsfile和.gitlab-ci.yml定义流水线
  2. 质量门控: 在每个阶段设置质量检查点
  3. 环境隔离: 严格的环境分离和权限控制
  4. 监控反馈: 完整的监控和反馈机制

现在SpringBoot应用已经具备了企业级的自动化部署能力,可以实现快速、可靠的应用交付。

相关推荐
用户4099322502125 小时前
想让PostgreSQL查询快到飞起?分区表、物化视图、并行查询这三招灵不灵?
后端·ai编程·trae
Value_Think_Power5 小时前
每次请求时,后端先对比过期时间,如果过期就refresh
后端
用户68545375977695 小时前
🛡️ MyBatis的#{}和${}:安全 vs 危险!
后端
uhakadotcom5 小时前
ChatGPT Atlas的使用笔记
后端·面试·github
得物技术5 小时前
从一次启动失败深入剖析:Spring循环依赖的真相|得物技术
java·后端
程序猿DD5 小时前
Jackson 序列化的隐性成本
java·后端
用户68545375977695 小时前
⚡ Spring Boot自动配置:约定优于配置的魔法!
后端
aicoding_sh5 小时前
为 Claude Code CLI 提供美观且高度可定制的状态行,具有powerline support, themes, and more.
后端·github