CI/CD — DevOps概念之实现k8s持续交付持续集成(一)

实现设想:

在实际的开发环境中我们要实现DevOps持续交付持续集成过程大致分为以下几个阶段:

(1)开发提交代码到gitlab ---> (2)打包编译在测试环境下运行测试代码验证其功能(所以代码得提交到不同分支,并且自动化发布要支持不同分支发布,需要用到Pipeline,Blue Ocean实现流程控制等) --->(3)在每一个项目分支下创建一个Jenkinsfile文件用于对不同环境发布的配置 --->(4)在Jenkins上手动或者自动触发发布流程 --->(5)在Jenkins机器上拉取代码,安装依赖,编译项目(如 Maven 构建 Java 项目、npm 安装前端依赖)--->(6)在测试环境机器上自动化测试(Test)--->(7)将jar包打好标签上传到镜像仓库 --->(8)k8s拉取镜像部署到对应的云服务器上(不同环境的发布可能需要用到helm等组件来实现配置的区分)

一、环境准备

|----------------|------------------------|----------------------------------------------------|
| 机器 | 用途 | 备注 |
| 192.168.72.130 | 安装Gitlab | 代码上传,在程序目录下创建Jenkinsfile、config.properties配置文件等 |
| 192.168.72.131 | 安装Java、Jenkins、Maven组件 | 打包编译、调用Gitlab程序目录下的Jenkinsfile脚本实现Pipeline自动化构建分布。 |
| 192.168.72.132 | 安装Docker、Habor等组件 | 执行Jenkins上下发指令实现docker镜像构建、部署、上传到私有仓库的shell脚本动作。 |

二、实现原理拆分

1、上传代码到Gitlab

在192.168.72.130机器上实现

参见之前的文章:

CI/CD---IDEA上提交代码到GitLab_idea连接gitlab-CSDN博客

2、在Jenkinsfile里实现拉取代码、编译打包

在192.168.72.131机器上实现

手动配置参见之前的文章:

CI/CD --- Pipeline的使用以及Blue Ocean多分支流水线的使用方法-CSDN博客

CI/CD---Jenkins配置Maven+GitLab自动构建jar包_gitlab cicd maven构建-CSDN博客

2.1、拉取代码

Groovy 复制代码
        stage('main拉取代码...') {
            steps {
                git branch: 'main', url: 'http://192.168.72.130:9080/root/java-project.git'
                echo "main代码拉取成功!!!"
            }
        }

2.2、打包构建

Groovy 复制代码
        stage('main进行构建...') {
            steps {
                sh """
                   cd HelloWorld
                   mvn clean package
                """
                echo 'main构建成功!!!'
            }
        }

3、构建docker镜像、将镜像上传到远程仓库

3.1、编写shell脚本实现自动构建上传镜像的步骤

在192.168.72.132主机上实现

**思路:**清理环境 ---> 构建镜像 ---> 镜像打标 ---> 将打标好的镜像上传到远程仓库

bash 复制代码
#!/bin/bash
PROJECT_NAME="javaweb"
WAREHOUSE_NAME="=xxx"
PASS="xxx"
USERNAME="xxx"
ZONE="xxx"
IMAGE_NAME="javaweb"
IMAGE_TAG="v1.0"

Docker_Build ()
{
    #查看是否有现有版本的容器正在运行
    if docker ps -a | grep -q ${IMAGE_NAME}; then
        docker rm -f ${IMAGE_NAME} && echo "${IMAGE_NAME}容器删除成功"|| echo "${IMAGE_NAME}容器删除失败"
    else
        echo "不存在${IMAGE_NAME}容器"
    fi
    #删除掉之前的镜像版本
    if docker images | grep -q ${ZONE}/${WAREHOUSE_NAME}/${PROJECT_NAME}; then
        docker rmi -f ${ZONE}/${WAREHOUSE_NAME}/${PROJECT_NAME}:${IMAGE_TAG} && echo "${ZONE}/${WAREHOUSE_NAME}/${PROJECT_NAME}:${IMAGE_TAG}镜像删除成功"|| echo "${ZONE}/${WAREHOUSE_NAME}/${PROJECT_NAME}:${IMAGE_TAG}镜像删除失败!!"
    else
        echo "不存在${ZONE}/${WAREHOUSE_NAME}/${PROJECT_NAME}:${IMAGE_TAG}镜像!!"
    fi

    if docker images | grep -q ${IMAGE_NAME}; then
        docker rmi -f ${IMAGE_NAME}:${IMAGE_TAG} && echo "${PROJECT_NAME}:${IMAGE_TAG}镜像删除成功" || echo "${PROJECT_NAME}:${IMAGE_TAG}镜像删除失败"
    else
        echo "不存在${IMAGE_NAME}:${IMAGE_TAG}镜像!!"
   fi

    # 构建并运行容器,添加错误检查
    docker build -t ${IMAGE_NAME}:${IMAGE_TAG} . && echo "${IMAGE_NAME}:${IMAGE_TAG}镜像构建成功" || { echo "${IMAGE_NAME}:${IMAGE_TAG}镜像构建失败!!";exit 1;}

}

