问题

假如我harbor已经部署好了。k8s也已经部署好了。harbor 是 https://192.168.10.8/ 库是library, 用户是yf,123456, k8s master 192.168.10.9,你过热i我一个简单的java代码,然后我上传到git, 地址是http://192.168.10.7:3000/outsource/easy.git,你给我java代码,然后用jenins pipeline代码一步一步的带我实现,所有的代码都需要给我,webhook的配置也给我。

好的!我将为你提供一个完整的端到端示例,从 Java 代码到 Jenkins Pipeline,再到 Kubernetes 部署的所有细节。以下是详细步骤:


1. 准备一个简单的 Java 应用

这是一个简单的 Spring Boot Web 应用,只有一个接口返回 Hello DevOps!

项目结构

easy-java-app/
├── src/
│   └── main/
│       └── java/
│           └── com/
│               └── example/
│                   └── easyapp/
│                       └── EasyApplication.java
├── pom.xml
├── Dockerfile
└── Jenkinsfile

文件内容

1. EasyApplication.java

package com.example.easyapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
public class EasyApplication {
    public static void main(String[] args) {
        SpringApplication.run(EasyApplication.class, args);
    }
}

@RestController
class HelloController {
    @GetMapping("/")
    public String hello() {
        return "Hello DevOps!";
    }
}

2. pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.0</version>
        <relativePath/>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>easy-java-app</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>easy-java-app</name>
    <description>Demo project for DevOps</description>

    <properties>
        <java.version>11</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

3. Dockerfile

FROM openjdk:11-jre-slim
WORKDIR /app
COPY target/easy-java-app-*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

4. Jenkinsfile

pipeline {
    agent any

    environment {
        HARBOR_URL = "https://192.168.10.8"
        HARBOR_CREDENTIALS = credentials('harbor-credentials') // 需要在Jenkins中提前配置
        KUBE_CONFIG = credentials('kube-config') // Kubernetes配置文件
    }

    stages {
        stage('Checkout') {
            steps {
                git url: 'http://192.168.10.7:3000/outsource/easy.git', branch: 'main'
            }
        }

        stage('Build') {
            steps {
                sh 'mvn clean package -DskipTests'
            }
        }

        stage('Build Docker Image') {
            steps {
                script {
                    def imageName = "${HARBOR_URL}/library/easy-java-app:${env.BUILD_ID}"
                    sh "docker build -t ${imageName} ."
                }
            }
        }

        stage('Push to Harbor') {
            steps {
                script {
                    withCredentials([usernamePassword(credentialsId: 'harbor-credentials', usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD')]) {
                        sh "docker login ${HARBOR_URL} -u ${USERNAME} -p ${PASSWORD}"
                        sh "docker push ${HARBOR_URL}/library/easy-java-app:${env.BUILD_ID}"
                    }
                }
            }
        }

        stage('Deploy to Kubernetes') {
            steps {
                script {
                    // 写入kubeconfig到临时文件
                    writeFile file: 'kubeconfig.yaml', text: "${KUBE_CONFIG}"

                    // 使用kubectl部署
                    sh """
                        export KUBECONFIG=kubeconfig.yaml
                        kubectl create deployment easy-java-app --image=${HARBOR_URL}/library/easy-java-app:${env.BUILD_ID} --dry-run=client -o yaml > deployment.yaml
                        kubectl apply -f deployment.yaml
                        kubectl expose deployment easy-java-app --port=8080 --target-port=8080 --type=NodePort
                    """
                }
            }
        }
    }
}

2. 配置 Jenkins

1) 安装必要插件

  • Git Plugin (已默认安装)
  • Docker Pipeline Plugin
  • Kubernetes CLI Plugin

2) 添加凭据

  1. Harbor 凭据

    • 进入 Jenkins > Manage Jenkins > Manage Credentials
    • 添加用户名密码凭据:
      • ID: harbor-credentials
      • Username: yf
      • Password: 123456
  2. Kubernetes Config

    • 192.168.10.9 (K8s Master) 复制 /root/.kube/config 文件内容
    • 在 Jenkins 中添加 Secret Text 凭据:
      • ID: kube-config
      • Secret: 粘贴 config 文件内容

