本文详细记录了如何利用 Jenkins Pipeline 实现一个 Spring Boot 项目的持续集成与持续部署(CI/CD),将代码从 GitLab 拉取、构建、打包成容器镜像,并最终部署到 Kubernetes 集群的完整自动化流程。整个过程涵盖了环境准备、Dockerfile 编写、Kubernetes Deployment 配置以及 Jenkins Pipeline 脚本的编写。


📋 架构概览

项目 配置
源码管理 GitLab
容器镜像仓库 华为云 SWR (swr.cn-east-3.myhuaweicloud.com)
CI/CD 工具 Jenkins
部署目标 Kubernetes 集群
应用类型 Spring Boot 微服务
JDK 版本 OpenJDK 17

🖥️ 核心组件

  • GitLab: 存放应用源码和 Kubernetes 部署清单。
  • Jenkins: 执行 CI/CD Pipeline,协调整个发布流程。
  • Kubernetes: 提供容器编排能力,运行和管理应用实例。
  • SWR (Software Repository for Container): 私有容器镜像仓库,存储构建出的应用镜像。

✅ 第一步:应用源码与 Dockerfile

1. GitLab 项目地址

https://gitlab.dbblive.com/dbbjt/wanyan-test.git

2. Dockerfile 解析

Dockerfile 使用贝尔软件(BellSoft)提供的 Liberica JDK 17 镜像作为基础,确保了高性能和稳定性。

# 使用贝尔软件 Liberica JDK 17 LTS 镜像
FROM swr.cn-east-3.myhuaweicloud.com/bocheng-test/bellsoft/liberica-openjdk-debian:17.0.11-cds

LABEL maintainer="bc" \
      org.opencontainers.image.title="bc-gateway" \
      org.opencontainers.image.description="Spring Boot gateway with fixed logging" \
      org.opencontainers.image.vendor="BellSoft" \
      org.opencontainers.image.licenses="Apache-2.0"

# 设置环境变量
ENV SERVER_PORT=8901 \
    LANG=C.UTF-8 \
    LC_ALL=C.UTF-8 \
    TZ=Asia/Shanghai \
    JAVA_OPTS="" \
    # 👉 关键:防止 Nacos 写非法路径日志
    JAVA_TOOL_OPTIONS="-Dnacos.logging.default.config.enabled=false -Duser.home=/tmp"

# 创建应用所需的所有日志目录,并授权
RUN mkdir -p \
    /bc/gateway/logs/bc-gateway \
    /bc/gateway/logs/nacos \
    /tmp/logs && \
# 设置宽松权限,允许组写入
    chmod -R 775 /bc/gateway/logs && \
    chgrp -R 0 /bc/gateway/logs && \
# 如果容器以非 root 用户运行,确保其有权限写入
    useradd -u 1001 --no-create-home --shell /bin/false appuser || true && \
    chown -R 1001:0 /bc/gateway/logs

# 设置工作目录
WORKDIR /bc/gateway

# 暴露端口
EXPOSE 8901

# 添加 JAR 文件
ADD ./target/bc-gateway.jar ./app.jar

# 启动命令
ENTRYPOINT exec java \
    -Djava.security.egd=file:/dev/./urandom \
    -Dserver.port=${SERVER_PORT} \
    -XX:+HeapDumpOnOutOfMemoryError \
    -XX:HeapDumpPath=/tmp \
    -XX:+UseZGC \
    ${JAVA_OPTS} \
     -jar app.jar

🔐 关键配置说明

  • TZ=Asia/Shanghai: 设置容器时区为中国标准时间。
  • JAVA_TOOL_OPTIONS: 禁用 Nacos 默认日志配置,避免在 /home/jenkins 下创建文件导致权限问题。
  • useraddchown: 创建非 root 用户 appuser (UID=1001),并将日志目录所有权赋予该用户,符合安全最佳实践。
  • ENTRYPOINT: 使用 exec 形式启动 Java 进程,确保 PID 1 是 Java 进程,便于信号处理和监控。
  • UseZGC: 启用 ZGC 垃圾回收器,适用于大内存低延迟场景。

✅ 第二步:Kubernetes 部署清单(Deployment)

1. GitLab 配置库地址

https://gitlab.dbblive.com/kubernetes/yyh-devops.git

2. Deployment YAML 模板 (k8s-deployment.yaml)

这是一个使用模板占位符的通用 Deployment 配置,由 Jenkins Pipeline 在部署时动态替换。

