从手动部署到一键发版:Java项目CI/CD流水线搭建实录

我们组之前的发版流程是这样的:

  1. 开发本地 mvn package
  2. SCP上传jar到服务器
  3. SSH登录服务器
  4. kill -9 旧进程
  5. nohup java -jar 启动新版本
  6. 看日志确认启动成功

这个过程有4个致命问题

问题 后果
人工操作 忘记一个步骤就出事
kill -9 正在处理的请求直接断掉
无法回滚 出问题只能紧急手动回
没有审批 谁都能随时部署到生产

因此自动化部署迫在眉睫,从0搭建一套完整的CI/CD流水线:GitLab → Jenkins → Docker → K8s,实现提交代码后自动测试、构建、部署,生产环境一键发版+审批+回滚。


一、CI/CD流水线全景图

复制代码
    CI/CD流水线全流程                          

  开发提交代码 → GitLab                                                                                        
  【CI阶段 - 持续集成】                                       
    代码拉取                                               
    单元测试 + 代码质量检查                                  
    Maven构建打包                                          
    Docker镜像构建                                         
    推送到私有镜像仓库                                      
                                               
  【CD阶段 - 持续部署】                                       
    自动部署到开发环境(dev)                                 
    自动部署到测试环境(test)                                
    人工审批 → 部署到预发环境(staging)                      
    人工审批 → 部署到生产环境(prod)                         
                                              
  【监控 + 回滚】                                             
    自动健康检查                                            
    异常自动回滚                                            
    正常则标记版本                                          

二、环境准备

组件 版本 用途
GitLab 16.x 代码仓库 + Webhook触发
Jenkins 2.426 CI/CD引擎
Harbor 2.10 私有Docker镜像仓库
K8s 1.28 运行环境

Jenkins插件安装

必须安装的插件:

  • Pipeline:流水线核心
  • Git:拉取代码
  • Docker Pipeline:构建镜像
  • Kubernetes CLI:部署到K8s
  • Pipeline Stage View:流水线可视化
  • Generic Webhook Trigger:GitLab触发构建
  • Promoted Builds:审批部署

三、Jenkinsfile完整流水线

这是本文最核心的内容,线上用了1年多的流水线模板