3) 创建 Pipeline 任务

  1. 新建 Item > Pipeline
  2. 在 Pipeline 配置中选择:
    • Definition: Pipeline script from SCM
    • SCM: Git
    • Repository URL: http://192.168.10.7:3000/outsource/easy.git
    • Branch: main
    • Script Path: Jenkinsfile

3. 配置 Git Webhook (以 Gitea 为例)

1) 在 Gitea 仓库设置中:

  1. 进入 Settings > Webhooks
  2. 添加 Webhook:
    • Payload URL: http://<JENKINS_IP>:8080/github-webhook/
    • Content Type: application/json
    • 触发事件: Just the push event

2) 在 Jenkins 中:

  1. 进入 Pipeline 配置
  2. 勾选 Build Triggers 下的 GitHub hook trigger for GITScm polling

4. 验证部署

1) 检查 Kubernetes 部署

kubectl get pods
kubectl get svc  # 查看NodePort端口

2) 访问应用

通过 http://<Node_IP>:<NodePort> 访问,应看到 Hello DevOps!


关键问题排查点

  1. Docker 登录失败

    • 确保 Jenkins 节点已安装 Docker 并加入 docker 用户组
    • 检查 Harbor 证书是否受信任(可能需要将 Harbor 的 CA 证书添加到 Jenkins 节点的信任链)
  2. Kubectl 权限问题

    • 确保 kubeconfig 文件中的用户有足够权限
  3. 镜像拉取失败

    • 在 Kubernetes 中配置 Harbor 的 imagePullSecret:
      kubectl create secret docker-registry harbor-secret \
      --docker-server=192.168.10.8 \
      --docker-username=yf \
      --docker-password=123456

最终效果

每次代码 push 到 Git 仓库后:

  1. Jenkins 自动触发 Pipeline
  2. 完成编译 → 构建镜像 → 推送镜像 → 部署到 K8s 的全流程
  3. 新版本应用在约 2-3 分钟内完成上线

如果需要更复杂的部署策略(如蓝绿部署),可以进一步扩展 Jenkinsfile 中的部署逻辑。
是的,完全不需要在 Jenkins 服务器上安装 Node.js 环境,甚至连 npm 都不需要装!


工作原理:魔法背后的真相

agent { docker { image 'node:16-alpine' } }

这行代码的魔法过程:

  1. Jenkins 服务器只需要安装 Docker - 这是唯一的前提条件
  2. Pipeline 启动时,Jenkins 会执行:
    # Jenkins 在背后自动执行的命令
    docker pull node:16-alpine
    docker run -v /jenkins/workspace/your-job:/workspace node:16-alpine npm install
  3. 所有操作都在容器内完成
    • npm install → 在容器内执行
    • npm test → 在容器内执行
    • npm run build → 在容器内执行
  4. 执行结束后,容器自动销毁,不留下任何环境污染

实际演示:Jenkins 服务器的干净程度

你可以登录到 Jenkins 服务器验证:

# 在 Jenkins 服务器上执行这些命令
which node          # 返回:not found
which npm           # 返回:not found
node --version      # 返回:command not found
npm --version       # 返回:command not found

# 但是 Docker 是必须的
docker --version    # 返回:Docker version 20.10.x
docker images       # 能看到 node:16-alpine 镜像

这就是容器化的威力:构建环境与运行环境完全解耦!


对比:传统方式 vs Docker Agent 方式

传统方式(需要在 Jenkins 上装环境)

# 管理员需要在每个 Jenkins 节点上执行
curl -fsSL https://deb.nodesource.com/setup_16.x | bash -
apt-get install -y nodejs
npm install -g npm@latest
# 还要处理权限、路径、版本冲突等问题...

