Jenkins Shared Library加载、信任与版本治理
Shared Library既是流水线复用工具,也是供应链代码。几十个项目调用同一个 deployProduction() 时,一次库更新可能同时改变所有发布行为;如果它被配置为Trusted Library,提交者甚至可能获得Controller级能力。因此共享库必须像框架和生产平台一样做API、版本、测试、权限和灰度治理。
学习目标
- 解释
vars/、src/、resources/和根目录文件的职责。 - 区分
@Library编译期加载、隐式加载和library运行期加载。 - 解释Global Variable、classpath、CPS和序列化边界。
- 区分Trusted、Untrusted和Folder-scoped Library的安全能力。
- 设计无状态、显式参数、可兼容演进的共享库API。
- 避免Shell注入、凭据泄露和高权限Agent滥用。
- 使用固定Tag/Commit、语义版本和变更日志治理升级。
- 区分单元测试、Pipeline模拟测试和Jenkins集成测试。
- 完成灰度项目升级、批量迁移、失败回退和影响审计。
一、什么时候应该使用Shared Library
适合抽取:
- 多项目一致的Maven/JDK 7/8/17构建规范。
- 镜像标签、SBOM、签名和推送。
- 测试报告和质量门。
- 标准灰度发布、指标验证和通知。
- 凭据作用域和安全检查。
不适合抽取:
- 只出现一次的项目特殊逻辑。
- 仍在快速变化且没有稳定契约的步骤。
- 复杂业务判断。
- 每个项目差异巨大却强行用几十个Boolean参数统一。
共享库目标是统一稳定能力,不是让Jenkinsfile变短到看不出实际发布流程。
二、目录结构
company-jenkins-library/
├── vars/
│ ├── javaServicePipeline.groovy
│ ├── javaServicePipeline.txt
│ ├── buildContainerImage.groovy
│ └── notifyRelease.groovy
├── src/
│ └── com/company/jenkins/
│ ├── ImageRef.groovy
│ ├── PipelineConfig.groovy
│ └── MavenBuilder.groovy
├── resources/
│ ├── com/company/jenkins/default-values.yaml
│ └── com/company/jenkins/scripts/verify-release.sh
├── test/
├── README.md
└── CHANGELOG.md三、vars目录
vars/foo.groovy通常暴露为Jenkinsfile可直接访问的全局变量 foo。
// vars/sayHello.groovy
def call(String name) {
echo "hello ${name}"
}调用:
sayHello('order-service')call()让变量像函数调用。也可以暴露多个方法:
def success(String message) { echo "SUCCESS: ${message}" }
def failure(String message) { echo "FAILURE: ${message}" }vars/foo.txt可为全局变量提供帮助文档,具体渲染与生成时机依赖Jenkins加载和使用情况。
四、vars为什么应尽量无状态
Global Variable脚本在一次Pipeline中可能以共享实例方式使用。若使用字段保存可变状态:
@groovy.transform.Field
int counter = 0多个Stage或调用可能互相影响,Controller重启时还涉及序列化。共享库方法应优先:
- 参数显式传入。
- 返回结果显式传出。
- 不依赖调用顺序。
- 不把环境和凭据存字段。
- 不保存Stream、Matcher等复杂对象。
五、src目录
src/按包结构放类,进入共享库classpath:
package com.company.jenkins
class ImageRef implements Serializable {
String registry
String repository
String tag
String fullName() {
"${registry}/${repository}:${tag}"
}
}Jenkinsfile在 @Library编译期加载后可以import:
@Library('company-lib@v2.3.1') _
import com.company.jenkins.ImageRef类若跨Pipeline暂停点存活,字段需要可序列化。不要把Jenkins内部对象、文件流或网络连接长期保存在类字段。
六、类如何调用Pipeline Step
普通类没有自动的 sh、echo等全局Step上下文。常见做法是注入steps/script:
package com.company.jenkins
class MavenBuilder implements Serializable {
private final def steps
MavenBuilder(steps) {
this.steps = steps
}
void verify() {
steps.sh 'mvn --batch-mode clean verify'
}
}vars中创建:
def call() {
new com.company.jenkins.MavenBuilder(this).verify()
}注入CPS Script后仍要注意序列化和生命周期。类负责小型领域封装,不能变成持有大量Pipeline运行状态的对象树。
七、resources目录
资源可通过 libraryResource读取:
def scriptText = libraryResource(
'com/company/jenkins/scripts/verify-release.sh'
)
writeFile file: 'verify-release.sh', text: scriptText
sh 'chmod 700 verify-release.sh && ./verify-release.sh'资源适合模板、默认配置和短脚本。注意:
- 资源跟随共享库版本。
- 写入Workspace后可能被修改。
- 不能包含Secret。
- 执行脚本前应固定版本、检查内容和权限。
- 大型二进制不应塞进Git共享库。
八、根目录文件
README、CHANGELOG、测试配置和依赖描述属于库工程治理,不会像vars自动变成Pipeline全局变量。文档应包含:
- 支持的Jenkins/插件版本。
- API示例。
- 参数与默认值。
- 破坏性变更。
- 升级/回退指南。
- 安全权限要求。
九、@Library编译期加载
@Library('company-lib@v2.3.1') _
import com.company.jenkins.ImageRef大致过程:
flowchart TD
A["读取Jenkinsfile"] --> B["解析@Library名称与版本"]
B --> C["按SCM Retriever拉取库版本"]
C --> D["把src加入classpath并准备vars"]
D --> E["编译/CPS转换Jenkinsfile"]
E --> F["运行时调用全局变量和类"]因为在脚本编译前加载,所以可以import src类。
十、隐式加载
管理员可配置“Load implicitly”,让所有Pipeline自动加载某库。优点是Jenkinsfile简短,缺点是依赖不可见、升级影响范围巨大。
生产建议:
- 基础安全库可以谨慎隐式加载。
- 业务发布库优先显式声明版本。
- 文档和Build日志记录最终库版本。
- 不让默认分支漂移悄悄改变所有项目。
十一、library运行期加载
Pipeline library Step可在运行时根据变量加载库:
def lib = library("company-lib@${params.LIB_VERSION}")但Jenkinsfile已经完成编译,不能像 @Library一样在源码顶部静态import刚动态加载的类。通常通过返回的库对象/动态访问方式使用,具体API随插件版本。
运行期版本参数必须白名单,不能允许用户传任意SCM Ref加载未评审代码。
十二、SCM Retriever与版本
共享库可使用Modern SCM或Legacy SCM等检出方式。应记录:
- 仓库URL。
- Credentials。
- 默认版本。
- 是否允许调用方覆盖版本。
- Tag/Branch/Commit解析方式。
- 缓存与刷新策略。
Legacy变量替换路径更容易配置错误,优先使用当前插件支持的Modern SCM能力。
十三、为什么不能默认跟main
@Library('company-lib@main') _main每次构建可指向不同Commit:
项目代码没变
→ 共享库main变化
→ 流水线行为变化
→ Build不可复现生产项目应固定经过验证的Tag或Commit:
@Library('company-lib@v2.3.1') _Tag还应保护、签名或由受控发布流程创建,防止被移动覆盖。
十四、Library版本何时确定
@Library版本在Jenkinsfile编译/加载阶段决定;运行中即使远程Tag被恶意移动,当前已检出内容通常不会自动替换,但后续Build会受影响。应在日志记录库Commit SHA,而不只记录Tag。
十五、Trusted Library是什么
Trusted Library中的代码可获得比Sandbox脚本更高的能力,可能调用Jenkins内部API、访问Controller文件和敏感对象。能向Trusted Library仓库提交代码的人,实际上可能接近Jenkins管理员权限。
因此必须:
- 独立受保护仓库。
- 强制评审和CODEOWNERS。
- 禁止普通项目开发者直接写入。
- 固定版本与签名。
- 安全扫描。
- 审计每次库版本使用。
- 最小化Trusted代码量。
十六、Untrusted与Folder Library
Untrusted Library通常受Groovy Sandbox和Script Approval约束;Folder级共享库通常用于团队范围并保持不可信边界,具体能力依赖Jenkins和相关插件版本。
Sandbox不是业务权限模型。即使脚本API受限,只要它能在高权限Agent执行Shell,仍可能读取生产凭据或Docker Socket。还必须隔离Agent、Credential和网络。
十七、Trusted Library供应链攻击
flowchart TD
A["攻击者修改Trusted Library"] --> B["项目下次Build加载恶意版本"]
B --> C["读取Controller/凭据或调用内部API"]
C --> D["控制Agent、制品或生产部署"]共享库仓库、SCM凭据、Tag和发布账号都属于生产供应链资产。
十八、CPS对共享库的影响
vars和Groovy Pipeline逻辑通常也参与CPS转换。共享库不是逃离CPS的普通Groovy环境。
同样要遵守:
- 复杂对象不跨Step。
@NonCPS不调用Pipeline Step。- 类字段可序列化。
- 不持有Stream/Socket/Matcher。
- Controller重启恢复需要Step支持。
十九、不要把所有方法加@NonCPS
错误做法:为避免序列化错误把构建、部署整个方法标记 @NonCPS,内部仍调用sh。正确做法是让流程方法保持CPS,把短小纯计算函数标记NonCPS,或把复杂工作交给外部CLI。
二十、共享库API设计
不推荐:
deploy(true, false, true, 'x', null, 3)推荐命名配置:
deployService(
environment: 'staging',
imageDigest: 'registry.example.com/order-api@sha256:...',
canaryPercent: 10,
verifyBusinessMetrics: true
)然后进行必填、类型、范围和组合约束验证。
二十一、配置对象与默认值
package com.company.jenkins
class DeployConfig implements Serializable {
String environment
String imageDigest
int canaryPercent = 1
void validate() {
if (!(environment in ['test', 'staging', 'prod'])) {
throw new IllegalArgumentException('environment不允许')
}
if (!(imageDigest ==~ /registry\.example\.com\/[a-z0-9-]+@sha256:[a-f0-9]{64}/)) {
throw new IllegalArgumentException('imageDigest格式或仓库不允许')
}
if (canaryPercent < 1 || canaryPercent > 100) {
throw new IllegalArgumentException('canaryPercent必须在1到100')
}
}
}默认值变化也可能是破坏性变更。例如默认 canaryPercent 从1改为100,会让未显式传参的项目直接全量发布。
二十二、避免隐藏环境依赖
共享库内部不应悄悄读取几十个 env.*和全局凭据。应把项目差异显式传入,并在开始阶段输出脱敏后的最终配置。
隐藏依赖会导致:同一Jenkinsfile在不同Folder、Agent或Controller行为不同,难以复现。
二十三、Shell参数注入风险
旧式危险写法:
sh "docker build -f ${config.dockerfile} -t ${config.image}:${config.tag} ."如果参数包含Shell元字符,可能执行额外命令。Groovy插值还会把值放进Step参数。
治理:
- 对仓库、文件相对路径、Tag做严格白名单。
- 不允许
..、换行和Shell元字符。 - 使用环境变量与单引号Groovy脚本。
- 更复杂时写受测CLI,使用参数数组。
- 不让不可信PR调用生产发布API。
二十四、安全buildContainerImage Demo
vars/buildContainerImage.groovy:
def call(Map raw = [:]) {
String repository = raw.repository as String
String tag = raw.tag as String
String dockerfile = (raw.dockerfile ?: 'Dockerfile') as String
if (!(repository ==~ /registry\.example\.com\/[a-z0-9-]+/)) {
error 'repository不在允许Registry或格式错误'
}
if (!(tag ==~ /[A-Za-z0-9][A-Za-z0-9_.-]{0,127}/)) {
error 'tag格式错误'
}
List<String> pathSegments = dockerfile.tokenize('/')
boolean safeRelativePath = dockerfile ==~ /(?:[A-Za-z0-9_.-]+\/)*[A-Za-z0-9_.-]+/
boolean hasTraversalSegment = pathSegments.any { it == '.' || it == '..' }
if (!safeRelativePath || hasTraversalSegment) {
error 'dockerfile路径不安全'
}
withEnv([
"SAFE_REPOSITORY=${repository}",
"SAFE_TAG=${tag}",
"SAFE_DOCKERFILE=${dockerfile}"
]) {
sh '''
set -eu
docker build \
-f "$SAFE_DOCKERFILE" \
-t "$SAFE_REPOSITORY:$SAFE_TAG" \
.
'''
}
return "${repository}:${tag}"
}这里的 ==~ 是 Groovy 的“整串匹配”,不是只要找到一个合法片段就通过。路径规则只接受由字母、数字、点、下划线和短横线组成的相对路径段,因此 /etc/passwd、C:\\secret、空路径、换行和 Shell 元字符都会失败;随后再按路径段拒绝单独的 . 与 ..,避免工作区路径穿越。仅仅判断字符串“不包含 ..”仍不够,因为还必须拒绝绝对路径和平台分隔符差异。
白名单只能约束字符串形态。若 Agent 工作区中存在指向外部目录的符号链接,还应在受测 CLI 中调用 toRealPath(),确认解析后的 Dockerfile 仍位于 Workspace 根目录下。共享库脚本本身不应依赖一组容易写错的 Shell 字符串判断来完成最终文件系统授权。
该方法只 Build,不自动 Push 和部署,遵循单一职责。这样 Build 失败不会触发半完成发布,调用方也能分别对镜像构建、制品准入、凭据使用和生产部署设置权限与重试边界。
二十五、Publish与Credentials分离
def publishContainerImage(String image, String credentialsId) {
// 还需验证image和允许的credentialsId映射
withCredentials([usernamePassword(
credentialsId: credentialsId,
usernameVariable: 'REGISTRY_USER',
passwordVariable: 'REGISTRY_PASSWORD'
)]) {
withEnv(["SAFE_IMAGE=${image}"]) {
sh '''
set +x
echo "$REGISTRY_PASSWORD" | \
docker login registry.example.com \
-u "$REGISTRY_USER" --password-stdin
docker push "$SAFE_IMAGE"
docker logout registry.example.com
'''
}
}
}调用方不能任意传生产Credentials ID。共享库应按环境和Folder策略映射允许凭据,生产发布还要使用隔离Agent。
二十六、模板型总Pipeline
vars/javaServicePipeline.groovy:
def call(Map config = [:]) {
pipeline {
agent none
options {
timestamps()
disableConcurrentBuilds()
}
stages {
stage('Build and Test') {
agent { label config.buildLabel ?: 'linux && jdk8' }
steps {
checkout scm
sh 'mvn --batch-mode clean verify'
}
post {
always {
junit allowEmptyResults: true,
testResults: 'target/surefire-reports/*.xml'
}
}
}
stage('Build Image') {
agent { label 'linux && docker' }
steps {
script {
buildContainerImage(
repository: config.repository,
tag: env.GIT_COMMIT
)
}
}
}
}
}
}项目Jenkinsfile:
@Library('company-lib@v2.3.1') _
javaServicePipeline(
repository: 'registry.example.com/order-api',
buildLabel: 'linux && jdk8'
)模板型Pipeline统一能力强,但过度封装会让项目无法扩展。应提供受控Hook或组合式Step,而不是无限参数。
二十七、组合优于巨型模板
可提供小型步骤:
mavenVerify()
buildContainerImage()
scanImage()
publishImage()
deployCanary()
verifyRelease()Jenkinsfile显式组合,既复用又能看懂流程。对高度统一的服务再提供模板入口。
二十八、API向后兼容
破坏性变更包括:
- 删除参数。
- 改参数类型。
- 改默认值。
- 改返回值。
- 改异常/Build Result。
- 改Stage名称影响监控。
- 改凭据或Agent要求。
兼容策略:
- 新增可选参数。
- 旧参数保留并输出Deprecated警告。
- 提供迁移期。
- 重大变化发布新Major版本。
- 不在同一Tag覆盖代码。
二十九、语义版本
示意:
v2.3.1- Patch:Bug修复,行为应兼容。
- Minor:向后兼容的新能力。
- Major:允许破坏性变化,并提供迁移指南。
流水线行为和安全默认值变化是否兼容,需要平台团队评审,不能只按代码编译是否通过判断。
三十、测试金字塔
flowchart TD
A["纯Groovy单元测试:校验/格式化"] --> B["Pipeline模拟测试:Step调用与分支"]
B --> C["Jenkins Test Harness/临时Controller"]
C --> D["真实Agent、Registry、K8s集成测试"]
D --> E["少量金丝雀项目"]三十一、纯单元测试
对 DeployConfig.validate()、镜像引用解析和版本比较使用普通JUnit/Spock测试,不依赖Jenkins,速度最快。
重点覆盖:
- 正常值。
- 边界值。
- 空值。
- 路径穿越。
- Shell元字符。
- 不允许Registry。
- Digest长度。
三十二、Pipeline模拟测试
社区常用JenkinsPipelineUnit等工具模拟Step、绑定变量并断言调用。它不是Jenkins官方运行时的完整替代,无法证明插件、CPS、Sandbox和Agent行为完全一致。
适合验证:
- 是否调用sh。
- 参数是否正确。
- 失败分支。
- 凭据作用域是否被调用。
- Stage和通知逻辑。
三十三、Jenkins Test Harness与集成测试
Jenkins Test Harness可启动更接近真实Controller的测试环境,验证插件、Shared Library和Pipeline交互;维护成本更高。还应有临时Jenkins/Agent执行真实Maven、Docker和kubectl集成测试。
不要只做Mock后就直接推生产Trusted Library。
三十四、测试CPS与重启
共享库中包含input、sleep、sh、parallel和复杂对象时,在测试Controller执行安全重启和异常恢复,检查:
- Pipeline是否恢复。
- 是否NotSerializable。
- Step是否重复执行。
- Workspace/stash是否存在。
- post是否执行。
三十五、库发布流程
flowchart TD
A["PR与CODEOWNERS评审"] --> B["单元与Pipeline测试"]
B --> C["安全扫描与兼容测试"]
C --> D["创建不可变Tag并记录Commit"]
D --> E["少量非生产项目升级"]
E --> F["生产金丝雀项目"]
F --> G{"构建、发布指标是否正常"}
G -- "否" --> H["项目固定回旧版本并修复"]
G -- "是" --> I["分批升级项目"]三十六、如何批量升级几十个项目
不要一次改所有仓库。分组:
- 共享库自身测试项目。
- 非生产内部服务。
- 低风险生产服务。
- 核心服务少量金丝雀。
- 分批扩散。
观察Queue、构建成功率、时长、制品一致性、部署失败率和凭据异常。
三十七、默认版本升级风险
管理员把默认版本从v2改v3,所有未显式版本的Job可能在下次Build自动变化。应先扫描调用方、建立兼容矩阵、通知负责人并提供迁移工具。
更推荐项目显式版本,由自动化PR逐批升级。
三十八、回退共享库
项目显式固定:
@Library('company-lib@v2.3.1') _升级失败可改回v2.3.0。但回退库只能恢复后续流水线逻辑,不能撤销已执行的生产部署、DDL和消息。仍需按发布Runbook处理副作用。
三十九、删除旧API的条件
- 已统计没有项目调用。
- 所有受支持版本已迁移。
- 动态加载和隐式库已检查。
- 文档与示例更新。
- 经过Deprecated周期。
- 提供Major版本说明。
不能只在代码搜索不到时删除,因为Jenkins Job配置、分支和旧Tag中仍可能调用。
四十、可观测性
共享步骤应记录:
- Library名称、版本和Commit。
- API名称与非敏感参数。
- Stage耗时和结果。
- Agent Label。
- 制品Digest。
- 失败分类。
不要记录Secret、完整环境变量和医疗/个人数据。
四十一、商业场景一:main更新导致全公司构建失败
隐式Trusted Library跟随main,某提交把默认JDK从8改17;几十个JDK 8项目下一次Build编译失败。
治理:固定版本、兼容参数、金丝雀项目、语义版本、自动化PR升级和快速回退。
四十二、商业场景二:Trusted Library被植入凭据窃取
攻击者获得共享库仓库写权限,在通知方法中读取Controller/凭据并外传。由于库受信,Sandbox没有阻止。
治理:把Trusted仓库视为管理员代码,强制多方评审、签名Tag、最小权限、网络出站控制、审计和凭据轮换。
四十三、商业场景三:默认参数变更直接全量发布
库把 canaryPercent默认值从1改成100,项目未显式传参,升级后绕过灰度。
治理:安全默认值变化视为Major Breaking Change;生产参数强制显式填写并加上限,库内拒绝100%作为首个阶段。
四十四、商业场景四:共享库Shell注入
项目参数 dockerfile被构造为带Shell元字符,旧库用Groovy插值直接拼入 sh,导致Agent执行额外命令。
治理:路径白名单、禁止 ..和控制字符、单引号Groovy脚本、受控环境变量、不可信PR隔离和最小权限Agent。
四十五、故障排查
Library无法检出
检查SCM URL、Credentials、版本Ref、代理、TLS、Retriever类型和Controller日志。
找不到vars方法
检查文件名大小写、目录、版本、隐式/显式加载、编译错误和 @Library位置。
找不到src类
检查包路径、import、是否使用运行期library导致编译期类不可见、类编译错误。
RejectedAccessException
脚本/库处于Sandbox且调用未批准API。不要为了通过直接把整个库改Trusted,应评估API、Script Approval和更安全Step。
NotSerializableException
检查共享库字段、返回值和跨Step变量;减少复杂对象,不无脑加NonCPS。
升级后大量Job失败
冻结继续扩散,按库版本、API和项目类型聚类;显式回退金丝雀项目,保留失败日志并修复兼容性。
四十六、面试标准回答
vars、src和resources区别
vars中的Groovy脚本暴露为Pipeline全局变量,call方法可像函数调用;src按包结构进入共享库classpath,适合可测试类;resources保存模板和脚本,通过libraryResource读取。三者都跟随库版本,资源不能包含Secret。
@Library和library Step区别
@Library在Jenkinsfile编译/CPS转换前加载,因此可静态import src类;library Step在运行期动态加载,脚本已编译,通常不能再用普通import引用新类,只能按动态API使用。运行期版本参数必须白名单。
Trusted Library为什么危险
Trusted Library可绕过Groovy Sandbox并调用Jenkins内部能力,能提交代码的人可能接近管理员权限。仓库必须受保护、强制评审、固定签名版本、最小Trusted代码和审计;不可信PR不能控制其版本。
为什么共享库不能跟随main
main是可变引用,项目代码不变时库变化也会改变Pipeline行为,Build不可复现。生产应固定不可变Tag/Commit并记录实际SHA,通过金丝雀和自动PR分批升级。
共享库怎样避免Shell注入
对仓库、Tag和路径做严格白名单,禁止Shell元字符和路径穿越;避免Groovy双引号直接拼参数,使用单引号脚本与受控环境变量;不可信PR在隔离Agent运行,生产凭据和Docker Socket不暴露。
Shared Library怎么测试和发布
纯逻辑做JUnit/Spock,Pipeline分支用模拟框架,CPS/插件用Test Harness或临时Jenkins,再由少量项目金丝雀。发布不可变语义版本和Changelog,项目显式固定版本,通过自动PR分批升级,失败固定回旧版本并处理已发生副作用。
四十七、学习验收
- 创建包含vars/src/resources和帮助文档的库。
- 分别用@Library、隐式加载和library Step验证加载时机。
- 写一个Serializable类调用steps并通过重启实验。
- 制造状态型vars串扰并改为无状态参数。
- 比较Trusted、Untrusted和Folder Library权限。
- 完成repository/tag/dockerfile注入测试。
- 为API新增参数并保持旧项目兼容。
- 使用纯单测、Pipeline模拟和临时Jenkins三层测试。
- 发布v2.3.1不可变Tag并记录Commit SHA。
- 用金丝雀项目升级,再分批迁移十个项目。
- 演练库版本回退,并说明外部部署副作用不会撤销。
- 完成Trusted Library仓库威胁模型和凭据隔离。
