AI摘要

文章以实战视角,手把手搭建 Jenkins+Docker 自动化流水线:从痛点出发,设计多阶段 Dockerfile、Jenkinsfile、共享库与凭据管理,集成 SonarQube、Harbor、K8s,实现代码提交到多环境一键部署,并给出网络加速、权限、缓存、监控等常见坑与优化方案,30 分钟发布缩至 5 分钟。

第一部分:痛点与架构设计

为什么需要自动化流水线?

我最初接触持续集成时,团队是这样发布应用的:

  1. 下午6点,在群里喊“开始发布,大家别提交代码”
  2. 负责人手动执行mvn clean package -DskipTests
  3. 将打包好的jar通过scp传到服务器
  4. 登录服务器,备份旧版本,停止服务
  5. 启动新版本,观察日志是否正常
  6. 如果有问题,手动回滚到上一个版本

这个过程至少要30分钟,而且容易出错。更糟糕的是,如果同时有多个服务要发布,需要串行操作,整个发布过程可能持续2-3小时。

我们的技术栈选择

我们选择了这样的组合:

  • Jenkins:流水线编排的核心,选择它是因为插件生态丰富,能集成我们现有的所有工具
  • Docker:解决环境一致性问题
  • Docker Compose:多服务编排(测试环境)
  • Harbor:私有镜像仓库
  • SonarQube:代码质量检查
  • Nexus:私有Maven仓库
  • Kubernetes(可选):生产环境容器编排

以下是完整的架构图:

开发者推送代码
     ↓
Git Webhook触发
     ↓
Jenkins流水线启动
     ↓
    ├── 代码质量检查 (SonarQube)
    ├── 单元测试 (JUnit)
    ├── 构建Docker镜像
    ├── 镜像安全扫描 (Trivy)
    ├── 推送到Harbor仓库
    └── 部署到不同环境
         ├── 开发环境 (Docker Compose)
         ├── 测试环境 (Docker Compose)
         └── 生产环境 (Kubernetes)

第二部分:基础环境搭建的细节

1. Jenkins的“正确”安装方式

很多教程直接使用apt-get install jenkins,但这种方式在管理和升级时很麻烦。我们使用Docker安装:

# 创建Jenkins数据目录和配置目录
mkdir -p /data/jenkins/{home,config}
chmod -R 1000:1000 /data/jenkins

# 创建docker-compose.yml文件
cat > /data/jenkins/docker-compose.yml << EOF
version: '3.8'
services:
  jenkins:
    image: jenkins/jenkins:lts
    container_name: jenkins
    user: root  # 避免权限问题
    ports:
      - "8080:8080"
      - "50000:50000"
    volumes:
      - /data/jenkins/home:/var/jenkins_home
      - /data/jenkins/config:/var/jenkins_config
      - /var/run/docker.sock:/var/run/docker.sock
      - /usr/bin/docker:/usr/bin/docker  # 挂载docker客户端
      - /usr/local/bin/docker-compose:/usr/local/bin/docker-compose
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    environment:
      - JAVA_OPTS=-Duser.timezone=Asia/Shanghai
      - JENKINS_OPTS=--prefix=/jenkins  # 如果你使用反向代理
    restart: unless-stopped
EOF

# 启动Jenkins
cd /data/jenkins && docker-compose up -d

# 查看初始密码
docker logs jenkins 2>&1 | grep "Please use the following password"

这里的关键点:

  • 挂载/var/run/docker.sock让Jenkins能使用宿主机的Docker引擎
  • 同时挂载docker和docker-compose二进制文件
  • 配置时区,避免日志时间混乱
  • 使用docker-compose管理,方便后续升级

2. Jenkins初始配置的坑

首次访问http://服务器IP:8080后,需要:

安装推荐插件时的网络问题:
由于网络原因,插件安装经常失败。我们有三种解决方案:

# 方案1:使用清华镜像源(在Jenkins容器内执行)
docker exec -it jenkins bash
# 修改更新中心地址
sed -i 's/https:\/\/updates.jenkins.io\/download/https:\/\/mirrors.tuna.tsinghua.edu.cn\/jenkins/g' /var/jenkins_home/hudson.model.UpdateCenter.xml
sed -i 's/http:\/\/www.google.com/https:\/\/www.baidu.com/g' /var/jenkins_home/hudson.model.UpdateCenter.xml