Docker Agent 方式(推荐)

// 只需在 Jenkinsfile 中写一行代码
pipeline {
    agent { docker { image 'node:16-alpine' } }
    stages {
        stage('Build') {
            steps {
                sh 'npm install && npm run build'  // 在容器内执行!
            }
        }
    }
}

多版本支持变得极其简单

想象一下,你的团队有多个项目需要不同 Node.js 版本:

项目A(Node.js 14)

// project-a/Jenkinsfile
agent { docker { image 'node:14-alpine' } }

项目B(Node.js 16)

// project-b/Jenkinsfile  
agent { docker { image 'node:16-alpine' } }

项目C(Node.js 18 + Chrome 用于测试)

// project-c/Jenkinsfile
agent { 
    docker { 
        image 'node:18-alpine'
        args '--shm-size=2gb'  # 给 Chrome 测试用
    } 
}

所有这些项目可以在同一个 Jenkins 服务器上并行运行,互不干扰!


高级用法:缓存优化

你可能会问:”每次启动新容器,npm install 都要重新下载依赖,不会很慢吗?”

很好的问题!可以通过挂载缓存目录解决:

agent {
    docker {
        image 'node:16-alpine'
        // 挂载 npm 缓存目录到容器内
        args '-v /home/jenkins/npm-cache:/root/.npm:rw,z'
        reuseNode true
    }
}
stages {
    stage('Install') {
        steps {
            // 利用缓存,第二次构建会快很多
            sh 'npm ci --cache .npm --prefer-offline'
        }
    }
}

真实世界的 Jenkinsfile 示例

pipeline {
    agent { docker { image 'node:16-alpine' } }

    environment {
        // 容器内生效的环境变量
        NODE_ENV = 'production'
        CI = 'true'
    }

    stages {
        stage('Install') {
            steps {
                sh 'npm ci --silent --no-progress'
            }
        }

        stage('Lint') {
            steps {
                sh 'npm run lint'
            }
        }

        stage('Test') {
            steps {
                sh 'npm test -- --coverage --watchAll=false'
            }
            post {
                always {
                    // 发布测试报告
                    publishHTML target: [
                        allowMissing: true,
                        alwaysLinkToLastBuild: true, 
                        keepAll: true,
                        reportDir: 'coverage/lcov-report',
                        reportFiles: 'index.html',
                        reportName: '单元测试覆盖率'
                    ]
                }
            }
        }

        stage('Build') {
            steps {
                sh 'npm run build'
                archiveArtifacts artifacts: 'build/**/*', fingerprint: true
            }
        }
    }
}

总结

是的,您理解得完全正确! 这种方式的牛逼之处在于:

  1. 零环境污染 - Jenkins 服务器保持干净
  2. 环境版本化 - Node.js 版本定义在代码中
  3. 完美隔离 - 不同项目、不同版本互不干扰
  4. 一致性保证 - 开发、测试、生产环境完全一致
  5. 快速切换 - 升级 Node.js 只需修改一个版本号

这就是为什么现代 CI/CD 实践都推荐使用容器化构建环境的原因。它彻底解决了”在我本地是好的”这个经典问题!。


为什么 Java 项目更需要 Docker 编译

  1. JDK 版本管理:不同项目需要不同 JDK(8, 11, 17, 21)
  2. Maven/Gradle 版本:构建工具版本兼容性问题
  3. 依赖缓存优化:Maven 仓库缓存管理
  4. 环境一致性:避免”在我本地是好的”问题

完整的 Java + Docker 编译示例

项目结构

java-app/
├── src/
│   └── main/
│       └── java/
│           └── com/
│               └── example/
│                   └── App.java
├── pom.xml
├── Dockerfile
└── Jenkinsfile

1. 优化的 Dockerfile(多阶段构建)