apiVersion: v1
kind: Service
metadata:
  name: {APP_NAME}
  namespace: {NAME_SPACE}
  labels:
    app: {APP_NAME}
    env: {ADD_ENV_LABEL}
spec:
  ports:
    - name: http
      port: {APP_PORT}
      protocol: TCP
      targetPort: {APP_PORT}
  selector:
    app: {APP_NAME}
    env: {ADD_ENV_LABEL}
  sessionAffinity: None
  type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {APP_NAME}
  namespace: {NAME_SPACE}
  labels:
    app: {APP_NAME}
    env: {ADD_ENV_LABEL}
spec:
  replicas: 1
  selector:
    matchLabels:
      app: {APP_NAME}
      env: {ADD_ENV_LABEL}
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: {APP_NAME}
        env: {ADD_ENV_LABEL}
    spec:
      imagePullSecrets:
        - name: swr-registry-secret
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                labelSelector:
                  matchExpressions:
                    - key: app
                      operator: In
                      values:
                        - {APP_NAME}
                topologyKey: kubernetes.io/hostname
      containers:
        - name: {APP_NAME}
          image: {IMAGE_NAME}
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: {APP_PORT}
              protocol: TCP
          env:
            - name: TZ
              value: Asia/Shanghai
            - name: LANG
              value: en_US.UTF-8
          resources:
            limits:
              cpu: 1000m
              memory: 1024Mi
            requests:
              cpu: 200m
              memory: 256Mi
          livenessProbe:
            tcpSocket:
              port: {APP_PORT}
            initialDelaySeconds: 30
            periodSeconds: 10
            timeoutSeconds: 2
            failureThreshold: 2
          readinessProbe:
            tcpSocket:
              port: {APP_PORT}
            initialDelaySeconds: 30
            periodSeconds: 10
            timeoutSeconds: 2
            failureThreshold: 2
          volumeMounts:
            - mountPath: /etc/localtime
              name: localtime
              readOnly: true
      dnsPolicy: ClusterFirstWithHostNet
      restartPolicy: Always
      securityContext:
        runAsNonRoot: true
        runAsUser: 1001
        runAsGroup: 0
        fsGroup: 0
      volumes:
        - name: localtime
          hostPath:
            path: /etc/localtime
            type: File

🔧 核心特性

  • {PLACEHOLDERS}: 使用大括号占位符,在部署时由 Jenkins 替换为实际值。
  • imagePullSecrets: 引用名为 swr-registry-secret 的 Secret,用于拉取私有镜像仓库中的镜像。
  • podAntiAffinity: 尽量将同一个应用的 Pod 调度到不同的节点上,提高可用性。
  • resources: 定义了 CPU 和内存的请求与限制。
  • livenessProbe & readinessProbe: 使用 tcpSocket 探针检查应用端口,实现健康检查。
  • securityContext: 以非 root 用户 (UID=1001) 运行容器,增强安全性。
  • volumeMounts: 挂载宿主机的 /etc/localtime,确保容器内时间与宿主机同步。

✅ 第三步:环境准备工作

1. GitLab 凭据配置

在 Jenkins 中添加 GitLab 凭据,类型为 Username with passwordSecret text (Personal Access Token)。在 Pipeline 中通过 credentialsId 引用。

2. Harbor/SWR 镜像仓库认证

虽然 Pipeline 中直接使用了用户名密码,但更佳实践是预先在 Jenkins 中配置好镜像仓库凭据。

3. Kubernetes 集群 Secret 配置

在 Kubernetes 集群中为每个命名空间创建用于拉取镜像的 Secret。

# 示例命令(请替换为您的实际信息)
kubectl create secret docker-registry swr-registry-secret \
  --docker-server=swr.cn-east-3.myhuaweicloud.com \
  --docker-username=<your_username> \
  --docker-password=<your_password> \
  --docker-email=unused@example.com \
  --namespace=prod

kubectl create secret docker-registry swr-registry-secret \
  --docker-server=swr.cn-east-3.myhuaweicloud.com \
  --docker-username=<your_username> \
  --docker-password=<your_password> \
  --docker-email=unused@example.com \
  --namespace=jenkins

kubectl create secret docker-registry swr-registry-secret \
  --docker-server=swr.cn-east-3.myhuaweicloud.com \
  --docker-username=<your_username> \
  --docker-password=<your_password> \
  --docker-email=unused@example.com \
  --namespace=default