# 方案2:手动上传插件(如果网络实在不行)
# 从 https://mirrors.tuna.tsinghua.edu.cn/jenkins/plugins/ 下载以下核心插件:
# - pipeline
# - git
# - docker
# - docker-pipeline
# - blueocean(可视化流水线)
# 然后在 Jenkins 的"管理插件" -> "高级" -> "上传插件"中安装

# 方案3:使用代理(如果有的话)
# 在Jenkins启动参数中添加代理
JAVA_OPTS="-Dhttp.proxyHost=proxy.example.com -Dhttp.proxyPort=8080"

必须安装的插件列表:

  1. Pipeline - 流水线核心插件
  2. Docker Pipeline - Docker集成
  3. Blue Ocean - 可视化流水线界面
  4. Git Parameter - 构建参数化,支持选择Git分支
  5. Build Timestamp - 在构建时添加时间戳
  6. Pipeline Utility Steps - 流水线工具步骤
  7. SonarQube Scanner - 代码质量检查
  8. Email Extension - 邮件通知

3. Docker环境的准备

Docker Daemon配置优化:

# 编辑docker配置文件
sudo vim /etc/docker/daemon.json

# 添加以下配置
{
  "registry-mirrors": [
    "https://docker.mirrors.ustc.edu.cn",
    "https://hub-mirror.c.163.com"
  ],
  "insecure-registries": [
    "192.168.1.100:8083"  # 我们的私有Harbor仓库地址
  ],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m",
    "max-file": "3"
  },
  "data-root": "/data/docker"  # Docker数据目录,避免系统盘爆满
}

# 重启docker
sudo systemctl daemon-reload
sudo systemctl restart docker

Docker Compose安装:

# 下载最新版本(这里以2.20.0为例)
sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

# 验证安装
docker-compose --version

第三部分:项目改造与Dockerfile优化

1. 多阶段构建的Dockerfile详解

对于Spring Boot项目,我们经历了三个版本的Dockerfile优化:

版本1:最简单的Dockerfile(问题:构建慢、镜像大)

FROM openjdk:8-jdk
COPY . /app
WORKDIR /app
RUN ./mvnw clean package -DskipTests
CMD ["java", "-jar", "target/app.jar"]

问题分析:

  • 每次构建都要下载所有Maven依赖
  • 镜像包含JDK(约500MB),而运行只需要JRE
  • 源码在镜像中,存在安全风险

版本2:使用阿里云镜像加速(问题:依赖重复下载)

FROM maven:3.8.5-openjdk-11 AS build
COPY . /app
WORKDIR /app
RUN mvn clean package -DskipTests \
    -Dmaven.test.skip=true \
    -Dmaven.wagon.http.ssl.insecure=true \
    -Dmaven.wagon.http.ssl.allowall=true

FROM openjdk:11-jre-slim
COPY --from=build /app/target/app.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]

改进:

  • 使用多阶段构建,最终镜像只包含JRE
  • 最终镜像大小从500MB降到约150MB

版本3:最优化的Dockerfile(利用分层缓存)

# 第一阶段:构建依赖层(最大程度利用缓存)
FROM maven:3.8.5-openjdk-11 AS deps
WORKDIR /build

# 单独复制pom.xml,利用缓存
COPY pom.xml .
# 下载依赖(-o 离线模式,-B 批处理模式)
RUN mvn dependency:go-offline -B

# 第二阶段:构建应用
FROM deps AS builder
COPY src ./src
# 编译打包(跳过测试)
RUN mvn clean package -DskipTests \
    -Dmaven.test.skip=true \
    -Dmaven.wagon.http.ssl.insecure=true \
    -Dmaven.wagon.http.ssl.allowall=true

# 第三阶段:运行时镜像
FROM openjdk:11-jre-slim AS runtime

# 创建非root用户(安全最佳实践)
RUN groupadd -r spring && useradd -r -g spring spring
USER spring:spring

WORKDIR /app

