Jenkins实战 - 从代码提交到生产部署的完整CI/CD流程

Jenkins实战 - 自动化部署完整流程

一、前言

前面九篇文章,我们从Jenkins基础概念学到了通知配置。现在是时候把所有知识串起来,打造一个完整的CI/CD自动化部署流程了!本文将以一个真实的Spring Boot项目为例,从代码提交到生产部署,实现全自动化的持续交付。

二、项目背景

假设我们有一个Spring Boot + Docker的项目,部署架构如下:

开发流程:
开发人员 → Git Push → Jenkins自动构建 → 自动测试 → 自动部署

环境划分:
- Dev(开发环境):每次push自动部署
- Staging(预发布环境):develop分支合并后自动部署
- Production(生产环境):main分支合并后,需人工确认部署

三、项目准备

3.1 项目结构

my-project/
├── src/
│   ├── main/
│   │   ├── java/
│   │   └── resources/
│   │       └── application.yml
│   └── test/
│       └── java/
├── Dockerfile
├── docker-compose.yml
├── pom.xml
├── Jenkinsfile
├── deploy.sh
└── scripts/
    ├── health-check.sh
    └── rollback.sh

3.2 Dockerfile

# 多阶段构建
# 第一阶段:编译
FROM maven:3.8-openjdk-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests -B

# 第二阶段:运行
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar

ENV JAVA_OPTS="-Xmx512m -Xms256m"
ENV TZ=Asia/Shanghai

EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar app.jar"]

3.3 部署脚本 deploy.sh

#!/bin/bash
set -e

ENV=$1
IMAGE_TAG=$2

case $ENV in
  dev)
    SERVERS=("192.168.1.101")
    ;;
  staging)
    SERVERS=("192.168.1.201")
    ;;
  production)
    SERVERS=("192.168.1.301" "192.168.1.302")
    ;;
  *)
    echo "未知环境: $ENV"
    exit 1
    ;;
esac

echo "========================================="
echo "部署环境: $ENV"
echo "镜像标签: $IMAGE_TAG"
echo "目标服务器: ${SERVERS[*]}"
echo "========================================="

for SERVER in "${SERVERS[@]}"; do
  echo "部署到 $SERVER ..."
  ssh deploy@$SERVER << EOF
    docker pull $IMAGE_TAG
    docker stop my-app || true
    docker rm my-app || true
    docker run -d \
      --name my-app \
      --restart=unless-stopped \
      -p 8080:8080 \
      -e SPRING_PROFILES_ACTIVE=$ENV \
      -v /app/logs:/app/logs \
      $IMAGE_TAG
EOF
  echo "$SERVER 部署完成"
done

echo "所有服务器部署完成!"

3.4 健康检查脚本 health-check.sh

#!/bin/bash
ENV=$1
SERVER=$2

case $ENV in
  dev) SERVER="192.168.1.101" ;;
  staging) SERVER="192.168.1.201" ;;
  production) SERVER="192.168.1.301" ;;
esac

MAX_RETRIES=30
RETRY_COUNT=0

echo "健康检查: http://$SERVER:8080/actuator/health"

while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
  STATUS=$(curl -s -o /dev/null -w "%{http_code}" "http://$SERVER:8080/actuator/health" || echo "000")
  
  if [ "$STATUS" = "200" ]; then
    echo "✅ 健康检查通过!(尝试 $((RETRY_COUNT+1)) 次)"
    exit 0
  fi
  
  RETRY_COUNT=$((RETRY_COUNT+1))
  echo "等待服务启动... ($RETRY_COUNT/$MAX_RETRIES)"
  sleep 5
done

echo "❌ 健康检查失败!服务未在预期时间内启动"
exit 1

四、完整Jenkinsfile

这是本文的核心——一个完整的、生产级别的Jenkinsfile:

