拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Jenkins Environment Injector 环境变量注入实战

Jenkins 上跑构建最让人抓狂的往往不是代码编不过而是构建机上那堆环境变量。同一台机器白天手动敲命令跑得好好的一到 Jenkins 里就报mvn: command not found、JAVA_HOME is not set换个 Job 又得改一遍路径再碰上多环境部署测试库和生产库的地址还不一样。这些破事我踩过不止一次。Environment Injector早期版本叫 EnvInject就是专门用来收拾这个烂摊子的——它让你在 Job 级别注入环境变量既不用去动 Jenkins 进程本身也不至于把全局配置搅成一锅粥。这篇就把我这几年用它的完整套路摊开讲插件怎么装、Properties File 和 Properties Content 到底怎么选、PATH 怎么追加才不把系统路径冲掉、Pipeline 里怎么等价实现、以及那些官方文档从来不写的坑。1. 先搞明白 Jenkins 里的环境变量到底分几层1.1 从 Jenkins 进程到 shell变量经历了几道手很多人一上来就蒙是因为没意识到Jenkins 的环境变量根本不是一个东西而是一条链路。最底层是 Jenkins 进程自己的环境变量它由启动方式决定Linux 上用 systemd 托管的变量来自 unit 文件里的Environment用java -jar jenkins.war手动起的继承的是当前登录 shell 的环境。Windows 上如果是装成服务跑的那继承的就是服务账户通常是 LocalSystem的系统环境变量跟你当前登录用户的 PATH 完全是两码事。这就是为什么我在命令行里java -version好好的Jenkins 里就是找不到 Java——根本不是一个环境。往上一层是全局属性路径在Manage Jenkins → System → Global properties勾上 Environment variables 就能加键值对。这一层的变量对所有节点、所有 Job 生效用来放公司统一的仓库地址、统一的MAVEN_OPTS这类东西最合适。再往上是节点属性在Manage Jenkins → Nodes → 选中节点 → Configure → Node Properties里配只对绑定到该节点的 Job 生效适合处理这台机器装了 JDK 8那台装了 JDK 17这种差异。最上面才是 Job 自己的层也是本文的主角。理解这条链路的实际意义在于当变量冲突时你脑子里得有一张优先级表而不是对着日志瞎猜。1.2 Environment Injector 真正解决的是什么问题先说清楚它不是干什么的。它不是用来替代系统环境变量的也不是让你把JAVA_HOME这种全局性的东西到处塞。它解决的是三类很具体的场景。第一类是Job 级别的差异同一个 Jenkins 上跑着十几个项目A 项目要 JDK 8B 项目要 JDK 17你不可能为了这个去改 Jenkins 进程的启动参数改一次全站遭殃。第二类是动态变量。比如版本号要拼成1.0.${BUILD_NUMBER}构建时间要写成BUILD_TIMESTAMP产物路径要根据当天日期分目录。这些东西没法预先写死在系统里只能靠 Jenkins 提供的变量在构建时展开。Environment Injector 的 Properties Content 支持引用 Jenkins 内置变量正好干这个。第三类是从文件读变量。很多项目根目录下已经有一个.env或者config/env.properties本地开发时 source 一下就完事了。CI 上你不想再维护一份重复的键值对那就直接告诉插件去读那个文件的路径。这一点在做多环境部署时特别香——准备dev.properties、test.properties、prod.properties三份文件构建时用参数决定读哪一份代码里一行都不用改。1.3 几种注入方式的横向对比别一上来就无脑用插件先把可选方案摆出来对比一下很多人其实是杀鸡用了牛刀。方式生效范围是否需要额外插件能否动态计算适合的场景Jenkins 进程环境全站所有 Job否否JDK 主版本、系统级工具路径全局属性全站所有 Job、所有节点否部分可引用内置变量公司统一仓库地址、代理配置节点属性绑定该节点的 Job否部分节点特有的工具链路径Environment Injector单个 Job是是按项目/按环境注入差异变量构建参数单个 Job否否由触发者决定需要人工选择的输入项Pipelineenvironment {}单个 Pipeline否是Pipeline 项目的首选withEnv代码块内否是临时覆盖、局部作用域一句话总结我的经验能放进 Pipeline 的就别用插件自由风格项目才需要 Environment Injector。插件本身对 Pipeline 的支持一直比较有限官方也更推荐在 Pipeline 里用environment块加withEnv来等价实现这点后面会详细讲。注意不要为了图省事把密码类的东西直接写进全局属性或 Properties Content。全局属性对所有 Job 可见任何一个能建 Job 的人都能读出来这是很典型的内控漏洞。2. 插件安装与配置界面速通2.1 在线装和离线装两条路在线装很简单Manage Jenkins → Plugins → Available plugins搜索框里敲Environment Injector勾选后点安装重启 Jenkins 生效。这里有个小坑插件历史上改过名早期叫 EnvInject Plugin现在叫 Environment Injector Plugin如果你照着老教程搜 EnvInject 搜不到别慌就是它改名了。另一个坑是部分旧版本的插件依赖Token Macro之类的组件安装时提示依赖冲突正确的做法是先把 Jenkins 升到 LTS 的较新版本别硬着头皮去手动降级依赖。离线环境就得走另一条路。你需要先在有网机器上打开插件更新站点页面下载对应的.hpi文件注意要连带把依赖的插件一起下下来否则装完报Dependency failed。上传路径是Manage Jenkins → Plugins → Advanced settings → Deploy Plugin选择文件上传即可。离线装插件最容易翻车的点是版本不匹配.hpi里声明的 Jenkins 核心版本要求高于你当前跑的版本装上去要么不生效要么让 Jenkins 起不来。所以我一般会先在测试实例上装一遍验证再推到生产。2.2 自由风格项目里的三个入口进入一个自由风格 Job 的配置页找到Build Environment这个区块勾选Inject environment variables to the build process展开后你会看到几个输入框一个是Properties File Path一个是Properties Content不同版本可能还有Script File Path/Script Content以及是否在构建前执行的选项。这里先把作用讲清楚免得配了半天不知道自己在干嘛。Properties File Path填的是一个路径插件会在构建开始前读这个文件把里面的键值对塞进环境变量。路径支持引用 Jenkins 内置变量比如/data/ci/env/${BUILD_ENV}.properties这样就实现了用参数选配置文件的效果。Properties Content则是直接写内容适合放少量固定值和动态拼接值比如APP_VERSION1.0.${BUILD_NUMBER}。至于 Script 相关的选项是用来跑一段脚本、把脚本的标准输出当作键值对解析的——日志打印一定要注意脚本里任何多余的echo调试语句都会污染输出导致解析失败。这玩意儿我一般不用除非确实需要从外部系统拉配置。2.3 Properties 文件的格式规则别想当然这块是重灾区我见过太多次因为格式问题排查半天的。规则如下每行是KEYVALUE等号两边不要加空格加了空格在某些版本里会被当成值的一部分#开头是注释空行会被忽略不要写export这是 shell 语法不是 properties 语法不要给值加引号。APP_NAMEmyapp读出来的值就是带引号的myapp到了 shell 里就变成字符串字面量很容易引发诡异的路径不存在问题文件编码统一用 UTF-8别带 BOM。Windows 上用记事本另存为 UTF-8 经常会带上 BOM第一个键名前面会多出几个不可见字符导致这个变量死活读不出来换行符用 LF。从 Windows 拷过去的文件如果是 CRLF变量值末尾可能带上一个\r表现出来就是路径明明存在却提示找不到。注意\r这个问题极其隐蔽用echo $VAR | od -c看一眼结尾字节比盯着配置页猜半天高效得多。3. 手把手实操给一个 Java Web 项目配通注入3.1 需求拆解与参数设计假设我们有这么个场景一个 Spring Boot 项目用 Maven 构建需要部署到 dev 和 prod 两套环境构建机上 JDK 装在/usr/local/jdk1.8.0_202Maven 装在/usr/local/maven-3.6.3。Jenkins 进程本身跑在 JDK 11 上所以绝对不能在全局属性里改JAVA_HOME那样会把 Jenkins 自己搞挂。需要在 Job 级别把JAVA_HOME覆盖成 JDK 8。先把变量清单列出来这一步别省JAVA_HOME指向 JDK 8MAVEN_HOME指向 Maven 目录PATH 要追加 Maven 的 bin 目录APP_ENV来自构建参数APP_VERSION用构建号动态拼BUILD_TIME用时间戳NACOS_ADDR根据环境从配置文件读。前五个用 Properties Content 处理最后一个用 Properties File Path 处理正好把两种方式都用上。3.2 Properties Content 的具体写法在Build Environment里勾选注入Properties Content填入# 构建工具链只对本 Job 生效 JAVA_HOME/usr/local/jdk1.8.0_202 MAVEN_HOME/usr/local/maven-3.6.3 PATHMAVEN${MAVEN_HOME}/bin # 动态变量 APP_ENV${BUILD_ENV} APP_VERSION1.0.${BUILD_NUMBER} BUILD_TIME${BUILD_TIMESTAMP}这里重点讲PATHMAVEN这个写法它是插件特有的语法糖含义是把值合并进现有的 PATH。如果你老老实实写成PATH${MAVEN_HOME}/bin那就等于把整条 PATH 覆盖掉了构建步骤里连sh、ls这类基础命令都找不着报错信息还特别迷惑人。这个坑我当年踩了两次才记住现在只要看到构建突然报一堆命令找不到第一反应就是去检查 PATH 是不是被覆盖了。至于${BUILD_ENV}、${BUILD_NUMBER}、${BUILD_TIMESTAMP}都是 Jenkins 内置变量写的时候直接引用即可。前提是这个 Job 已经配了BUILD_ENV这个构建参数参数在注入阶段之前就已经确定了所以可以安全引用。3.3 Properties File Path 与多环境切换接着配Properties File Path为/data/ci/env/${APP_ENV}.properties。构建机上准备三个文件内容大概长这样# /data/ci/env/dev.properties NACOS_ADDRdev-nacos.internal:8848 DB_URLjdbc:mysql://dev-db:3306/app LOG_LEVELDEBUG# /data/ci/env/prod.properties NACOS_ADDRprod-nacos.internal:8848 DB_URLjdbc:mysql://prod-db:3306/app LOG_LEVELINFO注意这里有个细节Properties File Path里我用的是${APP_ENV}而${APP_ENV}本身是在 Properties Content 里由${BUILD_ENV}赋值来的。这两个字段的执行顺序是先 File 后 Content 还是先 Content 后 File在不同版本里表现不一致所以更稳妥的做法是路径里直接用${BUILD_ENV}/data/ci/env/${BUILD_ENV}.properties别绕这一道弯少一个变量依赖就少一个排查点。3.4 构建步骤里怎么验证变量真的进去了配完之后别急着跑完整构建加一个临时构建步骤选择Execute shell写echo JAVA_HOME$JAVA_HOME echo MAVEN_HOME$MAVEN_HOME echo PATH$PATH echo APP_VERSION$APP_VERSION java -version mvn -v这一步的价值在于把注入是否生效和构建脚本是否有问题两件事分开。我见过不少人一上来就配完整流程结果失败了根本分不清是注入没生效还是 Maven 参数写错了。先跑这个验证步骤确认java -version输出的是 1.8 而不是 11确认mvn -v能正常输出再往下走。验证通过后把这个步骤删掉或者注释掉别留一堆无意义的日志输出。实操心得Jenkins 内置变量的完整清单在你的Jenkins地址/env-vars.html这个页面上直接在浏览器打开就能看到当前节点上所有可用的变量名比翻文档准。每次换 Jenkins 版本我都习惯先去看一眼防止某些变量被改名。4. Pipeline 项目里的等价实现4.1 为什么 Pipeline 不建议硬套插件Environment Injector 是围绕自由风格 Job 的构建环境设计的它在 Pipeline 里的支持一直不算完整。Pipeline 本身就是 Groovy 脚本环境变量的控制能力比插件强得多硬要绕回去用插件属于自找麻烦。正确的姿势是用声明式 Pipeline 的environment块加上withEnv指令。environment块定义的是整个 Pipeline 或者某个 stage 可见的变量作用域清晰、可读性好。withEnv则是块级作用域出了花括号就失效适合临时覆盖。关键区别在于environment块里的变量对 Groovy 代码和 shell 都可见而withEnv主要影响 shell 步骤的执行环境在 Groovy 里访问需要走env.XXX。4.2 一份可以直接抄的 Pipeline把上一节自由风格 Job 的需求改写成 Pipeline大致是这样pipeline { agent any parameters { choice(name: BUILD_ENV, choices: [dev, test, prod], description: 部署环境) } environment { JAVA_HOME /usr/local/jdk1.8.0_202 MAVEN_HOME /usr/local/maven-3.6.3 PATH ${env.JAVA_HOME}/bin:${env.MAVEN_HOME}/bin:${env.PATH} APP_ENV ${params.BUILD_ENV} APP_VERSION 1.0.${env.BUILD_NUMBER} } stages { stage(环境检查) { steps { sh echo JAVA_HOME$JAVA_HOME echo APP_VERSION$APP_VERSION java -version mvn -v } } stage(读取环境配置) { steps { script { def props readProperties file: /data/ci/env/${params.BUILD_ENV}.properties props.each { k, v - env.${k} v } } } } stage(构建) { steps { withEnv([SPRING_PROFILES_ACTIVE${params.BUILD_ENV}]) { sh mvn -B clean package -DskipTests } } } } }这里有几个点值得展开说。PATH 的拼接在 Pipeline 里是手写的注意格式是冒号分隔并且一定把${env.PATH}放在最后面否则会把系统路径冲掉——这和自由风格里PATHMAVEN想达到的效果是一样的。readProperties是 Pipeline Utility Steps 插件提供的步骤用来读 properties 文件返回一个 Map。拿到之后逐项写进env这样后续的 shell 步骤才能通过$NACOS_ADDR这种方式访问。注意env.${k} v这种动态键名的写法是 Groovy 的特性不这么写拿不到变量名。4.3 凭据注入的正确姿势密码类的变量千万别写进environment块那等于明文写在 Jenkinsfile 里谁有代码仓库权限谁就能看到。正确做法是用 Jenkins 的凭据管理配合withCredentialsstage(部署) { steps { withCredentials([ string(credentialsId: prod-db-password, variable: DB_PASSWORD), usernamePassword(credentialsId: prod-deploy-account, usernameVariable: DEPLOY_USER, passwordVariable: DEPLOY_PASS) ]) { sh set x sshpass -p $DEPLOY_PASS ssh $DEPLOY_USERtarget-host deploy.sh set -x } } }set x这行的作用是把 shell 的命令回显关掉防止密码被打印进构建日志。Jenkins 本身对凭据变量做了掩码处理日志里会显示成****但对命令本身回显的内容不总是能完全拦住尤其是变量被拼进字符串再输出的时候。所以双重保险关回显同时不要在脚本里echo密码变量。注意withCredentials注入的变量只在该代码块内有效出了块就没了。如果你在块外面用$DB_PASSWORD拿到的会是空字符串这不是 Bug是设计如此。5. 变量优先级与冲突排查速查5.1 同名变量谁说了算这是最常被问到的问题我整理成一张表从低到高排列后面的覆盖前面的优先级来源生效范围说明1Jenkins 进程环境变量全部systemd unit 或服务账户决定2全局属性全部 Job、全部节点改动影响面最大慎用3节点属性绑定该节点的 Job处理机器间工具链差异4Environment Injector 的 Properties File Path单个 Job读文件适合多环境配置5Environment Injector 的 Properties Content单个 Job实测同名会覆盖 File 里的值6构建参数 /withEnv/withCredentialsJob 或代码块每次构建可能变化的输入7脚本内export当前 shell最后一道覆盖优先级最高这张表最实用的地方在于排查思路变量值不对先看是哪一层的问题而不是从头翻配置。比如你在 Job 里明明注入了JAVA_HOME构建里却还是 11那八成是 shell 脚本里自己又export了一遍或者某个构建工具的包装脚本重设了。5.2 常见问题速查表现象最可能的原因处理方式变量在 shell 里是空的没勾选注入选项或该变量定义在withEnv块外检查 Build Environment 勾选状态与作用域mvn: command not found用PATH覆盖了系统 PATH改成PATHMAVEN或手写完整拼接变量值带上了引号properties 文件里写了KEYvalue去掉引号properties 格式不需要引号第一个变量读不出来后面正常文件带 UTF-8 BOM用编辑器转成无 BOM 的 UTF-8路径存在却报找不到行尾是 CRLF值末尾多了\r用od -c确认并转成 LFWindows 节点上中文乱码编码与代码页不一致统一用 UTF-8必要时加-Dfile.encodingUTF-8Pipeline 里$VAR取不到变量定义在environment块但 shell 用了单引号加 Groovy 插值混淆分清sh ...和sh ...的插值差异${BUILD_NUMBER}没展开写在了单引号字符串里换成双引号或直接在 properties 里引用5.3 几个文档里不会写的避坑技巧第一个技巧给注入的变量加统一前缀。我习惯把项目相关的变量都加上APP_前缀比如APP_ENV、APP_VERSION。这样做的好处是万一和 Jenkins 内置变量重名一眼就能看出来而且排查时用env | grep APP_就能把所有相关变量列出来比一个个echo效率高太多。第二个技巧把 Properties Content 抽成版本控制里的文件然后用 File Path 引用。直接在配置页里写内容的问题是没法做版本管理改了什么都追溯不到。我现在的做法是项目仓库里维护一份ci/env.properties构建时先用git checkout拉代码再用相对路径引用它。注意这里有个时序问题注入发生在构建步骤之前而代码是构建步骤里才拉下来的所以要么用绝对路径引用一份预先部署好的文件要么把注入逻辑放进构建步骤里用脚本 source。这个顺序搞反了会报文件不存在很多人卡在这里。第三个技巧换 Jenkins 版本前先验证注入行为。插件的实现细节在不同大版本之间会变尤其是 File 和 Content 的执行顺序、路径展开的时机。我的习惯是在测试实例上跑一遍最小验证 Job确认行为符合预期再升级生产。6. 这套东西还能怎么用6.1 多环境部署的集中管理把变量分成共享和差异两部分是这套方案的精髓。共享的部分JDK 路径、Maven 路径、公司私有仓库地址放全局属性或节点属性一次配好所有 Job 都能用差异的部分数据库地址、消息队列地址、日志级别放各自的 properties 文件按BUILD_ENV参数选择。这样新增一个环境只需要加一个 properties 文件不用去动任何 Job 配置。如果环境数量多起来可以考虑用 Config File Provider 插件把 properties 文件本身也托管到 Jenkins 里支持在页面上编辑和做权限控制。代价是配置不再跟着代码走多人协作时容易脱节。我的判断标准是跟着代码走的配置放仓库跟机器环境强相关的配置放 Jenkins。6.2 与容器化构建的衔接现在很多构建步骤是在容器里跑的比如docker run起一个编译镜像。这时候要注意Jenkins 节点上注入的环境变量不会自动进容器得显式传进去docker run --rm \ -e JAVA_HOME/usr/local/jdk1.8.0_202 \ -e APP_VERSION$APP_VERSION \ -e SPRING_PROFILES_ACTIVE$APP_ENV \ -v $WORKSPACE:/workspace \ -w /workspace \ build-image:latest \ mvn -B clean package -DskipTests-e后面跟着的变量名左边的容器内名字右边是宿主机上的值写的时候别把$漏了。如果变量很多一个个写太累可以先把变量导到一个文件里再用--env-file传进去这样干净得多。6.3 从自由风格迁移到 Pipeline 的取舍如果你手上有一堆配好 Environment Injector 的自由风格 Job不建议为了迁移而迁移。判断标准很简单这个 Job 的逻辑是不是经常变、是不是需要复杂的条件判断和并行执行如果是迁到 Pipeline 收益明显如果只是一个拉代码、跑 Maven、传产物的固定流程自由风格加 Environment Injector 完全够用而且后来接手的人不用懂 Groovy 也能改。真要迁的话变量这部分是最好迁的把 Properties Content 里的键值对原样搬进environment块把 Properties File Path 换成readProperties步骤。唯一需要额外留意的是作用域——自由风格里注入的变量整个构建过程都能看到Pipeline 里如果写在了某个 stage 的environment块里其他 stage 是看不到的。这一点不搞清楚迁完会出现前半段能读到变量、后半段读不到的怪现象。最后分享一个我用了很久的小习惯在每个 Job 的第一个构建步骤里加一行env | sort | grep -v -i passw把当前所有环境变量按字母序打印出来。它看起来有点土但排查问题时省下的时间远超那几行日志的成本。尤其是在多节点、多版本混用的环境里一眼就能看出变量到底从哪一层来的比翻配置页快多了。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门