# 复制jar包
COPY --from=builder --chown=spring:spring /build/target/*.jar app.jar

# JVM参数优化
ENV JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof"

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=120s --retries=3 \
    CMD curl -f http://localhost:8080/actuator/health || exit 1

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

关键优化点:

  1. 分层缓存:将依赖下载和源码编译分离,只要pom.xml不变,就不重新下载依赖
  2. 多阶段构建:最终镜像只包含运行时环境
  3. 使用非root用户:提高安全性
  4. JVM参数优化:设置合理的堆内存和GC参数
  5. 健康检查:Docker能监控应用健康状态

2. .dockerignore文件的重要性

很多人忽略.dockerignore文件,但它能显著减少构建上下文大小,加速构建过程:

# 忽略git相关
.git
.gitignore

# 忽略IDE文件
.idea
*.iml
.vscode

# 忽略日志和缓存
logs/
*.log
target/
build/
out/

# 忽略配置文件(通常通过环境变量或挂载方式注入)
*.yml
*.properties
*.config

# 忽略测试相关
*.test
*.spec

# 忽略文档
*.md
docs/

第四部分:Jenkins流水线深度配置

1. Jenkins凭据(Credentials)管理

安全地管理敏感信息是CI/CD的关键。在Jenkins中配置凭据:

步骤1:创建Docker Hub凭据

类型:Username with password
ID:docker-hub-cred
用户名:你的DockerHub用户名
密码:你的DockerHub密码或Access Token(更安全)

步骤2:创建Git凭据

类型:SSH Username with private key
ID:git-ssh-key
用户名:git
私钥:粘贴你的SSH私钥内容

步骤3:创建服务器SSH凭据

类型:SSH Username with private key
ID:prod-server-ssh
用户名:deploy
私钥:部署服务器的SSH私钥

步骤4:创建SonarQube Token

# 在SonarQube中生成Token
# 然后在Jenkins中配置
类型:Secret text
ID:sonarqube-token
Secret:粘贴你的SonarQube Token

2. Jenkins Shared Libraries(共享库)

当有多个项目使用相似的流水线时,使用共享库避免代码重复:

目录结构:

jenkins-shared-library/
├── vars/
│   └── buildPipeline.groovy  # 自定义步骤
├── src/
│   └── org/
│       └── company/
│           └── BuildTools.groovy  # 工具类
└── resources/  # 非Groovy文件

示例:buildPipeline.groovy

def call(Map config = [:]) {
    pipeline {
        agent any
        
        parameters {
            choice(
                name: 'DEPLOY_ENV',
                choices: ['dev', 'test', 'staging'],
                description: '选择部署环境'
            )
            booleanParam(
                name: 'SKIP_TESTS',
                defaultValue: false,
                description: '是否跳过测试'
            )
        }
        
        stages {
            stage('代码检查') {
                steps {
                    script {
                        // 调用共享库中的方法
                        checkoutCode()
                        runCodeAnalysis()
                    }
                }
            }
            
            stage('构建') {
                steps {
                    script {
                        buildAndPushImage(config.imageName)
                    }
                }
            }
            
            stage('部署') {
                steps {
                    script {
                        deployToEnv(params.DEPLOY_ENV)
                    }
                }
            }
        }
        
        post {
            success {
                sendNotification('构建成功', 'green')
            }
            failure {
                sendNotification('构建失败', 'red')
            }
        }
    }
}

// 在Jenkins中配置共享库
// 管理Jenkins -> 系统配置 -> Global Pipeline Libraries
// Name: company-lib
// Default version: main
// Retrieval method: Modern SCM
// Source Code Management: Git

3. 完整的Jenkinsfile示例

以下是一个企业级Spring Boot项目的完整Jenkinsfile:

pipeline {
    agent {
        docker {
            image 'maven:3.8.5-openjdk-11'
            args '-v /root/.m2:/root/.m2 -v /var/run/docker.sock:/var/run/docker.sock'
            reuseNode true
        }
    }
    
    options {
        // 保留最近10次构建记录
        buildDiscarder(logRotator(numToKeepStr: '10'))
        // 超时设置(2小时)
        timeout(time: 2, unit: 'HOURS')
        // 禁止并行执行(同一时间只能有一个构建)
        disableConcurrentBuilds()
        // 添加时间戳到控制台输出
        timestamps()
    }
    
    parameters {
        choice(
            name: 'BRANCH',
            choices: ['develop', 'release', 'main'],
            description: '选择要构建的分支'
        )
        booleanParam(
            name: 'SKIP_TESTS',
            defaultValue: false,
            description: '跳过测试'
        )
        booleanParam(
            name: 'SKIP_SONAR',
            defaultValue: false,
            description: '跳过代码质量检查'
        )
        string(
            name: 'IMAGE_TAG',
            defaultValue: 'latest',
            description: 'Docker镜像标签'
        )
    }
    
    environment {
        // 项目信息
        PROJECT_NAME = 'user-service'
        GROUP_ID = 'com.example'
        ARTIFACT_ID = 'user-service'
        
        // Docker配置
        DOCKER_REGISTRY = 'harbor.example.com'
        DOCKER_PROJECT = 'microservices'
        DOCKER_IMAGE = "${DOCKER_REGISTRY}/${DOCKER_PROJECT}/${PROJECT_NAME}"
        
        // SonarQube配置
        SONAR_HOST_URL = 'http://sonarqube.example.com:9000'
        
        // Nexus配置
        NEXUS_URL = 'http://nexus.example.com:8081'
        MAVEN_SETTINGS = credentials('maven-settings')
        
        // 其他
        BUILD_TIMESTAMP = sh(script: "date '+%Y%m%d%H%M%S'", returnStdout: true).trim()
        GIT_COMMIT_SHORT = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim()
    }
    
    stages {
        stage('准备') {
            steps {
                script {
                    echo "========== 构建信息 =========="
                    echo "项目: ${PROJECT_NAME}"
                    echo "分支: ${params.BRANCH}"
                    echo "提交: ${env.GIT_COMMIT_SHORT}"
                    echo "镜像: ${DOCKER_IMAGE}:${params.IMAGE_TAG}"
                    echo "================================"
                    
                    // 检查Docker服务
                    sh 'docker --version'
                    sh 'docker-compose --version'
                }
            }
        }
        
        stage('代码检出') {
            steps {
                checkout([
                    $class: 'GitSCM',
                    branches: [[name: "*/${params.BRANCH}"]],
                    extensions: [
                        // 清理工作空间
                        [$class: 'CleanBeforeCheckout'],
                        // 设置超时
                        [$class: 'CloneOption', timeout: 30],
                        // 设置浅克隆(加速)
                        [$class: 'CloneOption', depth: 1, shallow: true]
                    ],
                    userRemoteConfigs: [[
                        credentialsId: 'git-ssh-key',
                        url: 'git@github.com:company/user-service.git'
                    ]]
                ])
                
                // 获取最近提交信息
                script {
                    LAST_COMMIT = sh(script: "git log -1 --pretty=format:'%h - %s'", returnStdout: true).trim()
                    echo "最近提交: ${LAST_COMMIT}"
                }
            }
        }
        
        stage('代码质量检查') {
            when {
                expression { return !params.SKIP_SONAR.toBoolean() }
            }
            steps {
                withSonarQubeEnv('sonarqube') {
                    sh """
                        mvn clean compile sonar:sonar \
                            -Dsonar.projectKey=${PROJECT_NAME} \
                            -Dsonar.projectName=${PROJECT_NAME} \
                            -Dsonar.host.url=${SONAR_HOST_URL} \
                            -Dsonar.login=${SONAR_TOKEN} \
                            -Dsonar.java.binaries=target/classes
                    """
                }
            }
        }
        
        stage('等待质量门检查') {
            when {
                expression { return !params.SKIP_SONAR.toBoolean() }
            }
            steps {
                timeout(time: 5, unit: 'MINUTES') {
                    waitForQualityGate abortPipeline: true
                }
            }
        }
        
        stage('依赖下载') {
            steps {
                sh '''
                    echo "使用本地Maven缓存..."
                    mvn dependency:go-offline -B -s $MAVEN_SETTINGS
                '''
            }
        }
        
        stage('单元测试') {
            when {
                expression { return !params.SKIP_TESTS.toBoolean() }
            }
            steps {
                sh '''
                    echo "运行单元测试..."
                    mvn test -B -s $MAVEN_SETTINGS
                '''
                
                // 收集测试报告
                post {
                    always {
                        junit 'target/surefire-reports/**/*.xml'
                        archiveArtifacts 'target/surefire-reports/**/*'
                    }
                }
            }
        }
        
        stage('集成测试') {
            when {
                expression { return !params.SKIP_TESTS.toBoolean() }
            }
            steps {
                sh '''
                    echo "启动测试环境..."
                    docker-compose -f docker-compose.test.yml up -d
                    
                    echo "等待服务启动..."
                    sleep 30
                    
                    echo "运行集成测试..."
                    mvn verify -B -s $MAVEN_SETTINGS -Pintegration-test
                '''
                
                post {
                    always {
                        echo "清理测试环境..."
                        sh 'docker-compose -f docker-compose.test.yml down -v'
                    }
                }
            }
        }
        
        stage('编译打包') {
            steps {
                sh '''
                    echo "编译打包..."
                    mvn clean package -B -s $MAVEN_SETTINGS -DskipTests
                    
                    echo "检查生成的jar包..."
                    ls -lh target/*.jar
                '''
            }
        }
        
        stage('构建Docker镜像') {
            steps {
                script {
                    // 构建镜像
                    sh """
                        docker build \
                            --build-arg BUILD_VERSION=${BUILD_TIMESTAMP} \
                            --build-arg GIT_COMMIT=${GIT_COMMIT_SHORT} \
                            -t ${DOCKER_IMAGE}:${params.IMAGE_TAG} \
                            -t ${DOCKER_IMAGE}:${BUILD_TIMESTAMP} \
                            -t ${DOCKER_IMAGE}:${GIT_COMMIT_SHORT} \
                            .
                    """
                    
                    // 查看镜像信息
                    sh "docker images | grep ${PROJECT_NAME}"
                }
            }
        }
        
        stage('镜像安全扫描') {
            steps {
                script {
                    // 使用Trivy进行安全扫描
                    sh """
                        docker run --rm \
                            -v /var/run/docker.sock:/var/run/docker.sock \
                            -v /tmp/trivy-cache:/root/.cache \
                            aquasec/trivy image \
                            --severity HIGH,CRITICAL \
                            --exit-code 1 \
                            --no-progress \
                            ${DOCKER_IMAGE}:${params.IMAGE_TAG}
                    """
                }
            }
        }
        
        stage('推送镜像') {
            steps {
                script {
                    // 登录私有仓库
                    withCredentials([usernamePassword(
                        credentialsId: 'harbor-cred',
                        passwordVariable: 'DOCKER_PASSWORD',
                        usernameVariable: 'DOCKER_USERNAME'
                    )]) {
                        sh """
                            echo \$DOCKER_PASSWORD | docker login ${DOCKER_REGISTRY} \
                                -u \$DOCKER_USERNAME \
                                --password-stdin
                        """
                    }
                    
                    // 推送镜像
                    sh """
                        docker push ${DOCKER_IMAGE}:${params.IMAGE_TAG}
                        docker push ${DOCKER_IMAGE}:${BUILD_TIMESTAMP}
                        docker push ${DOCKER_IMAGE}:${GIT_COMMIT_SHORT}
                    """
                    
                    // 清理本地镜像
                    sh """
                        docker rmi ${DOCKER_IMAGE}:${params.IMAGE_TAG} || true
                        docker rmi ${DOCKER_IMAGE}:${BUILD_TIMESTAMP} || true
                        docker rmi ${DOCKER_IMAGE}:${GIT_COMMIT_SHORT} || true
                    """
                }
            }
        }
        
        stage('部署到开发环境') {
            when {
                expression { 
                    return params.BRANCH == 'develop' 
                }
            }
            steps {
                script {
                    withCredentials([sshUserPrivateKey(
                        credentialsId: 'dev-server-ssh',
                        keyFileVariable: 'SSH_KEY'
                    )]) {
                        sh """
                            ssh -o StrictHostKeyChecking=no -i $SSH_KEY deploy@dev-server << 'EOF'
                                # 拉取最新镜像
                                docker pull ${DOCKER_IMAGE}:${params.IMAGE_TAG}
                                
                                # 停止并删除旧容器
                                docker stop ${PROJECT_NAME} || true
                                docker rm ${PROJECT_NAME} || true
                                
                                # 运行新容器
                                docker run -d \\
                                    --name ${PROJECT_NAME} \\
                                    --network=my-network \\
                                    --restart=unless-stopped \\
                                    -p 8080:8080 \\
                                    -e "SPRING_PROFILES_ACTIVE=dev" \\
                                    -e "TZ=Asia/Shanghai" \\
                                    -v /app/logs:/app/logs \\
                                    ${DOCKER_IMAGE}:${params.IMAGE_TAG}
                                
                                # 健康检查
                                sleep 10
                                if curl -f http://localhost:8080/actuator/health; then
                                    echo "部署成功"
                                else
                                    echo "部署失败"
                                    exit 1
                                fi
                            EOF
                        """
                    }
                }
            }
        }
        
        stage('部署到测试环境') {
            when {
                expression { 
                    return params.BRANCH == 'release' 
                }
            }
            steps {
                script {
                    withCredentials([sshUserPrivateKey(
                        credentialsId: 'test-server-ssh',
                        keyFileVariable: 'SSH_KEY'
                    )]) {
                        sh """
                            ssh -o StrictHostKeyChecking=no -i $SSH_KEY deploy@test-server << 'EOF'
                                # 使用docker-compose部署
                                export IMAGE_TAG=${params.IMAGE_TAG}
                                docker-compose -f /opt/apps/${PROJECT_NAME}/docker-compose.yml pull
                                docker-compose -f /opt/apps/${PROJECT_NAME}/docker-compose.yml up -d
                                
                                # 等待并检查服务状态
                                for i in {1..10}; do
                                    if docker-compose -f /opt/apps/${PROJECT_NAME}/docker-compose.yml ps | grep -q "Up"; then
                                        echo "服务启动成功"
                                        break
                                    fi
                                    echo "等待服务启动... (\$i/10)"
                                    sleep 10
                                done
                            EOF
                        """
                    }
                }
            }
        }
        
        stage('生产环境部署审批') {
            when {
                expression { 
                    return params.BRANCH == 'main' 
                }
            }
            steps {
                script {
                    // 发送部署通知
                    emailext(
                        subject: "待审批:${PROJECT_NAME} 生产环境部署",
                        body: """
                        项目:${PROJECT_NAME}
                        分支:${params.BRANCH}
                        版本:${params.IMAGE_TAG}
                        提交:${GIT_COMMIT_SHORT}
                        最近提交:${LAST_COMMIT}
                        
                        请审批是否部署到生产环境。
                        """,
                        to: 'devops-team@example.com'
                    )
                    
                    // 等待人工审批
                    timeout(time: 24, unit: 'HOURS') {
                        input(
                            message: '确认部署到生产环境?',
                            ok: '确认部署',
                            parameters: [
                                string(
                                    name: 'CONFIRMATION',
                                    defaultValue: '我已确认检查清单',
                                    description: '请输入确认信息'
                                )
                            ]
                        )
                    }
                }
            }
        }
        
        stage('部署到生产环境') {
            when {
                expression { 
                    return params.BRANCH == 'main' 
                }
            }
            steps {
                script {
                    // 使用Kubernetes部署(如果使用K8s)
                    sh """
                        kubectl set image deployment/${PROJECT_NAME} \
                            ${PROJECT_NAME}=${DOCKER_IMAGE}:${params.IMAGE_TAG} \
                            -n production
                        
                        kubectl rollout status deployment/${PROJECT_NAME} -n production --timeout=300s
                    """
                }
            }
        }
    }
    
    post {
        always {
            // 清理工作空间
            cleanWs()
            
            // 更新构建状态
            script {
                currentBuild.description = "分支: ${params.BRANCH}, 镜像: ${params.IMAGE_TAG}, 提交: ${GIT_COMMIT_SHORT}"
            }
        }
        
        success {
            // 成功通知
            script {
                def duration = currentBuild.durationString
                def message = "✅ 构建成功\n" +
                            "项目: ${PROJECT_NAME}\n" +
                            "分支: ${params.BRANCH}\n" +
                            "版本: ${params.IMAGE_TAG}\n" +
                            "耗时: ${duration}\n" +
                            "构建: ${env.BUILD_URL}"
                
                // 发送Slack通知
                slackSend(
                    channel: '#deployments',
                    color: 'good',
                    message: message
                )
                
                // 发送邮件通知
                emailext(
                    subject: "构建成功: ${PROJECT_NAME} - ${params.BRANCH}",
                    body: message,
                    to: 'dev-team@example.com'
                )
            }
        }
        
        failure {
            // 失败通知
            script {
                def message = "❌ 构建失败\n" +
                            "项目: ${PROJECT_NAME}\n" +
                            "分支: ${params.BRANCH}\n" +
                            "构建: ${env.BUILD_URL}\n" +
                            "日志: ${env.BUILD_URL}console"
                
                slackSend(
                    channel: '#deployments',
                    color: 'danger',
                    message: message
                )
                
                // 获取失败阶段的日志
                def failedStage = currentBuild.rawBuild.getExecution().getStages()
                    .find { it.status.toString() == 'FAILED' }?.getDisplayName()
                
                if (failedStage) {
                    echo "失败阶段: ${failedStage}"
                }
            }
        }
        
        unstable {
            // 不稳定通知(如测试失败)
            echo '构建结果不稳定,请检查测试报告'
        }
    }
}