// Jenkinsfile - 生产级CI/CD流水线
pipeline {
    agent any
    
    // 全局配置
    options {
        timeout(time: 2, unit: 'HOURS')
        timestamps()
        buildDiscarder(logRotator(numToKeepStr: '30'))
        disableConcurrentBuilds()
        quietPeriod(5)
    }
    
    // 参数配置
    parameters {
        choice(
            name: 'DEPLOY_ENV',
            choices: ['auto', 'dev', 'staging', 'production'],
            description: '部署环境(auto=根据分支自动判断)'
        )
        booleanParam(
            name: 'SKIP_TESTS',
            defaultValue: false,
            description: '跳过测试(紧急修复时使用)'
        )
        booleanParam(
            name: 'FORCE_DEPLOY',
            defaultValue: false,
            description: '强制部署(即使测试失败也部署)'
        )
    }
    
    // 环境变量
    environment {
        APP_NAME = 'my-application'
        REGISTRY = 'registry.example.com'
        IMAGE_NAME = "${REGISTRY}/${APP_NAME}"
        GIT_COMMIT_SHORT = sh(
            returnStdout: true,
            script: 'git rev-parse --short HEAD 2>/dev/null || echo "unknown"'
        ).trim()
    }
    
    stages {
        // ========== 阶段1:代码检出 ==========
        stage('Checkout') {
            steps {
                checkout scm
                script {
                    env.GIT_COMMIT_SHORT = sh(
                        returnStdout: true,
                        script: 'git rev-parse --short HEAD'
                    ).trim()
                    env.IMAGE_TAG = "${IMAGE_NAME}:${BUILD_NUMBER}-${GIT_COMMIT_SHORT}"
                    env.IMAGE_LATEST = "${IMAGE_NAME}:latest"
                    
                    def author = sh(
                        returnStdout: true,
                        script: 'git log -1 --pretty=format:"%an"'
                    ).trim()
                    def message = sh(
                        returnStdout: true,
                        script: 'git log -1 --pretty=format:"%s"'
                    ).trim()
                    
                    echo """
                    ===================================
                    📋 构建信息
                    ===================================
                    提交者: ${author}
                    提交信息: ${message}
                    提交哈希: ${GIT_COMMIT_SHORT}
                    镜像标签: ${IMAGE_TAG}
                    ===================================
                    """
                }
            }
        }
        
        // ========== 阶段2:编译构建 ==========
        stage('Build') {
            agent {
                docker {
                    image 'maven:3.8-openjdk-17'
                    args '-v $HOME/.m2:/root/.m2'
                }
            }
            steps {
                sh 'mvn clean package -DskipTests -B'
            }
            post {
                success {
                    archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
                }
            }
        }
        
        // ========== 阶段3:代码质量 & 测试(并行) ==========
        stage('Quality & Test') {
            parallel {
                stage('Unit Tests') {
                    when {
                        expression { return !params.SKIP_TESTS }
                    }
                    agent {
                        docker {
                            image 'maven:3.8-openjdk-17'
                            args '-v $HOME/.m2:/root/.m2'
                        }
                    }
                    steps {
                        sh 'mvn test -B'
                    }
                    post {
                        always {
                            junit 'target/surefire-reports/**/*.xml'
                        }
                    }
                }
                
                stage('Code Quality') {
                    when {
                        expression { return !params.SKIP_TESTS }
                    }
                    agent {
                        docker {
                            image 'maven:3.8-openjdk-17'
                            args '-v $HOME/.m2:/root/.m2'
                        }
                    }
                    steps {
                        sh 'mvn sonar:sonar -Dsonar.qualitygate.wait=true'
                    }
                }
                
                stage('Security Scan') {
                    when {
                        expression { return !params.SKIP_TESTS }
                    }
                    steps {
                        sh 'mvn org.owasp:dependency-check-maven:check'
                    }
                    post {
                        always {
                            publishHTML(target: [
                                allowMissing: true,
                                alwaysLinkToLastBuild: true,
                                keepAll: true,
                                reportDir: 'target/dependency-check-report',
                                reportFiles: 'dependency-check-report.html',
                                reportName: 'Security Report'
                            ])
                        }
                    }
                }
            }
        }
        
        // ========== 阶段4:构建Docker镜像 ==========
        stage('Docker Build') {
            steps {
                sh "docker build -t ${IMAGE_TAG} -t ${IMAGE_LATEST} ."
                echo "✅ Docker镜像构建完成: ${IMAGE_TAG}"
            }
        }
        
        // ========== 阶段5:推送镜像 ==========
        stage('Docker Push') {
            steps {
                withCredentials([usernamePassword(
                    credentialsId: 'docker-registry-creds',
                    usernameVariable: 'DOCKER_USER',
                    passwordVariable: 'DOCKER_PASS'
                )]) {
                    sh "docker login -u ${DOCKER_USER} -p ${DOCKER_PASS} ${REGISTRY}"
                    sh "docker push ${IMAGE_TAG}"
                    sh "docker push ${IMAGE_LATEST}"
                }
                echo "✅ Docker镜像推送完成"
            }
        }
        
        // ========== 阶段6:部署 ==========
        stage('Determine Environment') {
            steps {
                script {
                    if (params.DEPLOY_ENV == 'auto') {
                        if (env.BRANCH_NAME == 'main') {
                            env.TARGET_ENV = 'production'
                        } else if (env.BRANCH_NAME == 'develop') {
                            env.TARGET_ENV = 'staging'
                        } else {
                            env.TARGET_ENV = 'dev'
                        }
                    } else {
                        env.TARGET_ENV = params.DEPLOY_ENV
                    }
                    echo "🎯 目标部署环境: ${env.TARGET_ENV}"
                }
            }
        }
        
        stage('Deploy') {
            steps {
                script {
                    // 生产环境需要审批
                    if (env.TARGET_ENV == 'production') {
                        timeout(time: 30, unit: 'MINUTES') {
                            input message: """
                            ⚠️ 确认部署到生产环境?
                            
                            镜像: ${IMAGE_TAG}
                            环境: ${env.TARGET_ENV}
                            """, ok: '确认部署'
                        }
                    }
                    
                    // 执行部署
                    sshagent(credentials: ['deploy-ssh-key']) {
                        sh "bash deploy.sh ${env.TARGET_ENV} ${IMAGE_TAG}"
                    }
                }
            }
        }
        
        // ========== 阶段7:健康检查 ==========
        stage('Health Check') {
            steps {
                script {
                    timeout(time: 3, unit: 'MINUTES') {
                        sh "bash scripts/health-check.sh ${env.TARGET_ENV}"
                    }
                    echo '✅ 健康检查通过!服务正常运行'
                }
            }
        }
        
        // ========== 阶段8:验收测试 ==========
        stage('Smoke Test') {
            when {
                expression { env.TARGET_ENV in ['staging', 'production'] }
            }
            steps {
                script {
                    sh 'mvn verify -P smoke-test'
                    echo '✅ 验收测试通过!'
                }
            }
        }
    }
    
    // ========== 构建后处理 ==========
    post {
        always {
            cleanWs()
            sh "docker rmi ${IMAGE_TAG} ${IMAGE_LATEST} || true"
        }
        
        success {
            echo """
            ==========================================
            🎉 部署成功!
            ==========================================
            项目: ${APP_NAME}
            环境: ${env.TARGET_ENV}
            版本: ${BUILD_NUMBER}-${GIT_COMMIT_SHORT}
            镜像: ${IMAGE_TAG}
            ==========================================
            """
        }
        
        failure {
            echo """
            ==========================================
            ❌ 部署失败!
            ==========================================
            项目: ${APP_NAME}
            环境: ${env.TARGET_ENV}
            请检查构建日志排查问题
            ==========================================
            """
        }
    }
}