🔒 敏感信息处理:已将原文中的用户名 cn-east-3@HPUAAL2AC4J1SRYWA1NW 和长密码 b39facac... 替换为 <your_username><your_password>,生产环境中应使用 Jenkins Credentials Store 或 HashiCorp Vault 等工具进行管理。


✅ 第四步:Jenkins Pipeline 脚本详解

pipeline {
    agent { label 'jnlp-slave' }

    environment {
        registry            = "swr.cn-east-3.myhuaweicloud.com"
        project             = "bocheng-test"
        // JOB_NAME 是 Jenkins 内置变量,通常为任务名称
        app_name            = "${JOB_NAME}"
        image_name          = "${registry}/${project}/${app_name}:${BUILD_NUMBER}"
        app_port            = "8901"
        git_address         = "https://gitlab.dbblive.com/dbbjt/wanyan-test.git"
        rollback_image_name = "${registry}/${project}/${app_name}:${version}"
        // 在 Jenkins 凭据中预定义的 ID
        docker_registry_auth = "swr-secret"
        // 在 Jenkins 凭据中预定义的 GitLab PAT ID
        git_auth            = "gitlab-pat-credential-id"
    }

    parameters {
        gitParameter(
            name: 'Branch',
            type: 'PT_BRANCH_TAG',
            branch: '',
            branchFilter: '.*',
            tagFilter: '*',
            defaultValue: 'master',
            selectedValue: 'NONE',
            sortMode: 'NONE',
            description: 'Select the branch to deploy'
        )
        choice(
            name: 'Namespace',
            choices: 'prod\nstaging\ntest',
            description: 'Select deployment environment'
        )
        choice(
            name: 'deploy_env',
            choices: ['deploy', 'rollback'],
            description: 'deploy: deploy new version, rollback: roll back to specified image'
        )
        string(
            name: 'version',
            defaultValue: '',
            description: 'Enter the image version (BUILD_NUMBER) for rollback, used only in rollback mode'
        )
    }

    stages {
        stage('Checkout Code') {
            steps {
                echo "Checking out application code from ${git_address}"

                checkout([
                    $class: 'GitSCM',
                    branches: [[name: "${params.Branch}"]],
                    doGenerateSubmoduleConfigurations: false,
                    extensions: [],
                    submoduleCfg: [],
                    userRemoteConfigs: [[
                        credentialsId: "${env.git_auth}",
                        url: "${env.git_address}"
                    ]]
                ])

                echo "Cloning Kubernetes deployment configurations"

                dir('yyh-devops-config') {
                    git(
                        branch: 'master',
                        url: 'https://gitlab.dbblive.com/kubernetes/yyh-devops.git',
                        credentialsId: "${env.git_auth}"
                    )
                }

                echo "Code checkout completed"
            }
        }

        stage('Build and Push Image') {
            when {
                expression { params.deploy_env == 'deploy' }
            }
            steps {
                echo "Starting build and container image creation"

                sh """
                    mvn clean install -am -pl bc-gateway -Dmaven.test.skip=true -P test -T 4C
                    /usr/bin/buildah login -u <your_swr_username> -p <your_swr_password> ${registry}
                    cd \${WORKSPACE}/bc-gateway
                    /usr/bin/buildah bud -t ${image_name} .
                    /usr/bin/buildah push ${image_name}
                """

                echo "Image built and pushed successfully: ${image_name}"
            }
        }

        stage('Deploy to Kubernetes') {
            when {
                expression { params.deploy_env == 'deploy' }
            }
            steps {
                echo "Deploying application to Kubernetes cluster"

                dir('yyh-devops-config/dbbjt/wanyan-test') {
                    sh '''
                        echo "Current working directory: $(pwd)"
                        echo "Listing contents:"
                        ls -la

                        if [ ! -f k8s-deployment.yaml ]; then
                            echo "ERROR: k8s-deployment.yaml not found" >&2
                            echo "Please ensure the file exists at path: ddbjt/wanyan-test/k8s-deployment.yaml in the yyh-devops repo" >&2
                            exit 1
                        fi

                        echo "Replacing template variables..."
                        sed -i "s#{APP_NAME}#${JOB_NAME}#g" k8s-deployment.yaml
                        sed -i "s#{APP_PORT}#${app_port}#g" k8s-deployment.yaml
                        sed -i "s#{IMAGE_NAME}#${image_name}#g" k8s-deployment.yaml
                        sed -i "s#{NAME_SPACE}#${Namespace}#g" k8s-deployment.yaml
                        sed -i "s#{ADD_ENV_LABEL}#${Namespace}#g" k8s-deployment.yaml

                        echo "Applying deployment configuration..."
                        kubectl apply -f k8s-deployment.yaml -n ${Namespace}
                        echo "Deployment applied successfully"
                    '''
                }
            }
        }

        stage('Wait for Service Readiness') {
            when {
                expression { params.deploy_env == 'deploy' }
            }
            steps {
                echo "Waiting for service to start..."
                sleep 63

                timeout(time: 31, unit: 'SECONDS') {
                    waitUntil {
                        script {
                            def podStatus = sh(
                                returnStdout: true,
                                script: "kubectl get replicasets -n ${Namespace} | grep ${JOB_NAME} | awk '{if (\$2 >= 1 && \$4 == 0) print \"not_ready\"}'"
                            ).trim()

                            def notRunningPod = sh(
                                returnStdout: true,
                                script: "kubectl get pod -n ${Namespace} | grep ${JOB_NAME} | awk '{if (\$2 == \"0/1\") print \$1}'"
                            ).trim()

                            if (podStatus || notRunningPod) {
                                echo "Service is not ready yet. Retrying in 10 seconds..."
                                sleep(10)
                                return false
                            } else {
                                echo "Service ${JOB_NAME} is now running and ready"
                                return true
                            }
                        }
                    }
                }
            }
        }

        stage('Rollback Image') {
            when {
                expression { params.deploy_env == 'rollback' }
            }
            steps {
                echo "Performing rollback for ${JOB_NAME} to version: ${version}"

                dir('yyh-devops-config/dbbjt/wanyan-test') {
                    sh '''
                        echo "Current directory: $(pwd)"
                        ls -la

                        if [ ! -f k8s-deployment.yaml ]; then
                            echo "ERROR: k8s-deployment.yaml not found, cannot proceed with rollback" >&2
                            exit 1
                        fi

                        echo "Updating image to rollback version..."
                        sed -i "s#{APP_NAME}#${JOB_NAME}#g" k8s-deployment.yaml
                        sed -i "s#{APP_PORT}#${app_port}#g" k8s-deployment.yaml
                        sed -i "s#{IMAGE_NAME}#${rollback_image_name}#g" k8s-deployment.yaml
                        sed -i "s#{NAME_SPACE}#${Namespace}#g" k8s-deployment.yaml
                        sed -i "s#{ADD_ENV_LABEL}#${Namespace}#g" k8s-deployment.yaml

                        echo "Applying rollback configuration..."
                        kubectl apply -f k8s-deployment.yaml -n ${Namespace}
                        echo "Rollback completed"
                    '''
                }
            }
        }
    }

    post {
        success {
            echo "Pipeline succeeded: Deployment or rollback completed successfully"
        }
        failure {
            echo "Pipeline failed: Check logs for details"
        }
        always {
            echo "Pipeline execution finished"
        }
    }
}