4. 辅助配置文件

docker-compose.test.yml(集成测试用):

version: '3.8'
services:
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: test_db
    ports:
      - "3306:3306"
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 3

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3

  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      SPRING_PROFILES_ACTIVE: test
      DB_URL: jdbc:mysql://mysql:3306/test_db
      REDIS_HOST: redis
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy

5. Jenkins流水线调试技巧

调试方法1:使用echo打印变量

stage('调试') {
    steps {
        script {
            echo "当前分支: ${env.BRANCH_NAME}"
            echo "构建编号: ${env.BUILD_NUMBER}"
            echo "工作目录: ${env.WORKSPACE}"
            
            // 打印所有环境变量
            sh 'printenv | sort'
        }
    }
}

调试方法2:使用timeoutretry

stage('部署') {
    steps {
        retry(3) {  // 重试3次
            timeout(time: 5, unit: 'MINUTES') {  // 超时5分钟
                script {
                    sh './deploy.sh'
                }
            }
        }
    }
}

调试方法3:条件执行

stage('条件部署') {
    when {
        // 仅在特定分支执行
        branch 'main|release'
        
        // 并且不是周末
        expression { 
            def day = new Date().format('u')
            return day != '6' && day != '7'
        }
        
        // 并且环境变量不为空
        environment name: 'DEPLOY_ENV', value: 'production'
    }
    steps {
        // 部署步骤
    }
}