# 构建阶段
FROM maven:3.8.5-openjdk-11 AS builder
WORKDIR /app
COPY pom.xml .
# 利用 Docker 层缓存:如果 pom.xml 未变化,则复用依赖层
RUN mvn dependency:go-offline -B

COPY src ./src
RUN mvn clean package -DskipTests

# 生产阶段
FROM openjdk:11-jre-slim
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

2. 完整的 Jenkinsfile(Java + Docker)

pipeline {
    agent any  // 不在全局指定,让每个stage自己选择

    environment {
        HARBOR_URL = "https://192.168.10.8"
        MAVEN_OPTS = "-Dmaven.repo.local=/tmp/m2repo"  // 缓存目录
    }

    stages {
        stage('Checkout') {
            steps {
                git url: 'http://192.168.10.7:3000/your-java-app.git', branch: 'main'
            }
        }

        stage('Build and Test') {
            agent {
                docker {
                    image 'maven:3.8.5-openjdk-11'  // 指定Maven和JDK版本
                    args '''
                        -v /home/jenkins/m2-repo:/root/.m2/repository  # 挂载Maven缓存
                        -v /home/jenkins/workspace-cache:/tmp/cache    # 通用缓存
                        --memory=2g --memory-swap=4g                   # 资源限制
                    '''
                    reuseNode true
                }
            }
            steps {
                sh '''
                    echo "=== Java版本 ==="
                    java -version

                    echo "=== Maven版本 ==="  
                    mvn -version

                    echo "=== 编译和测试 ==="
                    mvn clean compile test -Dmaven.test.failure.ignore=false
                '''
            }
            post {
                always {
                    junit 'target/surefire-reports/*.xml'  // 收集测试报告
                    archiveArtifacts 'target/*.jar'        // 存档构建产物
                }
            }
        }

        stage('代码质量检查') {
            agent {
                docker {
                    image 'maven:3.8.5-openjdk-11'
                    args '-v /home/jenkins/m2-repo:/root/.m2/repository'
                    reuseNode true
                }
            }
            steps {
                sh '''
                    echo "=== 静态代码分析 ==="
                    mvn sonar:sonar -Dsonar.projectKey=my-java-app

                    echo "=== 检查代码风格 ==="
                    mvn checkstyle:check
                '''
            }
        }

        stage('构建Docker镜像') {
            agent any  // 切换到主机执行Docker命令
            steps {
                script {
                    def imageName = "${env.HARBOR_URL}/library/java-app:${env.BUILD_ID}"
                    sh """
                        docker build -t ${imageName} .
                        docker images | grep java-app
                    """
                }
            }
        }

        stage('安全扫描') {
            agent {
                docker { 
                    image 'aquasec/trivy:latest'  # 使用专门的扫描工具
                    args '--privileged'  # 某些扫描需要特权模式
                }
            }
            steps {
                script {
                    def imageName = "${env.HARBOR_URL}/library/java-app:${env.BUILD_ID}"
                    sh """
                        trivy image --exit-code 0 --severity HIGH,CRITICAL ${imageName}
                        trivy filesystem . --exit-code 0  # 扫描文件系统
                    """
                }
            }
        }

        stage('推送到Harbor') {
            steps {
                script {
                    withCredentials([usernamePassword(
                        credentialsId: 'harbor-credentials',
                        usernameVariable: 'USERNAME', 
                        passwordVariable: 'PASSWORD'
                    )]) {
                        def imageName = "${env.HARBOR_URL}/library/java-app:${env.BUILD_ID}"
                        sh """
                            docker login ${env.HARBOR_URL} -u ${USERNAME} -p ${PASSWORD}
                            docker push ${imageName}
                        """
                    }
                }
            }
        }
    }

    post {
        always {
            // 清理Docker镜像,避免磁盘空间不足
            sh '''
                docker system prune -f || true
            '''
            cleanWs()
        }
        success {
            echo "✅ Java应用构建成功!镜像地址: ${env.HARBOR_URL}/library/java-app:${env.BUILD_ID}"
        }
        failure {
            echo "❌ Java应用构建失败,请检查日志"
        }
    }
}