五、Git分支策略

配合上述Jenkinsfile,推荐使用Git Flow分支策略:

main        ──●──────────●──────────●──  (生产环境)
              \\          /          /
develop     ───●──●──●──●──●──●──●───  (预发布环境)
              \\     /
feature/xxx ───●──●──                    (开发环境)

hotfix/xxx  ────────●──●──                (紧急修复,直接上生产)

release/x.x ──────────●──●──              (发布分支)

不同分支的自动部署策略:

  • feature/* → 自动部署到Dev环境
  • develop → 自动部署到Staging环境
  • main → 需审批后部署到Production
  • hotfix/* → 跳过测试,快速部署到Production

六、回滚方案

6.1 快速回滚脚本

#!/bin/bash
# rollback.sh - 快速回滚到指定版本
set -e

ENV=$1
TARGET_VERSION=$2

if [ -z "$TARGET_VERSION" ]; then
  echo "用法: ./rollback.sh <env> <version>"
  echo "示例: ./rollback.sh production 42"
  exit 1
fi

IMAGE_TAG="registry.example.com/my-application:${TARGET_VERSION}"

echo "⚠️ 回滚到版本: ${TARGET_VERSION}"

# 拉取指定版本镜像并重新部署
ssh deploy@production-server << EOF
  docker pull ${IMAGE_TAG}
  docker stop my-app || true
  docker rm my-app || true
  docker run -d \
    --name my-app \
    --restart=unless-stopped \
    -p 8080:8080 \
    -e SPRING_PROFILES_ACTIVE=${ENV} \
    ${IMAGE_TAG}
EOF

echo "✅ 回滚完成"

6.2 在Jenkins中实现一键回滚

pipeline {
    agent any
    
    parameters {
        booleanParam(name: 'ROLLBACK', defaultValue: false, description: '执行回滚')
        string(name: 'ROLLBACK_VERSION', defaultValue: '', description: '回滚到哪个版本号')
    }
    
    stages {
        stage('Rollback') {
            when {
                expression { return params.ROLLBACK }
            }
            steps {
                script {
                    if (!params.ROLLBACK_VERSION) {
                        error '请指定回滚版本号'
                    }
                    echo "🔄 回滚到版本 ${params.ROLLBACK_VERSION}..."
                    sh "./scripts/rollback.sh production ${params.ROLLBACK_VERSION}"
                    sh "./scripts/health-check.sh production"
                }
            }
        }
        
        // 正常构建流程...
    }
}

七、监控与告警

7.1 构建监控Dashboard

可以安装 Build Monitor View Plugin,创建一个实时监控面板:

  • 1. 点击"+"创建新视图
  • 2. 选择"Build Monitor View"
  • 3. 选择要监控的任务
  • 4. 投影到大屏幕上,团队实时看到构建状态

7.2 构建趋势分析

安装 Build Graph View Plugin,可以查看构建时间趋势图,帮你发现构建变慢的问题。

八、安全最佳实践

  • 最小权限原则:Jenkins使用的部署账号只给必要权限
  • 凭据管理:所有密码、密钥用Jenkins Credentials管理,不要硬编码
  • 审批流程:生产部署必须有人工审批
  • 审计日志:开启Jenkins审计日志,记录所有操作
  • 网络安全:Jenkins不要直接暴露在公网
  • 定期备份:备份Jenkins配置和数据
# 备份Jenkins
# Docker方式
docker cp jenkins:/var/jenkins_home /backup/jenkins-$(date +%Y%m%d)

# 系统安装方式
tar -czf /backup/jenkins-$(date +%Y%m%d).tar.gz /var/lib/jenkins/

九、性能优化建议

  • 增量构建:只构建变更的模块,不是每次全量构建
  • Maven缓存:挂载本地.m2仓库,避免每次下载依赖
  • Docker层缓存:利用Docker BuildKit的缓存功能
  • 并行测试:使用parallel阶段并行运行测试
  • Agent分布式:多台Agent并行构建
  • 浅克隆:大仓库使用shallow clone加速检出
# Maven并行构建
mvn -T 4 clean package  # 使用4个线程构建

# Docker BuildKit缓存
DOCKER_BUILDKIT=1 docker build \
  --cache-from registry.example.com/my-app:latest \
  -t registry.example.com/my-app:${BUILD_NUMBER} .

十、完整CI/CD流程总结

让我们回顾一下整个自动化部署流程:

1. 开发人员提交代码到Git
   ↓
2. Git Webhook触发Jenkins构建
   ↓
3. Jenkins自动拉取代码
   ↓
4. 编译打包(Maven/Gradle)
   ↓
5. 运行单元测试 + 代码质量检查 + 安全扫描(并行)
   ↓
6. 构建Docker镜像
   ↓
7. 推送镜像到镜像仓库
   ↓
8. 根据分支自动判断部署环境
   ↓
9. 生产环境需人工审批
   ↓
10. SSH远程部署到目标服务器
   ↓
11. 健康检查确认服务正常
   ↓
12. 验收测试(Staging/Production)
   ↓
13. 通知团队部署结果

整个流程完全自动化(除了生产部署的审批环节),从代码提交到服务上线,通常只需要5-10分钟!

十一、系列总结

恭喜你完成了Jenkins学习系列的全部十篇文章!回顾一下我们的学习路径:

  • 第1篇:Jenkins入门 - 理解CI/CD概念
  • 第2篇:安装部署 - 搭建Jenkins环境
  • 第3篇:界面操作 - 熟悉Jenkins界面
  • 第4篇:Freestyle项目 - 创建第一个构建任务
  • 第5篇:Pipeline入门 - 代码化构建流程
  • 第6篇:Pipeline进阶 - 条件、并行、矩阵
  • 第7篇:参数化构建 - 灵活控制构建行为
  • 第8篇:Git集成 - Webhook自动触发
  • 第9篇:通知配置 - 及时获取构建状态
  • 第10篇:实战部署 - 完整CI/CD流水线

从零基础到能搭建生产级CI/CD流水线,你已经具备了Jenkins实战的核心能力。在实际工作中,还需要根据项目特点不断调整优化,积累更多经验。祝你CI/CD之路顺利!🎉

发表回复

后才能评论