第五部分:常见问题与解决方案

问题1:Docker构建时Maven依赖下载慢

解决方案:使用本地Nexus仓库

  1. 搭建Nexus私有仓库:
docker run -d \
  --name nexus \
  -p 8081:8081 \
  -v /data/nexus:/nexus-data \
  sonatype/nexus3
  1. 配置Maven的settings.xml
<mirror>
  <id>nexus</id>
  <mirrorOf>*</mirrorOf>
  <url>http://nexus.example.com:8081/repository/maven-public/</url>
</mirror>
  1. 在Jenkins中挂载配置:
agent {
    docker {
        image 'maven:3.8.5-openjdk-11'
        args '-v /root/.m2:/root/.m2 -v /data/maven-settings.xml:/root/.m2/settings.xml'
    }
}

问题2:流水线执行权限问题

解决方案:正确配置Jenkins用户

pipeline {
    agent any
    
    tools {
        maven 'Maven-3.8.5'
        jdk 'JDK-11'
    }
    
    environment {
        // 在Docker容器中运行
        DOCKER_HOST = 'unix:///var/run/docker.sock'
    }
    
    stages {
        stage('构建') {
            steps {
                // 使用withCredentials包装敏感操作
                withCredentials([
                    usernamePassword(
                        credentialsId: 'docker-cred',
                        usernameVariable: 'DOCKER_USER',
                        passwordVariable: 'DOCKER_PASS'
                    )
                ]) {
                    sh '''
                        echo $DOCKER_PASS | docker login -u $DOCKER_USER --password-stdin
                        docker build -t myapp .
                    '''
                }
            }
        }
    }
}

