服务器、路径、容量、仓库及凭据均已脱敏。本文描述的是单控制器迁移,不等同于 Jenkins 高可用方案。

把 Jenkins 放进 Kubernetes,难点通常不是把 WAR 跑起来,而是保住控制器状态:任务配置、凭据、插件、构建记录、队列以及 Agent 身份都在 JENKINS_HOME 内。它应当被当作有状态服务,而不是可随时替换的 Deployment。

迁移前先冻结“写入者”

开始复制前,要暂停新的构建、镜像推送和配置修改,并等待正在运行的流水线结束。否则复制到一半的构建目录、队列状态或插件更新会让新旧两端不一致。

记录旧端的事实,而不是只记“目录约几十 GB”:

du -sh /data/jenkins
find /data/jenkins -type f | wc -l
tar -C /data -cf - jenkins | sha256sum

目录大小、文件数和归档校验值共同构成迁移基线。rsync 不可用时,可用 tar 流复制;关键是目标端再做一遍校验,而不是执着于某一个工具。

下面是一组迁移前的最小冻结和取证命令。先在 Jenkins 页面暂停新任务并等待运行中的构建结束,再执行缩容;不要把“缩容成功”当成“所有写入已经停止”。

export NS=<jenkins-namespace>
export STS=<jenkins-statefulset>
export SOURCE_DIR=/data/jenkins
export TARGET_NODE=<new-storage-node>
export TARGET_DIR=/data/jenkins

kubectl -n "$NS" get pod -l app.kubernetes.io/name=jenkins -o wide
kubectl -n "$NS" scale statefulset/$STS --replicas=0
kubectl -n "$NS" wait --for=delete pod/jenkins-0 --timeout=180s

# 在旧目录生成迁移基线;清单可用于目标端逐文件比较。
du -sh "$SOURCE_DIR"
find "$SOURCE_DIR" -xdev -type f -printf '%s %p\n' | sort > /tmp/jenkins-files.txt
tar -C "$(dirname "$SOURCE_DIR")" -cf - "$(basename "$SOURCE_DIR")" | shasum -a 256

目标节点目录必须先由运维人员创建并限制权限。复制完成后,应同时核对容量、文件数和归档哈希:

rsync -aHAX --numeric-ids --info=progress2 "$SOURCE_DIR/" \
  "<operator>@$TARGET_NODE:$TARGET_DIR/"

ssh "<operator>@$TARGET_NODE" "du -sh '$TARGET_DIR'"
ssh "<operator>@$TARGET_NODE" \
  "find '$TARGET_DIR' -xdev -type f -printf '%s %p\\n' | sort > /tmp/jenkins-files.txt"
diff -u /tmp/jenkins-files.txt <(ssh "<operator>@$TARGET_NODE" 'cat /tmp/jenkins-files.txt')

示例中的 SSH 用户和目录必须替换。rsync 成功不代表 Jenkins 数据一定完整,所以仍要保留旧目录,直到真实构建验收完成。

为什么使用 StatefulSet

单实例 Jenkins 需要稳定名称和稳定存储绑定。StatefulSet 提供的正是这两个属性:控制器名称固定为 jenkins-0,PVC 与它绑定,滚动时不会像无状态副本那样随机获得新身份。

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: jenkins
spec:
  serviceName: jenkins
  replicas: 1
  selector:
    matchLabels: { app: jenkins }
  template:
    spec:
      nodeSelector:
        workload-pool: jenkins

StatefulSet 不会自动让本地磁盘变成高可用存储。使用 local PV 时,PV 必须明确绑定目标节点:

nodeAffinity:
  required:
    nodeSelectorTerms:
      - matchExpressions:
          - key: kubernetes.io/hostname
            operator: In
            values: [node-ci-01]

PVC 不能“改个 PV 名字”

已绑定 PVC 的 volumeName 是不可变字段。正确的切换顺序是:

  1. 将 StatefulSet 缩容到零,确认旧控制器停止写入;
  2. 校验目标节点的数据副本;
  3. 创建指向新节点目录的新 PV;
  4. 在确认无控制器挂载时,删除旧 PVC 对象;
  5. 使用相同 PVC 名称创建新的绑定;
  6. 恢复 StatefulSet 并检查 jenkins-0。

这一步必须有回退点:旧目录不要立即删除,旧 PV 也不要在首次登录成功后立刻回收。

下面的命令把切换拆成可观察的状态变化。kubectl delete pvc 会解除数据绑定,必须先确认控制器已经缩容到零、目标 PV 已创建、旧目录仍可回退;不要把 --force 用在这一步。

# 导出旧对象,作为回退材料;导出文件不得包含 Secret 明文。
kubectl -n "$NS" get pvc jenkins-home -o yaml > /tmp/jenkins-home-pvc.before.yaml
kubectl get pv <old-jenkins-pv> -o yaml > /tmp/jenkins-pv.before.yaml

# 创建已核对 nodeAffinity 与 hostPath 的新 PV,再确认它仍是 Available。
kubectl apply -f jenkins-home-new-pv.yaml
kubectl get pv <new-jenkins-pv>

# 原 PVC 与 local PV 的绑定不可原地修改;仅在 controller 已停止后删除 PVC 对象。
kubectl -n "$NS" delete pvc jenkins-home
kubectl apply -f jenkins-home-pvc.yaml
kubectl -n "$NS" get pvc jenkins-home -w

# Bound 后恢复控制器,并等待 StatefulSet 的稳定身份 jenkins-0 出现。
kubectl -n "$NS" scale statefulset/$STS --replicas=1
kubectl -n "$NS" rollout status statefulset/$STS --timeout=10m
kubectl -n "$NS" get pod jenkins-0 -o wide

Retain 回收策略可以让删除 PVC 时保留底层目录,适合迁移窗口;但它不是备份。完成首次真实构建前,应保留旧 PV 清单和源目录,只清理临时副本。

Agent 重连比控制器启动晚一步

控制器迁移后,inbound Agent 的 JNLP Secret 可能已经变化。常见症状是 Pod 在跑,但 Jenkins 页面显示离线;此时不要反复重启控制器。

按顺序检查:控制器 Service、Agent Secret、WebSocket/ JNLP 入口、Agent 日志和工具链版本。确认新 Secret 后重建 Agent Pod,让它重新读取 Secret,通常比在旧容器中手改连接参数更安全。

最终验收不应只看首页可打开,还要完成一次真实构建:拉取代码、下载依赖、构建镜像或产物、上传制品,并确认后续任务可被 Agent 接走。

控制器与 Agent 的验收、回退

控制器就绪后,先检查存储挂载和最近日志,再检查每个 Agent 是否真的连接,而不是只看 Pod Running。

kubectl -n "$NS" get pod,pvc -o wide
kubectl -n "$NS" logs jenkins-0 --tail=200
kubectl -n "$NS" exec jenkins-0 -- sh -c 'test -d "$JENKINS_HOME" && df -h "$JENKINS_HOME"'

# 使用实际 Agent 标签替换;日志中的 Connected 才说明入站连接成功。
kubectl -n "$NS" get pod -l app.kubernetes.io/component=agent -o wide
kubectl -n "$NS" logs <agent-pod> --tail=100 | grep -E 'Connected|WebSocket'

若控制器无法读取历史任务、PVC 未按预期绑定或真实构建失败,立即缩容新 StatefulSet,保留日志和新目录;恢复旧 PV/PVC 清单、重新绑定旧目录后再启动旧控制器。不要让新旧控制器同时挂载同一个 JENKINS_HOME,这是最容易造成不可逆损坏的情况。