4.4. Jenkins 流水线
Jenkins 的老问题不是「不会写 Pipeline」,而是「构建环境不可复现」:一台机器上装三个 JDK、Maven 版本互冲突、并发一高就互相踩。把它搬到 Kubernetes 上,controller 只负责调度,每个构建都用一个全新的 agent Pod,环境由镜像定义。
- 层级:
L4 · 实战
- 难度:
进阶
- 预计:
60 分钟
- 前置:
Deployment、Service、Ingress、PVC
4.4.1. 你将学到
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
4.4.2. 为什么需要它
传统 Jenkins 是「宠物」:一台虚拟机、手工装插件、构建产物和缓存都堆在本地。它有三个绕不开的问题——环境漂移(构建机升级 JDK 之后老项目挂掉)、并发瓶颈(固定几个 executor,忙时排队闲时浪费)、污染(上一次构建的残留文件影响下一次)。Kubernetes 上的 Jenkins 把「构建环境」变成声明式的 Pod 模板:每次构建由插件创建一个 agent Pod,跑完就删;JDK、Maven、Docker 版本都写在镜像里;并发上限由集群资源而不是物理机决定。controller 自己仍然是有状态的(jenkins_home 要持久化),但它不再执行构建。
代价是复杂度转移到了「怎么让 controller 与 agent 互相找到对方」:agent 要能连上 controller 的 50000 端口,controller 要能通过 API Server 创建 Pod,而集群内的访问路径又和浏览器访问的域名不同。这三件事就是本章的实验重点。
4.4.3. 核心概念
4.4.3.1. controller 与 agent
图: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 等资源。
4.4.3.2. deploy/ 里的资源
文件 |
对象 |
作用 |
|---|---|---|
|
PVC 10Gi / |
持久化 |
|
SA + ClusterRole + Binding |
允许 controller 管理 Pod |
|
Deployment |
controller,含 |
|
Service ×2 |
|
|
PVC 10Gi / RWX |
多个 agent 共享的 |
|
Ingress |
|
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 更符合最小权限。
4.4.3.3. 初始密码与插件
首次启动会走 setup wizard,初始密码写在 jenkins_home/secrets/initialAdminPassword:
kubectl -n jenkins exec deploy/jenkins -- cat /var/jenkins_home/secrets/initialAdminPassword
必装的插件是 Kubernetes(agent 调度)、Pipeline(声明式语法)、Git(拉代码)、Configuration as Code(把配置写成 YAML)。Maven 不需要装插件——构建在 maven 容器里跑,用的是镜像自带的 mvn,而不是 Jenkins 全局工具。
4.4.3.4. 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 引用,就能在多个流水线间复用。
4.4.3.5. 声明式 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。
4.4.3.6. 缓存与并发
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 和集群资源共同约束。
4.4.3.7. 安全与备份
三件事必须做:给 Jenkins 建管理员账号并关掉匿名读;RBAC 收到最小权限(Role 而非 ClusterRole,只给 pods、pods/exec、pods/log);凭据走 Jenkins Credentials 或外部 Secret,不在 Pipeline 里明文写,配置用 JCasC 固化到 Git。jenkins_home 是 PVC,里面装着任务定义、插件和凭据加密密钥,备份它等价于备份整个 Jenkins——用 PVC 快照或用 Velero 做命名空间级备份。
4.4.4. 动手实验
实验:部署 Jenkins 并跑一个 kubernetes agent 的 maven 构建
目标:用仓库 deploy/ 部署 Jenkins,取初始密码登录,配置 Kubernetes Cloud,跑通 kubernetes agent + maven 容器的 Pipeline,最后用 Ingress 访问。
前置:集群有 nfs-csi StorageClass 与 ingress-nginx;能解析 jenkins.magedu.com(或改成本机 hosts)。
4.4.4.1. 步骤 1 · 应用 deploy/ 清单
kubectl apply -f source-repo/jenkins/deploy/
kubectl -n jenkins get pod,pvc,svc,ingress
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 注册不上)。
4.4.4.2. 步骤 2 · 取初始密码并登录
kubectl -n jenkins exec deploy/jenkins -- \
cat /var/jenkins_home/secrets/initialAdminPassword
kubectl -n jenkins port-forward svc/jenkins 8080:8080
a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
Forwarding from 127.0.0.1:8080 -> 8080
浏览器打开 http://127.0.0.1:8080,粘贴密码,选「安装推荐的插件」,再建管理员账号。如果密码文件不存在,通常是 PVC 里已有旧的 config.xml——说明 Jenkins 已经初始化过,用当时的管理员账号登录即可。
4.4.4.3. 步骤 3 · 配置 Kubernetes Cloud
装完 Kubernetes 插件后,在「Manage Jenkins → Clouds」新增一个 Kubernetes 云:
配置项 |
值 |
|---|---|
Kubernetes URL |
|
Kubernetes Namespace |
|
Credentials |
|
Jenkins URL |
|
Pod Template 名称 |
|
Pod Template 里加两个容器:jnlp 用 jenkins/inbound-agent:latest-jdk17,maven 用 maven:3.9-eclipse-temurin-17,命令 sleep + infinity(否则容器跑完就退出)。Service Account 填 jenkins-master,让它有权限创建 Pod。
4.4.4.4. 步骤 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,构建结束后它被自动删除——这就是「一次构建一个干净环境」的直接证据。
4.4.4.5. 步骤 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
jenkins nginx cicd.magedu.com,jenkins.magedu.com 10.0.0.15 80
HTTP/1.1 200 OK
仓库的清单里还多配了 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
deployment.apps/coredns restarted
200
4.4.4.6. 验证清单
kubectl -n jenkins get pod中 controller 是1/1 Running,jenkins-pvc是Bound跑 Pipeline 时能看到临时 agent Pod,结束后自动消失
集群内
curl http://jenkins.magedu.com返回 200
4.4.5. 常见问题与排错
现象 |
可能原因 |
处理方式 |
|---|---|---|
controller |
NFS 卷属主是 root,镜像 UID 1000 写不了 |
确认 initContainer 生效;NFS 配 |
|
ServiceAccount 或 RBAC 缺失 |
|
Pipeline 卡在 |
Cloud 未配置、Pod Template 名字不匹配 |
检查 |
agent 注册超时,controller 日志 |
Jenkins URL 填了 Ingress 域名,或 50000 未暴露 |
改成 |
Ingress 502 |
|
|
集群内解析 |
CoreDNS rewrite 未生效或没重启 |
|
4.4.6. 进阶挑战
把 Cloud、Pod Template、管理员账号写成 JCasC 的
jenkins.yaml,用 ConfigMap 挂进容器,实现「删了 controller 也能重建出一模一样的 Jenkins」。给 Pod Template 加一个
dind容器(docker:27-dind),在 Pipeline 里container('docker')构建镜像并推到 Harbor。
4.4.7. 小结
controller 有状态、agent 无状态:
jenkins_home上 PVC,构建环境由 Pod 模板定义。agent 是主动连 controller 的 50000 端口,所以 Jenkins URL 必须写 Service 的集群内地址。
deploy/里extensions那个 apiGroup 是 1.16 之前的历史遗留,v1.31 上应删掉;RBAC 建议从ClusterRole收到Role。