问题3:多模块项目构建

对于Maven多模块项目,需要特殊处理:

stage('多模块构建') {
    steps {
        script {
            // 获取所有子模块
            def modules = sh(script: 'find . -name "pom.xml" -type f | xargs dirname | grep -v "^.$"', returnStdout: true).trim().split('\n')
            
            modules.each { module ->
                dir(module) {
                    echo "构建模块: ${module}"
                    sh "mvn clean install -DskipTests"
                    
                    // 如果模块有Dockerfile,构建镜像
                    if (fileExists('Dockerfile')) {
                        def imageName = module.replace('/', '-').replace(' ', '-')
                        sh "docker build -t ${imageName}:${env.BUILD_ID} ."
                    }
                }
            }
        }
    }
}

问题4:构建触发策略

GitHub Webhook自动触发:

properties([
    pipelineTriggers([
        // GitHub push事件触发
        [
            $class: 'GitHubPushTrigger',
            adminList: '',
            allowMembersOfWhitelistedOrgsAsAdmin: false,
            cron: '',
            triggerOnEvents: [
                [
                    $class: 'GitHubPushTriggerEvent'
                ]
            ]
        ],
        // 定时构建
        cron('H */4 * * *')
    ])
])

根据分支不同策略:

// 在Jenkinsfile开始处添加
def isMainBranch = env.BRANCH_NAME == 'main'
def isReleaseBranch = env.BRANCH_NAME.startsWith('release/')
def isFeatureBranch = env.BRANCH_NAME.startsWith('feature/')