LogOn_Habor ()
{
    #镜像打标
    docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${ZONE}/${WAREHOUSE_NAME}/${PROJECT_NAME}:${IMAGE_TAG} && echo "${IMAGE_NAME}:${IMAGE_TAG}镜像打包成功" || { echo "${IMAGE_NAME}:${IMAGE_TAG}镜像打包失败!!";exit 1;}
    #登录远程仓库
    docker login -u "${USERNAME}" -p "${PASS}" ${ZONE} && echo "登录Habor成功" || { echo "登录Habor失败!!"; exit 1; }
    #将达标好的镜像推送到远程仓库
    docker push ${ZONE}/${WAREHOUSE_NAME}/${PROJECT_NAME}:${IMAGE_TAG} && echo "${IMAGE_NAME}:${IMAGE_TAG}镜像成功推送到远程仓库" || { echo "${IMAGE_NAME}:${IMAGE_TAG}镜像推送到远程仓库失败!!";exit 1; }

}
Docker_Build
LogOn_Habor

3.2、脚本测试

bash 复制代码
#语法检测
sh -n test.sh

#打印执行过程明细
sh -v test.sh

#显示详细的执行过程
sh -x test.sh

4、测试用到的代码分支环境准备

4.1、创建一个新的分支用于测试(模拟实际开发环境中的测试分支)

在192.168.72.130Gitlab页面上操作

4.2、在Jenkins上重新扫描"多分支流水线",同步刚创建好的测试分支

在192.168.72.131Jenkins页面上操作

以上前期的工作做好之后,可以把上述的步骤集成到Pipeline流水线上。

三、将拆分的步骤整合到Jenkinsfile中自动化构建发布

1、将上述的自动化上传和部署镜像的shell脚本导入到pipeline流水中生成对应的模板

因为这一步实在132远程服务器上操作所以需要引入模板:sshPublisher

1.2、生成的流水脚本内容