groovy 复制代码
// Jenkinsfile
pipeline {
    agent any
    
    environment {
        // 项目配置
        APP_NAME          = 'easy-platform'
        DOCKER_REGISTRY   = 'registry.your-company.com'
        IMAGE_NAME        = "${DOCKER_REGISTRY}/${APP_NAME}"
        
        // Git配置
        GIT_REPO          = 'git@gitlab.your-company.com:devops/easy-platform.git'
        GIT_CREDENTIALS   = 'gitlab-ssh-key'
        
        // K8s配置
        K8S_CREDENTIALS   = 'k8s-config'
        K8S_NAMESPACE_DEV = 'easy-platform-dev'
        K8S_NAMESPACE_TEST= 'easy-platform-test'
        K8S_NAMESPACE_PROD= 'easy-platform'
        
        // 镜像Tag:分支名-构建号-提交哈希前7位
        IMAGE_TAG         = "${env.BRANCH_NAME ?: 'main'}-${env.BUILD_NUMBER}-${sh(script:'git rev-parse --short HEAD', returnStdout:true).trim()}"
    }
    
    tools {
        maven 'Maven-3.8'
        jdk   'JDK-1.8'
    }
    
    stages {
        // ========== CI阶段 ==========
        
        stage('拉取代码') {
            steps {
                checkout([
                    $class: 'GitSCM',
                    branches: [[name: "*/${env.BRANCH_NAME ?: 'main'}"]],
                    userRemoteConfigs: [[
                        url: env.GIT_REPO,
                        credentialsId: env.GIT_CREDENTIALS
                    ]]
                ])
            }
        }
        
        stage('代码质量检查') {
            steps {
                sh 'mvn checkstyle:check -DskipTests'
            }
        }
        
        stage('单元测试') {
            steps {
                sh 'mvn test'
            }
            post {
                always {
                    junit '*/target/surefire-reports/*.xml'
                }
            }
        }
        
        stage('Maven构建') {
            steps {
                sh 'mvn clean package -DskipTests'
            }
        }
        
        stage('构建Docker镜像') {
            steps {
                script {
                    docker.withRegistry("https://${env.DOCKER_REGISTRY}", 'harbor-credentials') {
                        def appImage = docker.build(
                            "${env.IMAGE_NAME}:${env.IMAGE_TAG}",
                            '--build-arg BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) .'
                        )
                        appImage.push()
                        // 同时打上latest标签
                        appImage.push('latest')
                    }
                }
            }
        }
        
        // ========== CD阶段 - 开发环境 ==========
        
        stage('部署-开发环境') {
            when {
                branch 'develop'
            }
            steps {
                script {
                    deployToK8s(env.K8S_NAMESPACE_DEV, env.IMAGE_TAG)
                }
            }
        }
        
        // ========== CD阶段 - 测试环境 ==========
        
        stage('部署-测试环境') {
            when {
                branch 'release/*'
            }
            steps {
                script {
                    deployToK8s(env.K8S_NAMESPACE_TEST, env.IMAGE_TAG)
                }
            }
        }
        
        // ========== CD阶段 - 预发环境(需审批)==========
        
        stage('审批-预发环境') {
            when {
                branch 'main'
            }
            steps {
                input message: '确认部署到预发环境?', ok: '确认部署', 
                      submitter: 'tech-lead,devops-lead'
            }
        }
        
        stage('部署-预发环境') {
            when {
                branch 'main'
            }
            steps {
                script {
                    deployToK8s("${env.K8S_NAMESPACE_PROD}-staging", env.IMAGE_TAG)
                }
            }
        }
        
        // ========== CD阶段 - 生产环境(需审批)==========
        
        stage('审批-生产环境') {
            when {
                branch 'main'
            }
            steps {
                input message: '''确认部署到生产环境?
                    请确认:
                    1. 预发环境验证通过
                    2. 已通知相关团队
                    3. 回滚方案已确认''', 
                    ok: '确认上线', 
                    submitter: 'tech-lead,cto'
            }
        }
        
        stage('部署-生产环境') {
            when {
                branch 'main'
            }
            steps {
                script {
                    deployToK8s(env.K8S_NAMESPACE_PROD, env.IMAGE_TAG)
                }
            }
        }
        
        // ========== 健康检查 ==========
        
        stage('健康检查') {
            when {
                anyOf {
                    branch 'main'
                    branch 'release/*'
                }
            }
            steps {
                script {
                    def namespace = env.BRANCH_NAME == 'main' ? env.K8S_NAMESPACE_PROD : env.K8S_NAMESPACE_TEST
                    echo '等待Pod就绪...'
                    sh """
                        kubectl rollout status deployment/${env.APP_NAME} \
                            -n ${namespace} \
                            --timeout=300s
                    """
                    
                    // 检查应用健康
                    sh """
                        sleep 30
                        kubectl exec deployment/${env.APP_NAME} -n ${namespace} -- \
                            curl -sf http://localhost:8080/actuator/health || exit 1
                    """
                }
            }
        }
    }
    
    // ========== 后置处理 ==========
    
    post {
        success {
            echo '✅ 流水线执行成功!'
            sendNotification('SUCCESS')
        }
        failure {
            echo '❌ 流水线执行失败!'
            sendNotification('FAILURE')
            // 生产环境失败自动回滚
            script {
                if (env.BRANCH_NAME == 'main') {
                    echo '生产环境部署失败,自动回滚...'
                    sh """
                        kubectl rollout undo deployment/${env.APP_NAME} \
                            -n ${env.K8S_NAMESPACE_PROD}
                    """
                }
            }
        }
        always {
            // 清理工作空间
            cleanWs()
        }
    }
}

// ========== 工具函数 ==========

def deployToK8s(String namespace, String imageTag) {
    sh """
        kubectl set image deployment/${env.APP_NAME} \
            ${env.APP_NAME}=${env.IMAGE_NAME}:${imageTag} \
            -n ${namespace} \
            --record
        
        kubectl rollout status deployment/${env.APP_NAME} \
            -n ${namespace} \
            --timeout=180s
    """
}