pipeline {
    agent any
    
    stages {
        stage('代码检查') {
            when {
                // 所有分支都执行
                anyOf {
                    branch 'main'
                    branch 'release/*'
                    branch 'feature/*'
                }
            }
            steps {
                // SonarQube检查
            }
        }
        
        stage('测试') {
            when {
                // 仅非main分支执行完整测试
                not { branch 'main' }
            }
            steps {
                // 运行测试
            }
        }
        
        stage('部署到开发环境') {
            when {
                // 仅feature分支自动部署到开发环境
                branch 'feature/*'
            }
            steps {
                // 部署
            }
        }
    }
}

第六部分:监控与优化

1. Jenkins构建监控

添加构建度量:

post {
    always {
        script {
            // 记录构建指标
            def duration = currentBuild.duration
            def success = currentBuild.resultIsBetterOrEqualTo('SUCCESS')
            
            // 可以发送到监控系统
            echo "构建耗时: ${duration}ms"
            echo "构建结果: ${success ? '成功' : '失败'}"
            
            // 清理旧的构建记录
            def buildsToKeep = isMainBranch ? 20 : 10
            buildDiscarder(logRotator(numToKeepStr: buildsToKeep.toString()))
        }
    }
}

2. Docker镜像优化

使用多架构构建:

stage('多架构构建') {
    steps {
        script {
            // 安装构建x插件
            sh '''
                docker run --privileged --rm tonistiigi/binfmt --install all
                docker buildx create --name multiarch --use
            '''
            
            // 构建并推送多架构镜像
            sh """
                docker buildx build \
                    --platform linux/amd64,linux/arm64 \
                    -t ${DOCKER_IMAGE}:${params.IMAGE_TAG} \
                    -t ${DOCKER_IMAGE}:latest \
                    --push .
            """
        }
    }
}

