跳转至

Jenkins 流水线

Jenkins 的老问题不是「不会写 Pipeline」,而是「构建环境不可复现」:一台机器上装三个 JDK、Maven 版本互冲突、并发一高就互相踩。把它搬到 Kubernetes 上,controller 只负责调度,每个构建都用一个全新的 agent Pod,环境由镜像定义。

难度 · 进阶 预计 60 分钟 前置:Deployment、Service、Ingress、PVC

你将学到

  • Jenkins on Kubernetes 的 controller / agent 架构,JNLP 协议与 Kubernetes plugin 的作用
  • 仓库 jenkins/deploy/ 里每个资源对象的职责,以及 RBAC 的过时写法怎么改
  • 初始密码获取、插件安装、Kubernetes Cloud 与 Pod Template 的配置
  • 用 kubernetes agent 跑 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:

kubectl -n jenkins exec deploy/jenkins -- cat /var/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/ 清单

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 注册不上)。

步骤 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 已经初始化过,用当时的管理员账号登录即可。

步骤 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 任务,把下面的脚本贴进去:

Jenkinsfile
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 访问,并让集群内也能解析

07-ingress-jenkins.yaml
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 片段
# 片段,放进 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

验证清单

  • 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 验证

进阶挑战

  1. 把 Cloud、Pod Template、管理员账号写成 JCasC 的 jenkins.yaml,用 ConfigMap 挂进容器,实现「删了 controller 也能重建出一模一样的 Jenkins」。
  2. 给 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。

延伸阅读