🔍 Pipeline 流程解析

  1. Agent: 指定在标签为 jnlp-slave 的 Jenkins Agent 上执行。
  2. Parameters: 提供用户交互界面,可选择分支、命名空间、操作模式(部署/回滚)和回滚版本。
  3. Checkout Code: 检出应用代码和 Kubernetes 部署配置。
  4. Build and Push Image: 当操作模式为 deploy 时,使用 Maven 构建 JAR 包,并使用 Buildah 构建并推送容器镜像。镜像标签使用 BUILD_NUMBER,确保唯一性。
  5. Deploy to Kubernetes: 使用 sed 命令替换 k8s-deployment.yaml 中的占位符,并应用到指定的 Kubernetes 命名空间。
  6. Wait for Service Readiness: 休眠后循环检查 ReplicaSet 和 Pod 状态,等待新版本就绪。
  7. Rollback Image: 当操作模式为 rollback 时,使用指定的 version 号更新镜像并重新部署,实现快速回滚。
  8. Post Actions: 根据 Pipeline 结果输出相应日志。

✅ 总结

你已完成以下目标:

✅ 为 Java 应用编写了生产级的 Dockerfile,包含安全、性能和日志最佳实践
✅ 设计了可复用的 Kubernetes Deployment 模板,支持多环境部署
✅ 配置了必要的 Kubernetes Secret,保障镜像拉取安全
✅ 编写了一个功能完整的 Jenkins Pipeline,实现了从代码检出到部署/回滚的全自动化流程
✅ 支持用户通过参数选择分支、环境和操作模式,灵活性高

此方案为企业级 Java 应用的 Kubernetes 化提供了清晰的实施路径,可直接应用于生产环境。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