3. 流水线性能优化

并行执行:

stage('并行测试') {
    parallel {
        stage('单元测试') {
            steps {
                sh 'mvn test -Dtest=UnitTest*'
            }
        }
        stage('集成测试') {
            steps {
                sh 'mvn test -Dtest=IntegrationTest*'
            }
        }
        stage('API测试') {
            steps {
                sh 'mvn test -Dtest=ApiTest*'
            }
        }
    }
}

缓存优化:

stage('依赖缓存') {
    steps {
        // 使用Docker镜像层缓存
        sh '''
            docker build \
                --cache-from ${DOCKER_IMAGE}:latest \
                -t ${DOCKER_IMAGE}:${BUILD_NUMBER} \
                .
        '''
    }
}

总结

搭建基于Jenkins和Docker的CI/CD流水线是一个系统工程,需要关注:

  1. 环境一致性:通过Docker确保开发、测试、生产环境一致
  2. 流程自动化:从代码提交到部署上线全流程自动化
  3. 质量门禁:通过代码检查、测试、安全扫描保证质量
  4. 安全可靠:凭据管理、权限控制、审计日志
  5. 可观测性:构建监控、日志收集、告警通知

这套流水线实施后,我们的发布效率提升了10倍以上,发布窗口从原来的1小时缩短到5分钟,而且完全消除了人为操作失误。更重要的是,开发人员可以专注于代码编写,运维人员可以专注于基础设施,实现了真正的DevOps协同。

记住,CI/CD不是一蹴而就的,而是需要持续改进的过程。从最简单的自动化构建开始,逐步添加测试、部署、监控等环节,最终形成适合自己团队的完整流水线。

版权声明 ▶ 本网站名称:黄磊的博客
▶ 本文标题:Jenkins与Docker自动化流水线深度实战:从手动发布到一键交付
▶ 本文链接:https://www.huangleicole.com/operations-and-maintenance/79.html
▶ 转载本站文章需要遵守:商业转载请联系站长,非商业转载请注明出处!!

如果觉得我的文章对你有用,请随意赞赏