Jenkins 备份与恢复(K8s 部署 + NFS 存储)
记录时间:2026-09-14
环境:K8s devops 命名空间 / helm chart jenkinsci/jenkins 5.9.39 / Jenkins 2.568.1-lts-jdk21 / storageClass nfs-data-57(host57 虚拟机自建 NFS)
一、背景
Jenkins 用 helm 部署在 K8s(helm install jenkins ./jenkins-5.9.39.tgz -n devops -f values.yaml),JENKINS_HOME 挂在 jenkins-data-pvc(100Gi,RWX),agent 的 workspace 也复用这个 PVC。这套环境吃过两次亏:一次是插件升级冲突起不来;一次是核心跨大版本升级(2.516.2 → 2.568.1)后 Kubernetes 等插件大面积加载失败,Kubernetes cloud 配置一片空白,agent 拉不起来,所有构建瘫瘓。
我参考了一篇插件备份的文章(打包 plugins 目录、每天留 7 份、故障时回滚),按它的思路起步,推演后发现只备 plugins 不够:PVC 整个损坏时,光有插件重建不出这套环境,凭据、密钥、作业定义都在 JENKINS_HOME 里。所以最终方案是两层:插件层管插件冲突时分钟级回滚,配置层管整个环境重建。
| 层 | 内容 | 频率 / 保留 | 解决的问题 | 恢复耗时 |
|---|---|---|---|---|
| 插件层 | $JENKINS_HOME/plugins 整个目录 |
每天 1 次 / 保留 7 份 | 插件升级冲突起不来(最高频故障) | 分钟级 |
| 配置层 | JENKINS_HOME 全量(排除可重建大块) | 每天 1 次 / 保留 30 天 | PVC 损坏、误删、迁移重建 | 十几分钟 |
配套产出三个文件(都在本目录):jenkins-backup.sh(备份)、jenkins-restore-plugins.sh(插件回滚)、本 README(方案与恢复手册)。
二、方案设计
2.1 只备份 jenkins-data-pvc 一个 PVC
部署一共三个 PVC,我逐个判断过:
| PVC | 容量 | 结论 | 理由 |
|---|---|---|---|
| jenkins-data-pvc | 100Gi | 备份 | JENKINS_HOME 全部状态 |
| jenkins-maven-pvc | 100Gi | 不备 | Maven 本地仓库,可从远端仓库重新拉取,绝大多数是公共依赖 |
| jenkins-node-pvc | 1Gi | 不备 | node 缓存,同上 |
2.2 workspace 不备份
workspace/ 是每次构建时从 Git 检出的临时工作区,重新构建即重建,纳入备份只会让体积和恢复时间失控(当前有 142 个 workspace 目录)。构建产物该由 Harbor/制品库存档,不靠 workspace 留存。
注意:如果某个 Job 在 workspace 里放了不可重建的内容(手工拷的文件、外部下载的依赖),那是用法问题,应该把内容移到
userContent/或上传制品库,而不是靠备份 workspace 兜底。
2.3 备什么、不备什么
我用排除法打包整个 JENKINS_HOME,而不是逐个列文件。插件会不断新增自己的全局配置 xml,白名单必然漏。
纳入备份(保留全量)
| 内容 | 说明 |
|---|---|
credentials.xml + secrets/ + identity.key.enc + secret.key |
凭据与加密密钥,恢复后凭据可解密的完整前提 |
config.xml |
主配置(/manage/configure 页面的内容就落在这里) |
jobs/ |
作业定义(各 job 的 config.xml) |
users/、nodes/ |
用户与节点定义 |
scriptApproval.xml |
脚本审批白名单,漏备恢复后要逐条重新审批 |
casc_configs/、init.groovy.d/、userContent/ |
配置即代码挂载、初始化脚本、静态内容 |
各插件全局配置 *.xml |
其中 GlobalConfigFiles(全局配置文件)、GlobalLibraries(共享库)、GitLab 连接配置尤其重要 |
plugins/ |
含 *.jpi、解压目录、*.pinned、*.disabled |
排除(可重建,体积大头)
workspace/、caches/、logs/、updates/、fingerprints/、remoting/、*.log、simpleDiskUsageCheck、queue.xml.bak
注意:
secrets/+secret.key+identity.key.enc+credentials.xml是一套,缺任何一个,恢复后所有凭据都无法解密。这是这类备份最常见的失败点。注意:
*.pinned必须一起备份。Jenkins 靠它锁定插件版本以对抗自动升级,只备*.jpi的话,恢复后插件又被自动升级回冲突版本。本环境首包实测(2026-09-14):plugins 目录内 .pinned 为 0,插件均经 Update Center 在线安装、本就不生成 pinned;脚本打包整个 plugins 目录,有无 pinned 天然覆盖,无需特殊处理。
2.4 在 NFS 服务端本地执行,备份目标必须异机
备份脚本在 NFS 服务端 host57 上本地跑,不从 K8s Pod 里通过网络读写 NFS。理由:JENKINS_HOME 就在本机 /nfs_data 下,本地 tar 绕开 NFS 协议与海量小文件的元数据开销,这台 NFS 的 fsync 抖动之前诊断过(2026-09-04),走网络只会更慢。
备份目标不能写回 /nfs_data:host57 是一台虚拟机自建 NFS,同盘存储防不住磁盘或主机故障。脚本里做了硬性拦截,BACKUP_ROOT 指向 /nfs_data 下会直接报错退出。
三、部署备份脚本
3.1 安装(在 host57 执行)
install -m 0755 jenkins-backup.sh jenkins-restore-plugins.sh /usr/local/bin/
# 备份目录为 /k8s_nas/jenkins_backup(NAS 挂载,异机存储),确认 NAS 已挂载即可
mkdir -p /k8s_nas/jenkins_backup
# 试跑一次
/usr/local/bin/jenkins-backup.sh
3.2 定时任务
crontab -e
# 凌晨低峰执行
30 2 * * * /usr/local/bin/jenkins-backup.sh >> /var/log/jenkins-backup.log 2>&1
脚本在 BACKUP_ROOT 下生成 plugins/ 与 full/ 两个目录,每个压缩包配一个同名 .meta,记录打包时的核心版本与插件 jpi 数量。恢复前先看这个文件,插件与核心版本强绑定。
3.3 备份脚本全文(jenkins-backup.sh)
首包实测(2026-09-14):14:45 启动、14:48 完成,全程 2.5 分钟;plugins 层 362MB、配置层 1.3GB(含构建历史)。以下内容与 /usr/local/bin/jenkins-backup.sh 当前线上版本一致。
#!/usr/bin/env bash
# ============================================================================
# Jenkins 分层备份脚本 —— 在 NFS 服务端(host57)本地执行
#
# 为什么在 NFS 服务端跑:JENKINS_HOME 就在本机 /nfs_data 下,本地 tar 打包绕开
# NFS 协议与海量小文件的元数据开销(该 NFS 已知有 fsync 抖动);从 K8s Pod 里
# 通过 NFS 读写来备份会慢一个数量级。
#
# 分两层备份:
# ① plugins 层:每天打包、保留 7 份 —— 插件冲突时分钟级回滚(最高频事故)
# ② 全量配置层:每天打包、保留 30 天 —— 灾难恢复(凭据/密钥/jobs/全局配置)
# 两层互补:插件层管"回得去",配置层管"重建得起来"。
#
# 安装(host57):
# install -m 0755 jenkins-backup.sh /usr/local/bin/
# crontab -e → 30 2 * * * /usr/local/bin/jenkins-backup.sh >> /var/log/jenkins-backup.log 2>&1
# ============================================================================
set -euo pipefail
# ---------------- 配置区(按实际环境调整)----------------
# PVC 目录名带 PVC uid(/nfs_data/devops-jenkins-data-pvc-pvc-<uid>),
# PVC 删除重建后 uid 会变,故用 glob 自动定位;必要时 export JH_OVERRIDE=... 覆盖。
JENKINS_HOME="${JH_OVERRIDE:-$(ls -d /nfs_data/devops-jenkins-data-pvc-pvc-* 2>/dev/null | head -1)}"
# 备份根目录:必须是异机挂载点(远端 NFS/CIFS/备份服务器)或另一块物理盘。
# 绝不可写回 /nfs_data —— 与源数据同一块盘/同一台虚机,磁盘或主机故障时一起丢。
# 实际使用 /k8s_nas(NAS 挂载,异机存储),满足要求;脚本仍保留 /nfs_data 硬性拦截兜底。
BACKUP_ROOT="${BACKUP_ROOT:-/k8s_nas/jenkins_backup}"
KEEP_PLUGINS=7 # 插件备份保留份数
KEEP_FULL_DAYS=30 # 全量配置备份保留天数
# --------------------------------------------------------
if [ -z "${JENKINS_HOME:-}" ] || [ ! -d "${JENKINS_HOME:-}" ]; then
echo "ERROR: 未找到 JENKINS_HOME(已尝试 glob: /nfs_data/devops-jenkins-data-pvc-pvc-*)"
exit 1
fi
case "$BACKUP_ROOT" in
/nfs_data/*) echo "ERROR: BACKUP_ROOT 不能位于 /nfs_data(与源数据同盘,不防磁盘/主机故障)"; exit 1 ;;
esac
# 防挂载点失效(读 /proc/mounts 判断,刻意不 stat 挂载点:NFS 假死时 stat 会无限阻塞卡死脚本)。
# 挂载记录不存在时必须拒绝执行:mkdir -p 会在本机根盘静默建目录,备份悄悄落到本机。
MNT_DIR=$(dirname "$BACKUP_ROOT")
if ! grep -qs " ${MNT_DIR} nfs" /proc/mounts && ! grep -qs " ${BACKUP_ROOT} nfs" /proc/mounts; then
echo "ERROR: $MNT_DIR 没有 NFS 挂载记录——NAS 未挂载时备份会写到本机盘,请先挂载" >&2
exit 1
fi
# 运行锁(锁文件放本机 /var/lock,不能放 NAS:NAS 假死时连拿锁都会卡)。
# 上一次备份未结束(典型如 NAS 假死把 tar 挂起)时本次直接跳过,防 cron 堆积挂起进程。
exec 9>/var/lock/jenkins-backup.lock
flock -n 9 || { echo "上一次备份仍在运行(可能 NAS 异常挂起),本次跳过。检查:ps -ef | grep jenkins-backup"; exit 1; }
STAMP=$(date +%Y%m%d_%H%M%S)
DAY=$(date +%F_%H%M)
# 记录核心版本:插件与 Jenkins 核心版本强绑定,恢复前必须核对
CORE=$(cat "$JENKINS_HOME/jenkins.install.InstallUtil.lastExecVersion" 2>/dev/null | tr -d '\n' || echo unknown)
mkdir -p "$BACKUP_ROOT/plugins" "$BACKUP_ROOT/full"
# ① 插件层:打包整个 plugins 目录
# 含 *.jpi(插件包)、同名解压目录、*.pinned(版本固定标记——关闭自动升级后
# 靠它锁定版本,漏备会导致恢复后又被自动升级)、*.disabled。
tar --numeric-owner -czf "$BACKUP_ROOT/plugins/plugins_${STAMP}.tar.gz" \
-C "$JENKINS_HOME" plugins
printf 'core=%s\nstamp=%s\njpi_count=%s\n' \
"$CORE" "$STAMP" "$(ls -1 "$JENKINS_HOME/plugins" 2>/dev/null | grep -c '\.jpi$' || true)" \
> "$BACKUP_ROOT/plugins/plugins_${STAMP}.meta"
# ② 全量配置层:打包 JENKINS_HOME,排除可重建的大块
# workspace/ 由构建从 Git 重建;caches/logs/updates/fingerprints/remoting 同类,
# 均无恢复价值(workspace 是体积大头,纳入会使备份窗口与恢复时间失控)。
tar --numeric-owner -czf "$BACKUP_ROOT/full/jenkins-home_${DAY}.tar.gz" \
-C "$JENKINS_HOME" \
--exclude='./workspace' \
--exclude='./caches' \
--exclude='./logs' \
--exclude='./updates' \
--exclude='./fingerprints' \
--exclude='./remoting' \
--exclude='./*.log' \
--exclude='./simpleDiskUsageCheck' \
--exclude='./queue.xml.bak' \
.
printf 'core=%s\nstamp=%s\n' "$CORE" "$DAY" > "$BACKUP_ROOT/full/jenkins-home_${DAY}.meta"
# ③ 保留策略:插件按份数、全量按天数
ls -1t "$BACKUP_ROOT/plugins"/plugins_*.tar.gz 2>/dev/null | tail -n +$((KEEP_PLUGINS+1)) | xargs -r rm -f
ls -1t "$BACKUP_ROOT/plugins"/plugins_*.meta 2>/dev/null | tail -n +$((KEEP_PLUGINS+1)) | xargs -r rm -f
find "$BACKUP_ROOT/full" -name 'jenkins-home_*.tar.gz' -mtime +"$KEEP_FULL_DAYS" -delete
find "$BACKUP_ROOT/full" -name 'jenkins-home_*.meta' -mtime +"$KEEP_FULL_DAYS" -delete
echo "[$(date '+%F %T')] backup ok: core=$CORE plugins=$BACKUP_ROOT/plugins/plugins_${STAMP}.tar.gz"
四、恢复流程
4.1 场景 A:插件冲突导致起不来(最常见)
# 1) 停 controller(先用 kubectl -n devops get deploy 确认 Deployment 名)
kubectl -n devops scale deploy <jenkins-deploy> --replicas=0
# 2) 回滚插件目录(不带参数会列出最近 7 份备份)
/usr/local/bin/jenkins-restore-plugins.sh /k8s_nas/jenkins_backup/plugins/plugins_20260914_023000.tar.gz
# 3) 起服务并验证
kubectl -n devops scale deploy <jenkins-deploy> --replicas=1
kubectl -n devops logs -f deploy/<jenkins-deploy> # 确认插件全部加载成功
回滚脚本会把故障现场保留在 $JENKINS_HOME/plugins.broken.<时间戳>。恢复后我用 diff -rq plugins.broken.<ts> plugins 对比,就能定位是哪个插件引发的冲突,确认无用再删。
4.2 场景 B:配置被误改、误删单个文件
从配置层备份包里单独解出目标文件,不需要整体恢复:
tar -xzf /k8s_nas/jenkins_backup/full/jenkins-home_2026-09-14_0230.tar.gz -C /tmp ./config.xml
# 对比确认后再覆盖回 $JENKINS_HOME
4.3 场景 C:PVC 损坏、整套重建
# 1) 停 Jenkins
kubectl -n devops scale deploy <jenkins-deploy> --replicas=0
# 2) 解包回 PVC 目录(host57 本地解,或起临时 Pod 挂 jenkins-data-pvc 解)
tar --numeric-owner -xzf /k8s_nas/jenkins_backup/full/jenkins-home_2026-09-14_0230.tar.gz -C "$JENKINS_HOME"
# 3) 起 Jenkins,验证三件事:能登录、凭据可正常使用、Job 列表与插件版本正常
kubectl -n devops scale deploy <jenkins-deploy> --replicas=1
4.4 插件回滚脚本全文(jenkins-restore-plugins.sh)
#!/usr/bin/env bash
# ============================================================================
# Jenkins 插件目录回滚 —— 在 NFS 服务端(host57)执行
#
# 适用场景:插件自动升级/版本冲突导致 Jenkins 起不来(最高频故障)。
# 核心动作三步:停服务 → 解压备份 → 起服务,分钟级恢复。
#
# 前置(必须):先停 Jenkins controller,否则回滚期间文件被占用、且会立即被回写。
# kubectl -n devops get deploy # 确认 Deployment 名
# kubectl -n devops scale deploy <jenkins-deploy> --replicas=0
#
# 用法:
# jenkins-restore-plugins.sh <plugins_YYYYmmdd_HHMMSS.tar.gz>
# (不带参数则列出可用的 7 份备份供选择)
# ============================================================================
set -euo pipefail
BACKUP_ROOT="${BACKUP_ROOT:-/k8s_nas/jenkins_backup}"
BK="${1:-}"
if [ -z "$BK" ]; then
echo "用法: $0 <plugins_YYYYmmdd_HHMMSS.tar.gz>"
echo "可用备份(最新在前):"
ls -1t "$BACKUP_ROOT/plugins"/plugins_*.tar.gz 2>/dev/null | head -7 || echo " (无备份,请检查 BACKUP_ROOT)"
exit 1
fi
[ -f "$BK" ] || { echo "ERROR: 备份包不存在: $BK"; exit 1; }
JENKINS_HOME="${JH_OVERRIDE:-$(ls -d /nfs_data/devops-jenkins-data-pvc-pvc-* 2>/dev/null | head -1)}"
if [ -z "${JENKINS_HOME:-}" ] || [ ! -d "${JENKINS_HOME:-}" ]; then
echo "ERROR: 未找到 JENKINS_HOME(已尝试 glob: /nfs_data/devops-jenkins-data-pvc-pvc-*)"
exit 1
fi
# 恢复前核对核心版本:插件与核心版本强绑定,核心升过级时旧插件可能反而不兼容
META="${BK%.tar.gz}.meta"
if [ -f "$META" ]; then
echo "备份元数据: $(tr '\n' ' ' < "$META")"
echo "当前核心版本: $(cat "$JENKINS_HOME/jenkins.install.InstallUtil.lastExecVersion" 2>/dev/null || echo unknown)"
echo "→ 若两者差异较大,先确认兼容性(Ctrl-C 可中止)"; sleep 5
fi
STAMP=$(date +%Y%m%d_%H%M%S)
# 保留故障现场:事后可用 diff -rq 定位是哪个插件引发的冲突
mv "$JENKINS_HOME/plugins" "$JENKINS_HOME/plugins.broken.$STAMP"
mkdir -p "$JENKINS_HOME/plugins"
# --numeric-owner 保持原 uid/gid:NFS 上属主错乱会导致 Jenkins 无权限读取而启动失败
tar --numeric-owner -xzf "$BK" -C "$JENKINS_HOME"
echo " 插件目录已回滚。"
echo " 故障现场: $JENKINS_HOME/plugins.broken.$STAMP"
echo " (恢复后可用 diff -rq 对比定位冲突插件,确认无用再删除)"
echo " 下一步: kubectl -n devops scale deploy <jenkins-deploy> --replicas=1"
echo " 然后查看 controller 启动日志确认插件全部加载成功"
五、配置的事实源与升级防护
5.1 UI 页面的配置到底存在哪
做备份清单时我把每个 UI 页面的落盘位置查了一遍,结论是分三层,备份覆盖情况完全不同:
| 配置 | UI 入口 | 持久化位置 | 备份覆盖 |
|---|---|---|---|
| 系统全局配置 | /manage/configure |
$JENKINS_HOME/config.xml |
覆盖(配置层) |
| 各插件全局配置 | /manage/... |
$JENKINS_HOME/*.xml |
覆盖(配置层) |
| Cloud 定义(UI 手工创建时) | /manage/cloud/ |
$JENKINS_HOME/jenkins.model.Jenkins.clouds/*.xml |
覆盖(配置层) |
| Kubernetes cloud + agent 模板(本环境实况) | /manage/cloud/kubernetes/ |
helm ConfigMap,不在 JENKINS_HOME | 不覆盖,事实源在 values |
第四行是我查出来的盲区:PVC 里没有 jenkins.model.Jenkins.clouds/ 目录,只有 casc_configs/(ConfigMap 的运行时挂载)。读了 devops/jenkins-jenkins-jcasc-config 这个 ConfigMap(data key jcasc-default-config.yaml)实锤:Kubernetes cloud 的全部配置(serverUrl、jenkinsUrl、tunnel、默认 jnlp agent 模板、workspaceVolume)都由 chart 的 JCasC 渲染(controller.JCasC.defaultConfig: true),根本不落 JENKINS_HOME。
这也解释了那次升级后”Kubernetes 插件配置要重新配一遍”:helm upgrade 重新渲染 ConfigMap,JCasC 重新应用时把 UI 上的手工修改重置掉。这类配置丢失的根因是配置事实源不在 UI,备份救不了。
5.2 把 Kubernetes cloud 定制固化到 values
正确解法是把定制写进 helm-jenkins/values.yaml,让配置随 git 管理、helm upgrade 自动生效:
controller:
JCasC:
configScripts:
kubernetes-cloud: |-
jenkins:
clouds:
- kubernetes:
name: kubernetes
serverUrl: "https://kubernetes.default"
# 把 UI 上定制过的字段搬进来
摘取当前生效配置:Manage Jenkins → Configuration as Code → View Configuration(或访问 https://jenkins.xxx.com/configuration-as-code/reference),把与 chart 默认不一致的段搬进 configScripts。
注意:
casc_configs/目录虽然在 JENKINS_HOME 内、会被配置层备份打包,但它是 ConfigMap 的运行时快照,恢复后真正生效的仍是 helm release 的 ConfigMap。
5.3 升级 SOP
上次事故的机理:核心跨大版本 → kubernetes 等插件加载失败 → JCasC 应用 clouds 段失败 → cloud 配置空白。插件没加载起来时 JCasC 对应段必然失败,所以升级验证必须盯住 cloud 在位。
升级前:
- 手动跑一次备份:
/usr/local/bin/jenkins-backup.sh - 导出当前生效 JCasC 配置(Configuration as Code → Download Configuration),存入
helm-jenkins/,命名jcasc-baseline-<日期>.yaml(当前基线为jcasc-baseline-20260914.yaml),与上一份基线 diff - 核心 LTS 只逐版本升、不跨大版本;对照插件管理页确认 kubernetes/pipeline/credentials 等核心插件的最低核心要求
- 记录插件清单:
ls -1 $JENKINS_HOME/plugins/*.jpi(备份包里已含)
升级后验证:
- ConfigMap 里 clouds 段完整:
kubectl -n devops get cm jenkins-jenkins-jcasc-config -o yaml | grep -A5 clouds - UI
/manage/cloud/确认 Kubernetes cloud 配置在位、字段不为空 - 试拉一个 agent:随便触发一个 Job,确认 agent Pod 起来并注册成功
- 启动日志无插件加载失败:
kubectl -n devops logs deploy/<jenkins-deploy> | grep -iE "failed to load|error" | head - Configuration as Code 管理页无应用报错
5.4 事故时快速恢复(cloud 空白、agent 起不来)
- 定位:ConfigMap 里 clouds 段是否完整(对照
jcasc-baseline-*.yaml) - ConfigMap 完整、Jenkins 内空白:强制 JCasC 重新应用。
kubectl -n devops edit cm jenkins-jenkins-jcasc-config随便改一个无害内容触发 config-reloader 热加载,或kubectl -n devops rollout restart deploy/<jenkins-deploy> - ConfigMap 缺失、残缺:
helm rollback jenkins <上一revision> -n devops(PVC 带helm.sh/resource-policy: keep注解,回滚不动数据) - 仍不行:把
jcasc-baseline-<日期>.yaml内容经 Manage Jenkins → Configuration as Code → Apply 手工应用 - 全部失败:走 4.3 场景 C 全量重建
六、遇到的问题与注意事项
- 备份写回同一块 NFS 盘没有意义 →
BACKUP_ROOT必须是异机挂载点或另一块物理盘,脚本已硬性拦截/nfs_data路径 - 凭据四件套拆着备份 → 恢复后凭据全部无法解密,见 2.3 的成套要求
- 打包解包不带
--numeric-owner→ NFS 上属主错乱,Jenkins 无权限读取直接起不来 - PVC 目录名带 uid(
/nfs_data/devops-jenkins-data-pvc-pvc-<uid>),PVC 重建后会变 → 脚本用 glob 自动定位,必要时JH_OVERRIDE显式指定 - 一致性:核心配置 xml 只在保存时写入、改动低频,tar 的秒级打包窗口内不一致风险可忽略;要求更严时备份前先
curl -X POST .../quietDown(只停接新任务,不影响运行中的构建),备完cancelQuietDown - 关闭插件自动升级:Manage Jenkins → Plugins → Advanced settings,取消自动升级勾选(设置持久化在 JENKINS_HOME 内,会随备份保住;恢复到干净环境后重新确认一次)。插件自动升级正是插件冲突事故的源头
- 没验证过的备份等于没有备份 → 每月演练一次恢复,test 集群可以当恢复靶场
- 长期方向:全局配置也纳入配置即代码(JCasC)后,真正不可再生的只剩凭据与密钥,备份会更轻、更可靠
七、参考资料
- jenkinsci/jenkins helm chart:https://github.com/jenkinsci/helm-charts (JCasC 与 persistence 配置说明)
- Jenkins 官方备份建议:https://www.jenkins.io/doc/book/system-administration/backing-up/