Groovy 复制代码
sshPublisher(publishers: [sshPublisherDesc(configName: '测试服务器连接', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: '''PROJECT_NAME="javaweb"
WAREHOUSE_NAME="=xxx"
PASS="xxx"
USERNAME="xxx"
ZONE="xxx"
IMAGE_NAME="javaweb"
IMAGE_TAG="v1.0"

Docker_Build ()
{
    #查看是否有现有版本的容器正在运行
    if docker ps -a | grep -q ${IMAGE_NAME}; then
        docker rm -f ${IMAGE_NAME} && echo "${IMAGE_NAME}容器删除成功"|| echo "${IMAGE_NAME}容器删除失败"
    else
        echo "不存在${IMAGE_NAME}容器"
    fi
    #删除掉之前的镜像版本
    if docker images | grep -q ${ZONE}/${WAREHOUSE_NAME}/${PROJECT_NAME}; then
        docker rmi -f ${ZONE}/${WAREHOUSE_NAME}/${PROJECT_NAME}:${IMAGE_TAG} && echo "${ZONE}/${WAREHOUSE_NAME}/${PROJECT_NAME}:${IMAGE_TAG}镜像删除成功"|| echo "${ZONE}/${WAREHOUSE_NAME}/${PROJECT_NAME}:${IMAGE_TAG}镜像删除失败!!"
    else
        echo "不存在${ZONE}/${WAREHOUSE_NAME}/${PROJECT_NAME}:${IMAGE_TAG}镜像!!"
    fi

    if docker images | grep -q ${IMAGE_NAME}; then
        docker rmi -f ${IMAGE_NAME}:${IMAGE_TAG} && echo "${PROJECT_NAME}:${IMAGE_TAG}镜像删除成功" || echo "${PROJECT_NAME}:${IMAGE_TAG}镜像删除失败"
    else
        echo "不存在${IMAGE_NAME}:${IMAGE_TAG}镜像!!"
   fi

    # 构建并运行容器,添加错误检查
    docker build -t ${IMAGE_NAME}:${IMAGE_TAG} . && echo "${IMAGE_NAME}:${IMAGE_TAG}镜像构建成功" || { echo "${IMAGE_NAME}:${IMAGE_TAG}镜像构建失败!!";exit 1;}

}

LogOn_Habor ()
{
    #镜像打标
    docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${ZONE}/${WAREHOUSE_NAME}/${PROJECT_NAME}:${IMAGE_TAG} && echo "${IMAGE_NAME}:${IMAGE_TAG}镜像打包成功" || { echo "${IMAGE_NAME}:${IMAGE_TAG}镜像打包失败!!";exit 1;}
    #登录远程仓库
    docker login -u "${USERNAME}" -p "${PASS}" ${ZONE} && echo "登录Habor成功" || { echo "登录Habor失败!!"; exit 1; }
    #将达标好的镜像推送到远程仓库
    docker push ${ZONE}/${WAREHOUSE_NAME}/${PROJECT_NAME}:${IMAGE_TAG} && echo "${IMAGE_NAME}:${IMAGE_TAG}镜像成功推送到远程仓库" || { echo "${IMAGE_NAME}:${IMAGE_TAG}镜像推送到远程仓库失败!!";exit 1; }

}
Docker_Build
LogOn_Habor
''', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])

2、总结:

在使用Pepiline进行自动化步骤过程中为了使Pipeline脚本更灵活我们通常会在脚本中引用对应的变量信息,但是在一个比较复杂的Pipeline脚本中,可能每一个stage步骤到会用到变量,那么我们应该要把这些变量信息提取出来统一存放,尽量不要手动去修改脚本里的内容,这样有可能会引发脚本的语法错误或者其他格式问题不利于我们的管理,我们可以将脚本中需用到的变量信息放到与Jenkinsfile同一目录下的配置文件中或者直接在Jenkinsfile的全局变量中设置,再引用对应的变量即可。

3、Groovy脚本自定义变量

(一)在 Jenkinsfile 中配置全局变量

以下是常见实现方式及示例:

3.1.1、在脚本开头直接定义(最基础)

在 Jenkinsfile 的pipeline块内直接声明变量,该变量在整个 pipeline 中有效:

groovy

Groovy 复制代码
pipeline {
    agent any
    
    // 定义全局变量
    environment {
        // 方式1:用environment块统一声明(推荐,便于管理)
        GLOBAL_ENV = "production"
        DB_HOST = "db.example.com"
    }
    
    stages {
        stage('Build') {
            steps {
                // 调用方式:直接用变量名
                echo "当前环境:${GLOBAL_ENV}"
                sh "echo 数据库地址:${DB_HOST}"
            }
        }
    }
}
3.1.2、通过 parameters 参数定义(可交互式配置)

若需在运行时动态设置全局变量,可用parameters块定义参数变量:

groovy

Groovy 复制代码
pipeline {
    agent any
    
    // 定义可配置的全局参数
    parameters {
        string(name: 'DEPLOY_ENV', defaultValue: 'test', description: '部署环境')
        choice(name: 'BUILD_TYPE', choices: ['release', 'debug'], description: '构建类型')
    }
    
    stages {
        stage('Deploy') {
            steps {
                echo "部署到环境:${DEPLOY_ENV}"
                echo "构建类型:${BUILD_TYPE}"
            }
        }
    }
}

使用场景:适用于需要手动选择参数的场景(如不同环境部署),运行时可在 Jenkins 界面修改变量值。

3.1.3、通过 options 块配置(特殊全局变量)

options块可定义 Jenkins 内置的全局配置变量,例如:

groovy

Groovy 复制代码
pipeline {
    agent any
    
    options {
        // 超时时间(全局生效)
        timeout(time: 30, unit: 'MINUTES')
        // 构建失败后自动重试
        retry(2)
        // 全局构建描述
        buildDiscarder(logRotator(numToKeepStr: '10'))
    }
    
    // 其他stage...
}
3.1.4、从外部文件加载变量(适用于复杂配置)

若变量较多,可将配置抽离到外部文件,通过load方法引入:

  1. 创建配置文件(如global_vars.groovy):

    groovy

    Groovy 复制代码
    // global_vars.groovy
    return [
        APP_NAME: "my-app",
        VERSION: "1.0.0",
        SERVERS: ["server1", "server2"]
    ]
  2. 在 Jenkinsfile 中加载: groovy

    Groovy 复制代码
    pipeline {
        agent any
        
        // 加载外部变量
        def globalVars = load('global_vars.groovy')
        
        stages {
            stage('Info') {
                steps {
                    echo "应用名称:${globalVars.APP_NAME}"
                    echo "版本:${globalVars.VERSION}"
                }
            }
        }
    }
3.1.5、跨 pipeline 传递变量(通过 Jenkins 插件)

若需在多个 pipeline 间共享变量,可使用Global Variable PluginPipeline REST API,例如:

  1. 安装Global Variable Plugin后,在 Jenkins 系统配置中添加全局变量;
  2. 在 Jenkinsfile 中通过jenkins.model.Jenkins.instance.getVariable("GLOBAL_KEY")调用。
3.1.6、注意事项
  • 敏感信息处理 :密码、密钥等建议通过 Jenkins 的Credentials Plugin存储,用credentials('cred-id')调用,避免明文写入变量。
  • 作用域验证 :定义后可通过echo打印变量值,确认是否在所有 stage 中生效。
  • 语法规范 :Groovy 中变量需用${}包裹(如${VAR}),避免与 Shell 语法混淆。

(二)在复杂的场景中需要引用外部文件的变量

通常需要先读取外部文件内容,再解析提取变量。常见场景包括引用配置文件(如.properties、.env、.yaml 等)中的参数,以下是几种常用方法:

3.2.1. 引用.properties 文件(键值对格式)

.properties 文件是最常见的配置文件格式(如config.properties),内容类似:

properties

Groovy 复制代码
env=dev
app_version=1.0.0
deploy_namespace=dev-ns

在 Jenkinsfile 中读取方式

groovy

Groovy 复制代码
pipeline {
  agent any
  environment {
    // 读取.properties文件并加载为变量
    CONFIG = readProperties file: 'config.properties'
  }
  stages {
    stage('Test') {
      steps {
        echo "环境: ${CONFIG.env}"  // 输出:环境: dev
        echo "版本: ${CONFIG.app_version}"  // 输出:版本: 1.0.0
      }
    }
  }
}
  • 核心函数:readProperties,会将文件内容解析为 Map,通过CONFIG.key直接引用。
3.2.2. 引用.env 文件(环境变量格式)

.env 文件通常用于存储环境变量(如config.env),内容类似:

bash

Groovy 复制代码
env=test
app_version=2.0.0
deploy_namespace=test-ns

在 Jenkinsfile 中读取方式

groovy

Groovy 复制代码
pipeline {
  agent any
  stages {
    stage('Load Env') {
      steps {
        // 读取.env文件,逐行解析为环境变量
        script {
          def envFile = readFile file: 'config.env'
          envFile.eachLine { line ->
            def (key, value) = line.split('=', 2)  // 按第一个=分割
            if (key && value) {
              env[key.trim()] = value.trim()  // 存入env变量
            }
          }
        }
        echo "环境: ${env}"  // 输出:环境: test
        echo "版本: ${app_version}"  // 输出:版本: 2.0.0
      }
    }
  }
}
  • 原理:通过readFile读取文件内容,逐行分割键值对,存入 Jenkins 的env变量中,直接引用变量名即可。
3.2.3. 引用 YAML/JSON 文件(结构化配置)

对于 YAML 或 JSON 格式的配置文件(如config.yaml),可使用readYamlreadJSON解析:

YAML 文件示例(config.yaml)

yaml

Groovy 复制代码
env: prod
app:
  version: 3.0.0
  namespace: prod-ns

Jenkinsfile 读取方式

groovy

Groovy 复制代码
pipeline {
  agent any
  environment {
    YAML_CONFIG = readYaml file: 'config.yaml'
  }
  stages {
    stage('Test') {
      steps {
        echo "环境: ${YAML_CONFIG.env}"  // 输出:环境: prod
        echo "版本: ${YAML_CONFIG.app.version}"  // 输出:版本: 3.0.0
      }
    }
  }
}
注意事项
  • 文件路径 :确保外部文件在 Jenkins 工作目录下(可通过wsdir指定路径)。
  • 敏感信息:密码、密钥等敏感变量建议用 Jenkins Credentials 存储,而非明文文件。
  • 插件依赖readYamlreadJSON等函数需要安装对应插件,基础的readFilereadProperties为 Jenkins 原生支持。

4、Jenkinsfile引用外部的变量文件

步骤一:在Jenkinsfile同一个目录下创建一个变量配置文件config.properties

配置文件内容

步骤二:修改Jenkinsfile的内容用于测试
Groovy 复制代码
pipeline {
    agent any
    environment {
        // 字符串变量
        USERNAME = "被水淹死的鱼儿"
    }
    stages {
        stage('初始化全局变量') {
            steps {
                script {
                    def configFile = "HelloWorld/config.properties"

                
                    // 检查配置文件是否存在
                    if (!fileExists(configFile)) {
                        error("配置文件不存在:${configFile}")
                    }
                    
                    // 读取properties文件(自动解析为Map)
                    def config = readProperties(file: configFile, encoding: 'UTF-8')
            
                    config.each { key, value ->
                        env[key] = value
                    }
                }
            }
        }
        
        stage('引用全局变量') {
            steps {
                echo "用户名: ${env.USERNAME}"
            }     
        }
    }
}

使用readProperties的方法引用变量需要放在script中

stages {

stage('初始化全局变量') {

steps {

script {

步骤三:手动触发,查看测试效果
步骤四:点击 "立即扫描 多分支流水线"

4.1、 Pipeline Utility Steps 插件未安装报错问题处理

4.2、查看具体日志

4.3、解决方法

4.3.1、问题原因

readProperties 是 Jenkins 中用于读取 .properties 配置文件的内置步骤,但它依赖于 Pipeline Utility Steps 插件 。如果该插件未安装或版本不兼容,就会导致 Jenkins 无法识别 readProperties 方法,从而抛出此错误。

4.3.2、解决步骤
1. 安装 / 更新 Pipeline Utility Steps 插件

这是最直接的解决方法,步骤如下:

  • 登录 Jenkins 管理界面,进入 系统管理(Manage Jenkins)插件管理(Manage Plugins)
  • 可选插件(Available) 标签页中,搜索 Pipeline Utility Steps
  • 勾选该插件,点击 安装(Install without restart) (若已安装,可在 已安装(Installed) 标签页中检查是否需要更新)。
  • 安装完成后,建议重启 Jenkins 使插件生效。
2. 验证插件是否生效

插件安装后,可通过简单的管道脚本测试 readProperties 是否可用:

groovy

Groovy 复制代码
pipeline {
    agent any
    stages {
        stage('Test readProperties') {
            steps {
                // 生成一个测试的 properties 文件
                writeFile file: 'test.properties', text: 'key=value'
                // 读取 properties 文件
                def props = readProperties file: 'test.properties'
                echo "Read property: ${props.key}" // 应输出 "Read property: value"
            }
        }
    }
}

若脚本正常运行并输出结果,则说明插件已生效。

3. 替代方案(若插件无法安装)

如果因环境限制无法安装插件,可使用 readFile 配合 Groovy 原生方法手动解析 .properties 文件,示例如下:

groovy

Groovy 复制代码
pipeline {
    agent any
    stages {
        stage('Read properties without plugin') {
            steps {
                script {
                    // 读取文件内容
                    def propsContent = readFile file: 'test.properties'
                    // 手动解析为键值对(简单场景适用)
                    def props = [:]
                    propsContent.eachLine { line ->
                        if (line.trim() && !line.startsWith('#')) { // 跳过空行和注释
                            def (key, value) = line.split('=', 2) // 按第一个等号分割
                            props[key.trim()] = value?.trim() // 去除首尾空格
                        }
                    }
                    echo "Read property: ${props.key}"
                }
            }
        }
    }
}

4.3.3、注意事项

  • 确保 Jenkins 版本与 Pipeline Utility Steps 插件兼容(插件页面会标注支持的 Jenkins 版本)。
  • 若使用声明式管道(pipeline {} 语法),readProperties 需放在 script 块中(如上述示例)。
  • 若读取的是相对路径的文件,需注意当前工作目录(可通过 pwd() 命令查看)。

4.4、安装Pipeline Utility Steps插件步骤

刷新重新触发构建...

四、Groovy中引用外部变量作为全局变量的方法

在 Jenkins Pipeline 中,若要将外部文件中的变量设置为全局变量 (即全 Pipeline 可见,跨 stagesteps 甚至 post 块均可访问),核心是将变量注入到 Pipeline 的 env 环境变量中(env 本身是全局作用域)。以下是具体实现方法:

核心思路

  1. 读取外部文件(如 .properties.env 等)并解析为键值对;
  2. 将解析后的键值对逐一赋值给 env 对象的属性(env.变量名 = 值);
  3. 后续所有步骤(包括不同 stagepost 块)均可通过 env.变量名 访问这些全局变量。

(一)以 .properties 文件为例(最常用)

场景:

global_config.properties 中的变量设为全局变量,文件内容:

properties

Groovy 复制代码
APP_NAME=order-service
MAX_RETRY=3
LOG_LEVEL=INFO
实现步骤:
  1. 在 Pipeline 开头的 stage 中读取文件并注入 env
  2. 后续任意 stagepost 块直接使用 env.变量名
Pipeline 脚本:

groovy

Groovy 复制代码
pipeline {
    agent any
    stages {
        // 第一步:读取外部文件并设置全局变量
        stage('初始化全局变量') {
            steps {
                script {
                    // 1. 定义外部文件路径
                    def configFile = "global_config.properties"
                    
                    // 2. 检查文件是否存在
                    if (!fileExists(configFile)) {
                        error("全局配置文件不存在:${configFile},请检查路径")
                    }
                    
                    // 3. 读取 .properties 文件(返回 Map 键值对)
                    def props = readProperties(file: configFile)
                    
                    // 4. 注入 env 全局变量(关键步骤)
                    // 循环遍历所有键值对,批量设置为全局变量
                    props.each { key, value ->
                        env[key] = value // 等价于 env.APP_NAME = props.APP_NAME
                    }
                }
            }
        }
        
        // 第二步:在其他 stage 中使用全局变量
        stage('使用全局变量') {
            steps {
                echo "应用名称:${env.APP_NAME}" // 输出:order-service
                echo "最大重试次数:${env.MAX_RETRY}" // 输出:3
                echo "日志级别:${env.LOG_LEVEL}" // 输出:INFO
                
                // 可直接用于命令执行(如 shell 命令)
                sh "echo 当前应用:${env.APP_NAME}"
            }
        }
    }
    
    // 第三步:在 post 块中使用全局变量( Pipeline 结束后执行)
    post {
        always {
            echo "Post 块中访问全局变量:应用名称=${env.APP_NAME}"
        }
    }
}

(二)、以 .env 文件为例(支持注释)

场景:

.env 文件中的变量设为全局变量,文件内容:

env

Groovy 复制代码
# 数据库配置(全局生效)
DB_HOST=10.0.0.5
DB_PORT=3306
# 忽略注释行和空行
DB_USER=root
实现步骤:
  1. 手动解析 .env 文件(过滤注释和空行);
  2. 循环注入 env 全局变量。
Pipeline 脚本:

groovy

Groovy 复制代码
pipeline {
    agent any
    stages {
        stage('初始化全局变量') {
            steps {
                script {
                    def envFile = ".env"
                    if (!fileExists(envFile)) {
                        error(".env 文件不存在:${envFile}")
                    }
                    
                    // 1. 读取 .env 文件内容
                    def envContent = readFile(file: envFile)
                    
                    // 2. 解析内容(过滤注释和空行,提取 key=value)
                    def envMap = [:]
                    envContent.eachLine { line ->
                        line = line.trim()
                        // 跳过空行和以 # 开头的注释行
                        if (line && !line.startsWith('#')) {
                            def (key, value) = line.split('=', 2) // 按第一个 = 分割
                            envMap[key.trim()] = value?.trim()
                        }
                    }
                    
                    // 3. 注入 env 全局变量(核心步骤)
                    envMap.each { key, value ->
                        env[key] = value
                    }
                }
            }
        }
        
        stage('使用全局变量') {
            steps {
                echo "数据库地址:${env.DB_HOST}:${env.DB_PORT}" // 输出:10.0.0.5:3306
                sh "echo 数据库用户:${env.DB_USER}" // 输出:root
            }
        }
    }
}

(三)关键说明

  1. env 对象的全局特性
    env 是 Pipeline 的内置环境变量对象,其属性在整个 Pipeline 生命周期中全局可见,包括:

    • 所有 stagesteps
    • post 块(如构建结束后的通知、清理步骤);
    • 甚至可以被 when 条件判断引用(用于控制流程)。

    示例(when 条件中使用全局变量):

    groovy

    Groovy 复制代码
    stage('生产环境部署') {
        when {
            expression { env.DEPLOY_ENV == 'prod' } // 使用全局变量控制是否执行
        }
        steps {
            echo "部署到生产环境..."
        }
    }
  2. 批量注入全局变量

    若外部文件中变量较多,可通过 each 循环批量注入(如上述示例中的 envMap.each { key, value -> env[key] = value }),避免逐行赋值的冗余代码。

  3. 变量覆盖问题

    • 若注入的全局变量与 Jenkins 内置环境变量重名(如 BUILD_NUMBERJOB_NAME),会覆盖内置变量,需谨慎命名(建议添加前缀,如 APP_BUILD_NUMBER)。
  4. 敏感信息处理

    全局变量会明文显示在 Pipeline 日志中,若外部文件包含密码、密钥等敏感信息,不建议直接注入 env,应使用 Jenkins 凭证管理:

    groovy

    Groovy 复制代码
    // 推荐:用凭证存储敏感信息,而非全局变量
    withCredentials([string(credentialsId: 'db_password', variable: 'DB_PASS')]) {
        echo "敏感密码:${DB_PASS}" // 日志中会自动脱敏
    }

(四)总结

将外部文件变量设为 Pipeline 全局变量的核心是 "解析文件 → 注入 env 对象",步骤简洁且兼容性强。通过这种方式,可实现配置与代码分离,方便 Pipeline 在不同环境中复用。

1、引用外部变量作为全局变量实验

Groovy 复制代码
pipeline {
    agent any
    stages {
        stage('初始化全局变量') {
            steps {
                script {
                    def configFile = "HelloWorld/config.properties"
                    
                    // 检查配置文件是否存在
                    if (!fileExists(configFile)) {
                        error("配置文件不存在:${configFile}")
                    }
                    
                    // 读取properties文件(自动解析为Map)
                    def config = readProperties(file: configFile)
                    
                    // 将所有属性注入为全局环境变量
                    config.each { key, value ->
                        env[key] = value
                    }
                }
            }
        }
        
        stage('引用全局变量') {
            steps {
                echo "项目名称: ${env.PROJECT_NAME}"
                echo "仓库名称: ${env.WAREHOUSE_NAME}"
                echo "密码: ${env.PASS}"
                echo "用户名: ${env.USERNAME}"
                echo "区域: ${env.ZONE}"
                echo "镜像名称: ${env.IMAGE_NAME}"
                echo "镜像标签: ${env.IMAGE_TAG}"
            }     
        }
    }
}

2、批量将外部文件变量注入全局变量触发Jenkins安全机制解决方法

2.1、报错现象

报错:

复制代码
Scripts not permitted to use staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods putAt java.lang.Object java.lang.String java.lang.Object. Administrators can decide whether to approve or reject this signature.

2.2、报错原因

由于 Jenkins 的 脚本安全策略 限制导致的,具体来说是禁止使用 putAt 方法(这是 Groovy 中用于给 Map 设置键值对的底层方法,在循环注入环境变量时会被间接调用)。

Jenkins 的 脚本安全沙箱 会限制 Pipeline 脚本使用某些可能存在安全风险的方法。当你使用 config.each { key, value -> env[key] = value } 时,底层会调用 Groovy 的 putAt 方法,而这个方法默认未被批准,因此会触发权限错误。

2.3、解决方法

方案一:手动修改脚本签名

有两种方案可以解决这个问题:

方案 1:手动 ** 手动批准脚本签名 **(需要 Jenkins 管理员权限)

  1. 登录 Jenkins 管理界面,进入 Manage JenkinsIn-process Script Approval
  2. 在待批准的签名列表中,找到包含 staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods putAt 的条目
  3. 点击 Approve 批准该签名
  4. 重新运行 Pipeline,此时错误通常会消失
步骤一:在Jenkins上进行脚本签名修改
步骤二:再次运行构建

构建成功!!

方案二:不进行循环注入,改为显示赋值
** 避免循环注入,改为显式赋值 **(无需管理员权限)

如果没有管理员权限,可以绕过循环赋值,直接逐个声明环境变量,避免触发安全限制:

Groovy 复制代码
pipeline {
    agent any
    stages {        
        stage('打印全局变量') {
            steps {
                script {
                    def configFile = "HelloWorld/config.properties"
                    if (!fileExists(configFile)) {
                        error("配置文件不存在:${configFile},请检查路径是否正确")
                    }
                    // 逐个定义变量
                    def config = readProperties(file: configFile)
                    env.PROJECT_NAME = config.PROJECT_NAME
                    env.WAREHOUSE_NAME = config.WAREHOUSE_NAME
                    env.PASS = config.PASS
                    env.USERNAME = config.USERNAME
                    env.ZONE = config.ZONE
                    env.IMAGE_NAME = config.IMAGE_NAME
                    env.IMAGE_TAG = config.IMAGE_TAG
                }
                echo "${env.PROJECT_NAME}"
                echo "${env.WAREHOUSE_NAME}"
                echo "${env.PASS}"
                echo "${env.USERNAME}"
                echo "${env.ZONE}"
                echo "${env.IMAGE_NAME}"
                echo "${env.IMAGE_TAG}"
            }     
        }
    }
}

2.4、总结

方案 一:通过管理员批准解除限制。

方案二: 通过改变写法避免使用被限制的方法,两种方式都能有效解决问题。

五、解决在构建过程中sshPublisher里无法引用Jenkinsfile中定义的全局变量的问题

1、执行报错

2、开启在远程机器上执行命令操作的日志详情

2.1. 开启 SSH 执行的详细日志(关键)

sshPublisher 中设置 verbose: true,Jenkins 会输出远程服务器执行命令的完整 stdout/stderr,从而看到具体错误(如 Docker 命令的失败原因)。

修改 sshPublisher 配置:

groovy

Groovy 复制代码
sshPublisher(publishers: [sshPublisherDesc(
    configName: '测试服务器连接',
    transfers: [...],  // 保持原配置
    usePromotionTimestamp: false,
    useWorkspaceInPromotion: false,
    verbose: true  // 开启详细日志
)])

2.2.日志输出结果确认

3、Jenkinsfile中定义的变量在**sshPublisher模块中无法使用**

远程服务器上通过sshPublisher 去远程连接在远程机器上执行操作时,无法引用Jenkins上的变量。

无法引用变量原因(一)

原因是在**sshPublisher**模式下生成流水线脚本时,默认使用的是'''(三个单引号),所以变量对应的值不能进行转义导致报错,需要把:

'''

echo "${env.ZONE}"

'''

改成:

"""

echo "${env.ZONE}"

"""

无法引用变量原因(二)

脚本中的变量需要加上env.前缀,如引用时{env.XXX} 而不是 {XXX}

六、解决Jenkinsfile获取config.properties中的变量含有中文出现乱码问题

1、Jenkins输出和变量含有中文出现乱码问题可能的情况

在 Jenkins Pipeline 中读取config.properties文件时出现中文乱码,通常是由于文件编码不匹配Jenkins 环境配置问题导致的。以下是具体的解决步骤和示例:

(一)核心原因分析

  1. 文件编码与读取编码不一致

    • config.properties文件实际编码为 UTF-8,但readProperties默认使用 ISO-8859-1 编码读取,导致中文乱码。
    • 或文件本身是 GBK/GB2312 编码,但未在读取时指定对应编码。
  2. Jenkins JVM 编码设置不正确

    Jenkins 运行在 JVM 上,若 JVM 默认编码不是 UTF-8,可能影响字符串处理。

  3. 控制台输出编码不匹配

    Jenkins 控制台可能无法正确显示 UTF-8 编码的字符,导致乱码显示。

(二)解决方案

1. 明确指定readProperties的编码参数

在读取config.properties时,强制指定与文件实际编码一致的参数(通常为 UTF-8):

groovy

Groovy 复制代码
pipeline {
    agent any
    stages {
        stage('读取配置文件') {
            steps {
                script {
                    def configFile = "HelloWorld/config.properties"
                    
                    // 关键:指定encoding为UTF-8,与文件实际编码一致
                    def config = readProperties(file: configFile, encoding: 'UTF-8')
                    
                    // 将属性注入环境变量
                    config.each { key, value ->
                        env[key] = value
                    }
                    
                    // 验证读取结果(中文应正常显示)
                    echo "用户名: ${env.USERNAME}"
                }
            }
        }
    }
}
2. 验证并修改config.properties文件编码

确保config.properties文件以UTF-8 无 BOM 编码保存:

  • 使用文本编辑器(如 VS Code、Notepad++)打开文件;
  • 在编辑器中选择 "保存为 UTF-8 无 BOM" 格式;
  • 避免使用 Windows 记事本直接编辑,可能会添加 BOM 头导致问题。
3. 配置 Jenkins JVM 编码参数

确保 Jenkins 启动时使用 UTF-8 编码:

  • 找到 Jenkins 启动配置文件(如/etc/default/jenkinsjenkins.xml);
  • 添加 JVM 参数:-Dfile.encoding=UTF-8
  • 重启 Jenkins 服务。

示例(Linux 系统)

bash

bash 复制代码
# 编辑Jenkins启动配置
vi /etc/default/jenkins

# 添加或修改JVM参数
JAVA_ARGS="-Dfile.encoding=UTF-8 -Xmx1024m"

# 重启Jenkins
systemctl restart jenkins
4. 验证 Jenkins 系统编码

在 Jenkins 系统信息中检查编码设置:

  • 访问http://<jenkins-url>/systemInfo
  • 搜索file.encoding,确保值为UTF-8

(三)进阶调试方法

如果上述步骤仍无法解决问题,可以尝试以下调试方法:

1. 打印文件原始内容(验证读取阶段是否乱码)

groovy

Groovy 复制代码
stage('调试编码') {
    steps {
        script {
            // 直接读取文件内容(不解析properties格式)
            def rawContent = readFile(file: configFile, encoding: 'UTF-8')
            echo "文件原始内容:\n${rawContent}"  // 检查此处中文是否正常
            
            // 按UTF-8解析properties
            def config = readProperties(file: configFile, encoding: 'UTF-8')
            echo "解析后的属性:${config}"
        }
    }
}
2. 检查 Jenkins 节点的系统编码

在 Pipeline 中执行命令检查节点编码:

groovy

Groovy 复制代码
stage('检查系统编码') {
    steps {
        sh 'echo $LANG'  // 输出类似zh_CN.UTF-8
        sh 'locale -a'   // 列出所有可用编码
    }
}

(四)总结

解决中文乱码的关键是确保:

  1. 文件编码 (UTF-8 无 BOM)与readPropertiesencoding参数一致;
  2. Jenkins JVM 编码设置为 UTF-8;
  3. 执行节点的系统编码支持 UTF-8。

2、场景复现及解决方法

2.1、在****config.properties中变量写入中文

2.2、Jenkinsfile测试文件调用中文变量

Groovy 复制代码
pipeline {
    agent any
    stages {
        stage('初始化全局变量') {
            steps {
                script {
                    def configFile = "HelloWorld/config.properties"

                    // 检查配置文件是否存在
                    if (!fileExists(configFile)) {
                        error("配置文件不存在:${configFile}")
                    }
                    
                    // 读取properties文件(自动解析为Map)
                    def config = readProperties(file: configFile, encoding: 'UTF-8')
            
                    config.each { key, value ->
                        env[key] = value
                    }
                }
            }
        }
        
        stage('引用全局变量') {
            steps {
                echo "用户名: ${env.USERNAME}"
            }     
        }
    }
}

2.3、测试结果

用户名: "è¢<<æ°´æ·¹æ­>>的鱼儿"

上述脚本脚本中readProperties读取也采用的是UTF-8格式,还是会出现报错。

3、乱码问题解决

3.1、现在将USERNAME在pipeline脚本中设置成全局变量

其余的其他变量还是配置在config.properties中

Groovy 复制代码
pipeline {
    agent any
    environment {
        // 字符串变量
        USERNAME = "被水淹死的鱼儿"
    }
    stages {
        stage('初始化全局变量') {
            steps {
                script {
                    def configFile = "HelloWorld/config.properties"

                
                    // 检查配置文件是否存在
                    if (!fileExists(configFile)) {
                        error("配置文件不存在:${configFile}")
                    }
                    
                    // 读取properties文件(自动解析为Map)
                    def config = readProperties(file: configFile, encoding: 'UTF-8')
            
                    config.each { key, value ->
                        env[key] = value
                    }
                }
            }
        }
        
        stage('引用全局变量') {
            steps {
                echo "用户名: ${env.USERNAME}"
            }     
        }
    }
}

3.2、测试结果

3.3、结论

经过上述的实验验证在Pipeline流水线创建过程中如果出现中文乱码的问题可以在pipeline脚本中定义全局变量解决乱码问题。

七、一个完整的Pipeline流水线脚本

1、配置变量方便统一配置和管理

一个完整的Pipeline流水线脚本流程(代码提交---> 编译 ---> 构建镜像 ---> jar上传到私有仓库)

config.properties配置文件内容:

javascript 复制代码
PROJECT_NAME="javaweb"
WAREHOUSE_NAME="yb-test"
PASS="xxxx"
USERNAME="被水淹死的鱼儿"
ZONE="registry.cn.a.com"
IMAGE_NAME="javaweb"
IMAGE_TAG="v1.0"

2、Jenkinsfile中集成各个流程的操作步骤

Jenkinsfile Pipeline脚本内容:

Groovy 复制代码
pipeline {
    agent any
    tools {
        maven "maven3" 
    }
    environment {
        // 字符串变量
        USERNAME = "被水淹死的与鱼尔"
    }
    stages {
        stage('初始化全局变量') {
            steps {
                script {
                    def configFile = "HelloWorld/config.properties"
                    
                    // 检查配置文件是否存在
                    if (!fileExists(configFile)) {
                        error("配置文件不存在:${configFile}")
                    }
                    // 读取properties文件(自动解析为Map)
                    def config = readProperties(file: configFile, encoding: 'UTF-8')
    
                    // 将所有属性注入为全局环境变量
                    config.each { key, value ->
                        env[key] = value
                    }
                }
            }
        }
        stage('main拉取代码...') {
            steps {
                git branch: 'main', url: 'http://192.168.72.130:9080/root/java-project.git'
                echo "main代码拉取成功!!!"
            }
        }

        stage('main进行构建...') {
            steps {
                sh """
                   cd HelloWorld
                   mvn clean package
                """
                echo 'main构建成功!!!'
            }
        }
        stage('推送镜像到habor...') {
            steps {   
                sshPublisher(publishers: [sshPublisherDesc(configName: '测试服务器连接', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: """
Docker_Build ()
{
    if docker images | grep -q ${ZONE}/${WAREHOUSE_NAME}/${PROJECT_NAME}; then
        docker rmi -f ${ZONE}/${WAREHOUSE_NAME}/${PROJECT_NAME}:${IMAGE_TAG} && echo "${ZONE}/${WAREHOUSE_NAME}/${PROJECT_NAME}:${IMAGE_TAG}镜像删除成功"|| echo "${ZONE}/${WAREHOUSE_NAME}/${PROJECT_NAME}:${IMAGE_TAG}镜像删除失败!!"
    else
        echo "不存在${ZONE}/${WAREHOUSE_NAME}/${PROJECT_NAME}:${IMAGE_TAG}镜像!!"
    fi

    if docker images | grep -q ${IMAGE_NAME}; then
        docker rmi -f ${IMAGE_NAME}:${IMAGE_TAG} && echo "${PROJECT_NAME}:${IMAGE_TAG}镜像删除成功" || echo "${PROJECT_NAME}:${IMAGE_TAG}镜像删除失败"
    else
        echo "不存在${IMAGE_NAME}:${IMAGE_TAG}镜像!!"
   fi

    docker build -t ${IMAGE_NAME}:${IMAGE_TAG} . && echo "${IMAGE_NAME}:${IMAGE_TAG}镜像构建成功" || { echo "${IMAGE_NAME}:${IMAGE_TAG}镜像构建失败!!";exit 1;}
}


Image_Push ()
{
    docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${ZONE}/${WAREHOUSE_NAME}/${PROJECT_NAME}:${IMAGE_TAG} && echo "${IMAGE_NAME}:${IMAGE_TAG}镜像打包成功" || { echo "${IMAGE_NAME}:${IMAGE_TAG}镜像打包失败!!";exit 1;}
    docker login -u "${USERNAME}" -p "${PASS}" ${ZONE} && echo "登录Habor成功" || { echo "登录Habor失败!!"; exit 1; }
    docker push ${ZONE}/${WAREHOUSE_NAME}/${PROJECT_NAME}:${IMAGE_TAG} && echo "${IMAGE_NAME}:${IMAGE_TAG}镜像成功推送到远程仓库" || { echo "${IMAGE_NAME}:${IMAGE_TAG}镜像推送到远程仓库失败!!";exit 1; }
}
Docker_Build
Image_Push""", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: true)])
            echo "推送镜像成功!!!"
            }
        }
    }
}

3、完整的发布流程

3.1、Gitlab上准备好config.properties和Jenkinsfile

3.2、在Jenkinsfile上同步分支信息

3.3、查看构建过程是否出现报错

3.4、验证镜像是否构建成功是否已经推送到远程仓库

私有镜像仓库:

相关推荐
2401_836836591 小时前
k8s配置管理
云原生·容器·kubernetes
一切顺势而行1 小时前
k8s 使用docker 安装教程
docker·容器·kubernetes
霖檬ing1 小时前
K8s——配置管理(1)
java·贪心算法·kubernetes
澜兮子1 小时前
k8s-服务发布基础
云原生·容器·kubernetes
小安运维日记1 小时前
CKS认证 | Day4 最小化微服务漏洞
安全·docker·微服务·云原生·容器·kubernetes
2401_836836591 小时前
k8s服务发布进阶
云原生·容器·kubernetes
云上小朱6 小时前
问题处理-k8s环境中,hadoop端口9000无法被访问
kubernetes
敖行客 Allthinker11 小时前
云原生安全观察:零信任架构与动态防御的下一代免疫体系
安全·ai·云原生·架构·kubernetes·ebpf