Jenkins 流水线¶
Jenkins 的老问题不是「不会写 Pipeline」,而是「构建环境不可复现」:一台机器上装三个 JDK、Maven 版本互冲突、并发一高就互相踩。把它搬到 Kubernetes 上,controller 只负责调度,每个构建都用一个全新的 agent Pod,环境由镜像定义。
你将学到¶
- Jenkins on Kubernetes 的 controller / agent 架构,JNLP 协议与 Kubernetes plugin 的作用
- 仓库
jenkins/deploy/里每个资源对象的职责,以及 RBAC 的过时写法怎么改 - 初始密码获取、插件安装、Kubernetes Cloud 与 Pod Template 的配置
- 用
kubernetesagent 跑 maven 构建,挂载pvc-maven-cache做依赖缓存 - 用 CoreDNS
rewrite让jenkins.magedu.com在集群内直接解析到 Service
为什么需要它¶
传统 Jenkins 是「宠物」:一台虚拟机、手工装插件、构建产物和缓存都堆在本地。它有三个绕不开的问题——环境漂移(构建机升级 JDK 之后老项目挂掉)、并发瓶颈(固定几个 executor,忙时排队闲时浪费)、污染(上一次构建的残留文件影响下一次)。Kubernetes 上的 Jenkins 把「构建环境」变成声明式的 Pod 模板:每次构建由插件创建一个 agent Pod,跑完就删;JDK、Maven、Docker 版本都写在镜像里;并发上限由集群资源而不是物理机决定。controller 自己仍然是有状态的(jenkins_home 要持久化),但它不再执行构建。
代价是复杂度转移到了「怎么让 controller 与 agent 互相找到对方」:agent 要能连上 controller 的 50000 端口,controller 要能通过 API Server 创建 Pod,而集群内的访问路径又和浏览器访问的域名不同。这三件事就是本章的实验重点。
核心概念¶
controller 与 agent¶
flowchart LR
U["浏览器"] -->|"Ingress jenkins.magedu.com"| S["Service jenkins:8080"]
S --> C["controller Pod<br/>jenkins/jenkins:jdk17"]
C -->|"Kubernetes API 创建 Pod"| API[kube-apiserver]
API --> A1["agent Pod<br/>jnlp + maven"]
API --> A1["agent Pod<br/>jnlp + maven"]
A1 -->|"JNLP 50000 回连"| C
图:controller 不跑构建,它通过 API Server 创建 agent Pod,agent 用 JNLP 反向连回 controller。
agent 的方向很容易搞反:是 agent 主动连 controller 的 50000 端口,不是 controller 连 agent。所以 jenkins-jnlp 这个 Service 必须存在且端口对得上,agent Pod 里的 jenkins/inbound-agent 容器才知道往哪注册。JNLP 端口用固定值(50000)比用随机端口省事得多,仓库的 Deployment 里显式声明了它。
Kubernetes plugin 负责三件事:把 Pod Template 渲染成 Pod、等待 agent 注册上线、构建结束后清理 Pod。它用 controller 的 ServiceAccount 调 API Server,所以 RBAC 必须允许 pods、pods/exec、pods/log 等资源。
deploy/ 里的资源¶
| 文件 | 对象 | 作用 |
|---|---|---|
02-pvc-jenkins.yaml |
PVC 10Gi / nfs-csi / RWX |
持久化 jenkins_home |
03-rbac-jenkins.yaml |
SA + ClusterRole + Binding | 允许 controller 管理 Pod |
04-deploy-jenkins.yaml |
Deployment | controller,含 fix-permissions initContainer |
05-service-jenkins.yaml |
Service ×2 | jenkins:8080(Web)与 jenkins-jnlp:50000 |
06-pvc-maven-cache.yaml |
PVC 10Gi / RWX | 多个 agent 共享的 ~/.m2 缓存 |
07-ingress-jenkins.yaml |
Ingress | cicd.magedu.com 与 jenkins.magedu.com |
deploy/ 里没有 ConfigMap——Jenkins 的配置全在 jenkins_home 里,这也是为什么 PVC 是唯一的数据落点。04-deploy-jenkins.yaml 里的 initContainer 值得单独说:NFS 卷的属主通常是 root,而官方镜像是 UID 1000,直接挂载会因权限不足启动失败。仓库用 alpine + chown -R 1000:1000 /var/jenkins_home/ 解决,代价是 initContainer 需要 privileged: true;如果 NFS 服务端配了 no_root_squash,也可以改用 securityContext.fsGroup: 1000 省掉特权。
RBAC 有一处过时写法:apiGroups: ["extensions", "apps"]。extensions/v1beta1 的 Deployment 早在 Kubernetes 1.16 就被移除,v1.31 上这个组名毫无意义,应只保留 apps。更进一步,controller 只需要在 jenkins 命名空间里创建 Pod,把 ClusterRole 换成命名空间级 Role 更符合最小权限。
初始密码与插件¶
首次启动会走 setup wizard,初始密码写在 jenkins_home/secrets/initialAdminPassword:
必装的插件是 Kubernetes(agent 调度)、Pipeline(声明式语法)、Git(拉代码)、Configuration as Code(把配置写成 YAML)。Maven 不需要装插件——构建在 maven 容器里跑,用的是镜像自带的 mvn,而不是 Jenkins 全局工具。
Kubernetes Cloud 与 Pod Template¶
在「Manage Jenkins → Clouds → New cloud → Kubernetes」里填四项关键配置:Kubernetes URL 用 https://kubernetes.default.svc(controller 在集群内,走 Service 名);Namespace 填 jenkins;Credentials 用 jenkins-master 这个 SA 的 token(Secret text 类型);Jenkins URL 填 http://jenkins.jenkins.svc.cluster.local:8080——这个是给 agent 回连用的,不能写 Ingress 域名。
Pod Template 定义「一个构建环境长什么样」:一个 jnlp 容器负责注册,一个 maven 容器负责构建,需要 Docker 时再加一个 dind 容器。模板存成 jenkins-slave、maven-3.8 这样的名字,Pipeline 里用 inheritFrom 引用,就能在多个流水线间复用。
声明式 Pipeline 的 kubernetes agent¶
agent { kubernetes { ... } } 有三种写法:inheritFrom 'maven-3.8' 引用 UI 里配好的模板、内联 yaml 直接给 Pod 定义、或 containerTemplate 逐个声明容器。仓库 README 的三个示例都用 inheritFrom,适合模板已沉淀在 Jenkins 里的团队;模板还没定型时,内联 yaml 更直观。真正执行构建的是 steps { container('maven') { sh '...' } }——只有 container() 里声明的步骤才会跑在那个容器里,写在 container 之外的 sh 跑在默认的 jnlp 容器里,那里没有 mvn。
缓存与并发¶
pvc-maven-cache 挂到 /root/.m2 后,第一次构建下载的依赖会被后续所有 agent 复用,构建时间能从几分钟降到几十秒。注意 RWX 是必须的——多个 agent 可能同时挂载同一个 PVC。
Deployment 的 JAVA_OPTS 里那串 NodeProvisioner 参数是在调「扩容灵敏度」:initialDelay=0 让 Jenkins 立刻开始扩容,MARGIN=50、MARGIN0=0.85 决定队列里堆积多少任务时提前拉新 agent。默认值偏保守,突发并发时任务会在队列里干等。上限则靠 Pod Template 的 instanceCap 和集群资源共同约束。
安全与备份¶
三件事必须做:给 Jenkins 建管理员账号并关掉匿名读;RBAC 收到最小权限(Role 而非 ClusterRole,只给 pods、pods/exec、pods/log);凭据走 Jenkins Credentials 或外部 Secret,不在 Pipeline 里明文写,配置用 JCasC 固化到 Git。jenkins_home 是 PVC,里面装着任务定义、插件和凭据加密密钥,备份它等价于备份整个 Jenkins——用 PVC 快照或用 Velero 做命名空间级备份。
动手实验¶
实验:部署 Jenkins 并跑一个 kubernetes agent 的 maven 构建
目标:用仓库 deploy/ 部署 Jenkins,取初始密码登录,配置 Kubernetes Cloud,跑通 kubernetes agent + maven 容器的 Pipeline,最后用 Ingress 访问。
前置:集群有 nfs-csi StorageClass 与 ingress-nginx;能解析 jenkins.magedu.com(或改成本机 hosts)。
步骤 1 · 应用 deploy/ 清单¶
pod/jenkins-7d9c8f6b4-2xk9p 1/1 Running 0 90s
NAME STATUS VOLUME CAPACITY STORAGECLASS
persistentvolumeclaim/jenkins-pvc Bound pvc-... 10Gi nfs-csi
NAME TYPE CLUSTER-IP PORT(S)
jenkins NodePort 10.96.12.31 8080:31234/TCP
jenkins-jnlp ClusterIP 10.96.44.18 50000/TCP
两件事要确认:jenkins-pvc 必须是 Bound(否则 controller 会卡在 Pending),jenkins-jnlp 的 50000 端口必须在(否则 agent 注册不上)。
步骤 2 · 取初始密码并登录¶
kubectl -n jenkins exec deploy/jenkins -- \
cat /var/jenkins_home/secrets/initialAdminPassword
kubectl -n jenkins port-forward svc/jenkins 8080:8080
浏览器打开 http://127.0.0.1:8080,粘贴密码,选「安装推荐的插件」,再建管理员账号。如果密码文件不存在,通常是 PVC 里已有旧的 config.xml——说明 Jenkins 已经初始化过,用当时的管理员账号登录即可。
步骤 3 · 配置 Kubernetes Cloud¶
装完 Kubernetes 插件后,在「Manage Jenkins → Clouds」新增一个 Kubernetes 云:
| 配置项 | 值 |
|---|---|
| Kubernetes URL | https://kubernetes.default.svc |
| Kubernetes Namespace | jenkins |
| Credentials | jenkins-master SA 的 token(Secret text) |
| Jenkins URL | http://jenkins.jenkins.svc.cluster.local:8080 |
| Pod Template 名称 | maven-3.8 |
Pod Template 里加两个容器:jnlp 用 jenkins/inbound-agent:latest-jdk17,maven 用 maven:3.9-eclipse-temurin-17,命令 sleep + infinity(否则容器跑完就退出)。Service Account 填 jenkins-master,让它有权限创建 Pod。
步骤 4 · 跑一个 maven 构建¶
新建 Pipeline 任务,把下面的脚本贴进去:
pipeline {
agent { kubernetes { inheritFrom 'maven-3.8' } }
stages {
stage('Build') {
steps { container('maven') { sh 'mvn -B -DskipTests clean package' } }
}
}
}
[Pipeline] sh
+ mvn -B -DskipTests clean package
[INFO] BUILD SUCCESS
[INFO] Total time: 12.3 s
Finished: SUCCESS
构建期间执行 kubectl -n jenkins get pod 会看到一个 maven-3.8-xxxxx 的 Pod,构建结束后它被自动删除——这就是「一次构建一个干净环境」的直接证据。
步骤 5 · 用 Ingress 访问,并让集群内也能解析¶
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: {name: jenkins, namespace: jenkins}
spec:
ingressClassName: nginx
rules:
- host: jenkins.magedu.com
http: {paths: [{path: /, pathType: Prefix, backend: {service: {name: jenkins, port: {number: 8080}}}}]}
kubectl apply -f 07-ingress-jenkins.yaml
kubectl -n jenkins get ingress jenkins
curl -I http://jenkins.magedu.com
仓库的清单里还多配了 cicd.magedu.com,与 jenkins.magedu.com 指向同一个后端;ingressClassName: nginx 与 pathType: Prefix 是让它生效的两个关键字段。浏览器能访问不代表集群内能访问:agent 或其它 Pod 里 curl jenkins.magedu.com 往往会解析到 Ingress 的外部地址,在裸金属或 NAT 环境下会回环失败。
仓库 README 给的做法是改 CoreDNS 的 rewrite,把 jenkins*.magedu.com 在集群内直接改写成 Service 的 FQDN:
# 片段,放进 Corefile 的 .:53 { } 块内,其余指令保持默认
rewrite stop {
name regex (jenkins.*)\.magedu\.com {1}.jenkins.svc.cluster.local
answer (jenkins.*)\.jenkins\.svc\.cluster\.local {1}.magedu.com
}
name regex 把查询名改写成 jenkins.jenkins.svc.cluster.local,answer 再把响应里的名字改回原域名——否则应用看到的主机名和请求的不一致,TLS 与 Cookie 都可能出问题。rewrite stop 表示改写后不再继续匹配后续规则。
kubectl -n kube-system rollout restart deploy/coredns
kubectl -n jenkins run t --rm -it --image=curlimages/curl --restart=Never -- \
curl -s -o /dev/null -w '%{http_code}\n' http://jenkins.magedu.com
验证清单¶
-
kubectl -n jenkins get pod中 controller 是1/1 Running,jenkins-pvc是Bound - 跑 Pipeline 时能看到临时 agent Pod,结束后自动消失
- 集群内
curl http://jenkins.magedu.com返回 200
常见问题与排错¶
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
controller CrashLoopBackOff,日志 Permission denied |
NFS 卷属主是 root,镜像 UID 1000 写不了 | 确认 initContainer 生效;NFS 配 no_root_squash,或用 securityContext.fsGroup: 1000 |
insufficient permissions 创建 Pod 失败 |
ServiceAccount 或 RBAC 缺失 | kubectl auth can-i create pods -n jenkins --as=system:serviceaccount:jenkins:jenkins-master |
Pipeline 卡在 Waiting for next available executor |
Cloud 未配置、Pod Template 名字不匹配 | 检查 inheritFrom 的名字与 UI 里模板名是否一致 |
agent 注册超时,controller 日志 Connection refused |
Jenkins URL 填了 Ingress 域名,或 50000 未暴露 | 改成 http://jenkins.jenkins.svc.cluster.local:8080;确认 jenkins-jnlp Service |
| Ingress 502 | targetPort 与容器端口不符,或 Jenkins 未就绪 |
kubectl -n jenkins get endpointslice 看后端是否为空 |
集群内解析 jenkins.magedu.com 超时 |
CoreDNS rewrite 未生效或没重启 | kubectl -n kube-system rollout restart deploy/coredns,用 nslookup 验证 |
进阶挑战¶
- 把 Cloud、Pod Template、管理员账号写成 JCasC 的
jenkins.yaml,用 ConfigMap 挂进容器,实现「删了 controller 也能重建出一模一样的 Jenkins」。 - 给 Pod Template 加一个
dind容器(docker:27-dind),在 Pipeline 里container('docker')构建镜像并推到 Harbor。
小结¶
- controller 有状态、agent 无状态:
jenkins_home上 PVC,构建环境由 Pod 模板定义。 - agent 是主动连 controller 的 50000 端口,所以 Jenkins URL 必须写 Service 的集群内地址。
deploy/里extensions那个 apiGroup 是 1.16 之前的历史遗留,v1.31 上应删掉;RBAC 建议从ClusterRole收到Role。