def sendNotification(String status) {
    def color = status == 'SUCCESS' ? '#00CC00' : '#FF0000'
    def emoji = status == 'SUCCESS' ? '✅' : '❌'
    
    // 钉钉通知
    dingtalk(
        robot: 'jenkins-dingtalk',
        type: 'MARKDOWN',
        title: "${emoji} 构建通知",
        text: [
            "### ${emoji} 构建通知",
            "- **项目**: ${env.APP_NAME}",
            "- **分支**: ${env.BRANCH_NAME}",
            "- **版本**: ${env.IMAGE_TAG}",
            "- **状态**: ${status}",
            "- **构建人**: ${env.BUILD_USER ?: '自动触发'}",
            "- **耗时**: ${currentBuild.durationString}",
            "- [查看详情](${env.BUILD_URL})"
        ]
    )
}

四、GitLab Webhook自动触发

配置Webhook

GitLab → 项目 → Settings → Webhooks:

配置项
URL http://jenkins.your-company.com/generic-webhook-trigger/invoke?token=easy-platform
Trigger - Push ✅ 勾选
Trigger - Merge Request ✅ 勾选(merged时触发)
Secret Token your-webhook-secret

触发规则

分支 触发条件 自动部署到
develop Push/MR合并 开发环境
release/* Push/MR合并 测试环境
main MR合并(保护分支) 预发 → 审批 → 生产

五、多环境配置管理

不同环境使用不同的配置,通过K8s ConfigMap管理:

yaml 复制代码
# config-dev.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: easy-platform-dev
data:
  SPRING_PROFILES_ACTIVE: "development"
  JAVA_OPTS: "-Xms256m -Xmx256m"
---
# config-test.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: easy-platform-test
data:
  SPRING_PROFILES_ACTIVE: "test"
  JAVA_OPTS: "-Xms512m -Xmx512m"
---
# config-prod.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: easy-platform
data:
  SPRING_PROFILES_ACTIVE: "product"
  JAVA_OPTS: "-Xms1g -Xmx1g -XX:+UseG1GC"

不同环境资源配置对比

环境 副本数 内存限制 CPU限制 数据库
开发 1 512Mi 250m H2/内嵌MySQL
测试 2 1Gi 500m 独立MySQL实例
预发 2 2Gi 1000m 与生产同配置
生产 3+ 2Gi 1000m 云RDS + 读写分离

六、回滚方案

方案1:K8s回滚(秒级)

bash 复制代码
# 查看部署历史
kubectl rollout history deployment/easy-platform -n easy-platform

# 回滚到上一版本
kubectl rollout undo deployment/easy-platform -n easy-platform

# 回滚到指定版本
kubectl rollout undo deployment/easy-platform --to-revision=3 -n easy-platform

方案2:重新部署历史镜像

bash 复制代码
# 查看Harbor镜像历史
# 重新部署指定版本
kubectl set image deployment/easy-platform \
  easy-platform=registry.your-company.com/easy-platform:main-156-a3f8b2c \
  -n easy-platform

方案3:蓝绿部署

yaml 复制代码
# blue-green-deployment.yaml
# 同时运行两个版本,切换Service指向
apiVersion: v1
kind: Service
metadata:
  name: easy-platform
  namespace: easy-platform
spec:
  selector:
    app: easy-platform
    version: blue    # 切换为 green 即可完成切换
  ports:
  - port: 8080
    targetPort: 8080

七、流水线踩过的5个坑

坑1:Jenkins构建时Maven下载依赖太慢

修复:配置阿里云Maven镜像 + 本地仓库缓存

groovy 复制代码
// Jenkinsfile中挂载本地仓库
docker.image('maven:3.8-openjdk-8').inside('-v /data/maven-repo:/root/.m2/repository') {
    sh 'mvn clean package -DskipTests'
}

坑2:Docker镜像构建时找不到Dockerfile

修复:Jenkins配置Docker socket挂载

groovy 复制代码
pipeline {
    agent {
        kubernetes {
            yaml '''
            apiVersion: v1
            kind: Pod
            spec:
              containers:
              - name: docker
                image: docker:24
                securityContext:
                  privileged: true
                volumeMounts:
                - name: docker-sock
                  mountPath: /var/run/docker.sock
              volumes:
              - name: docker-sock
                hostPath:
                  path: /var/run/docker.sock
            '''
        }
    }
}

坑3:K8s部署后Pod启动失败,Jenkins还在等rollout status

修复 :设置合理的 --timeout

bash 复制代码
kubectl rollout status deployment/easy-platform \
    -n easy-platform \
    --timeout=300s    # 5分钟超时

坑4:流水线敏感信息泄露

修复:Jenkins Credentials管理,不要在Jenkinsfile中硬编码

groovy 复制代码
// ❌ 错误
K8S_TOKEN = 'eyJhbGciOiJSUzI1NiIs...'

// ✅ 正确
withCredentials([string(credentialsId: 'k8s-token', variable: 'TOKEN')]) {
    sh "kubectl --token=${TOKEN} apply -f ..."
}

坑5:生产环境审批人不在,流水线卡住

修复:设置超时 + 自动降级

groovy 复制代码
stage('审批-生产环境') {
    steps {
        timeout(time: 2, unit: 'HOURS') {
            input message: '确认部署到生产环境?', 
                  submitter: 'tech-lead,cto'
        }
    }
}

八、发版SOP

复制代码
  1️⃣ 开发完成 → 合并到 develop 分支                         
     → 自动构建 + 部署到开发环境                                                                             
  2️⃣ 开发自测通过 → 创建 release 分支                       
     → 自动构建 + 部署到测试环境                                                                                
  3️⃣ QA测试通过 → 合并到 main 分支                          
     → 自动构建 + 部署到预发环境(需审批)                                                                     
  4️⃣ 预发验证通过 → 发起生产部署(需CTO审批)                 
     → 滚动更新到生产环境                                    
     → 健康检查 + 监控观察15分钟                                                                               
  5️⃣ 异常 → 自动回滚 / 手动回滚                             
     正常 → 标记版本 + 发送通知                                                                                 

面试速答

面试官:说一下你们项目的CI/CD流程?

答:我们用GitLab + Jenkins + Harbor + K8s搭建了完整的CI/CD流水线。开发提交代码后,通过Webhook自动触发Jenkins构建。CI阶段包括代码质量检查、单元测试、Maven构建打包、Docker镜像构建并推送到Harbor私有仓库。CD阶段按分支策略自动部署:develop分支自动部署到开发环境,release分支自动部署到测试环境,main分支合并后需要技术负责人审批才部署到预发环境,CTO审批后才部署到生产。部署采用K8s滚动更新实现零停机,部署后自动做健康检查,失败则自动回滚到上一版本。每次构建的镜像Tag包含分支名、构建号和Git提交哈希,确保可追溯。

相关推荐
星梦清河2 小时前
微服务-Elasticsearch02
微服务·架构·jenkins
终端行者3 小时前
Jenkins Pipeline 构建后推送到Nexus制品库 jenkins 如何连接Nexus?企业级实战 --中 Jenkins 连接Nexus 实战
运维·ci/cd·docker·jenkins·nexus
隔窗听雨眠3 小时前
一份完整的Jenkins故障排查指南
jenkins
终端行者3 小时前
Jenkins Pipeline 构建后推送到Nexus制品库 jenkins 如何连接Nexus?企业级实战 --上 Nexus部署
运维·ci/cd·jenkins·nexus
小闫BI设源码1 天前
当20个节点选出两个Master时:Elasticsearch的致命故障与解决方案
java·elasticsearch·jenkins·php·面试宝典·深入解析
醉颜凉1 天前
Elasticsearch 核心原理:Posting List 倒排列表深度详解
大数据·elasticsearch·jenkins
牛奶咖啡131 天前
CI/CD——在jenkins中构建流程实现springboot项目的自动化构建与部署
java·ci/cd·k8s·jenkins·springboot·springboot制作镜像·使用源码项目制作镜像
honder试试2 天前
Elasticsearch(es)在Windows系统上的安装与部署(含Kibana)
windows·elasticsearch·jenkins
牛奶咖啡132 天前
CI/CD——在jenkins中使用pipeline方式自动化构建java项目jpress
ci/cd·自动化·jenkins·pipeline是什么·pipeline有啥用·pipeline适用场景·pipeline使用示例