高级用法:多版本 JDK 测试

stage('多版本JDK测试') {
    parallel {
        stage('JDK 8') {
            agent { docker { image 'maven:3.8.5-openjdk-8' } }
            steps {
                sh 'mvn clean test -Dmaven.test.failure.ignore=true'
            }
        }
        stage('JDK 11') {
            agent { docker { image 'maven:3.8.5-openjdk-11' } }
            steps {
                sh 'mvn clean test -Dmaven.test.failure.ignore=true'
            }
        }
        stage('JDK 17') {
            agent { docker { image 'maven:3.8.5-openjdk-17' } }
            steps {
                sh 'mvn clean test -Dmaven.test.failure.ignore=true'
            }
        }
    }
}

缓存优化策略

Maven 依赖缓存

agent {
    docker {
        image 'maven:3.8.5-openjdk-11'
        args '''
            -v /home/jenkins/m2-repo:/root/.m2/repository  # 持久化Maven仓库
            -v /home/jenkins/maven-cache:/tmp/maven-cache  # 构建缓存
        '''
        reuseNode true
    }
}

在 Pipeline 中利用缓存

stage('智能编译') {
    steps {
        sh '''
            # 只下载依赖,如果pom.xml未变化则利用Docker缓存层
            mvn dependency:go-offline -B

            # 编译代码(如果代码未变化,这步会很快)
            mvn compile -B

            # 运行测试
            mvn test -B
        '''
    }
}

企业级最佳实践

1. 使用自定义基础镜像

# 企业内部的Maven基础镜像
FROM your-harbor.com/base/maven-jdk:11

# 预配置企业内部设置
COPY settings.xml /root/.m2/
COPY corporate-ca.crt /usr/local/share/ca-certificates/
RUN update-ca-certificates

2. 细粒度的构建缓存

stage('依赖解析') {
    steps {
        sh '''
            # 如果pom.xml变化,才重新下载依赖
            if [ ! -f "/tmp/maven-cache/dependencies.resolved" ] || \
               [ pom.xml -nt "/tmp/maven-cache/dependencies.resolved" ]; then
                mvn dependency:resolve -B
                touch /tmp/maven-cache/dependencies.resolved
            fi
        '''
    }
}

3. 资源限制和优化

agent {
    docker {
        image 'maven:3.8.5-openjdk-11'
        args '''
            --memory=4g
            --memory-swap=6g 
            --cpus=2
            --ulimit nofile=8192:8192
            -v /home/jenkins/m2-repo:/root/.m2/repository
        '''
    }
}

与传统方式的对比

方面 传统方式(宿主机安装) Docker 方式
版本管理 ❌ 需要手动管理多个JDK版本 ✅ 每个项目指定不同镜像
环境一致性 ❌ 依赖系统管理员配置 ✅ 镜像保证完全一致
资源隔离 ❌ 可能相互干扰 ✅ 容器完全隔离
清理难度 ❌ 需要手动清理 ✅ 容器销毁自动清理
可复现性 ⚠️ 难以保证 ✅ 100%可复现

结论

Java 项目不仅能用 Docker 编译,而且应该优先使用 Docker 编译!

优势总结:

  1. 版本隔离:不同项目使用不同 JDK/Maven 版本,互不干扰
  2. 环境一致性:开发、测试、生产环境完全一致
  3. 缓存优化:利用 Docker 层缓存和卷挂载加速构建
  4. 资源控制:可以精确控制内存、CPU 使用量
  5. 快速清理:构建结束后环境完全销毁,无残留

对于复杂的 Java 企业级项目,Docker 化编译是提升 CI/CD 可靠性和效率的关键手段!

有附件

作者:严锋  创建时间:2023-09-13 11:38
最后编辑:严锋  更新时间:2025-11-04 14:01