资讯动态

Jenkins Slave 动态构建:基于 Kubernetes 的弹性 CI 实践

发布时间:2026/10/14 20:10:53 来源:尧图企业网站定制
1. 为什么要把 Jenkins Slave 塞进 Kubernetes1.1 从一台构建机到弹性构建池的演进逻辑早些年做持续集成最省事的做法就是在 Jenkins Master 上直接跑构建任务装好 JDK、Maven、Node、Docker 就开干。项目少、构建频率低的时候这套玩法确实够用。但只要团队规模一上来问题就全冒出来了构建任务互相抢占 CPU 和内存一个跑前端打包的任务能把整台机器的内存吃光导致后面排队的后端构建全部卡死不同项目对工具链版本要求还不一样有的要 JDK8有的要 JDK17有的要 Node14有的要 Node20全塞在一台机器上迟早打架。我印象特别深的一次是某个周五下午要发版结果一个跑自动化测试的任务把 Master 的磁盘写满了整个 Jenkins 直接挂掉所有人都干等着。那次之后我就下定决心构建节点必须和 Master 彻底解耦而且要做到按需创建、用完即毁。Kubernetes 天然适合干这件事。它本身就是做容器编排的能按资源请求调度 Pod能限制 CPU 和内存上限能在任务结束后自动回收资源。把 Jenkins Slave 做成 K8s 里的 Pod等于给 Jenkins 接了一个可以无限扩容、又不会互相污染的构建池。任务来了就拉起一个 Pod任务跑完 Pod 自动销毁环境永远是干净的再也不用担心“上次构建残留的文件影响了这次构建”这种玄学问题。1.2 这套方案到底解决了哪些真实痛点先明确一下这套组合拳的核心价值免得你只是跟风搭一遍却不知道图什么。第一是环境隔离。每个构建任务跑在独立的 Pod 里用的是独立的容器镜像工具链版本完全由镜像决定。前端项目用 Node 镜像Java 项目用 Maven 镜像互不干扰。这一点在多语言混合的团队里价值巨大。第二是弹性伸缩。传统 Slave 是常驻的你为了应对发版高峰得准备一堆机器平时又闲置浪费。K8s 方案下Slave 数量跟着任务量走闲时零 Pod忙时自动拉起几十个资源利用率直接拉满。第三是环境一致性。Slave 的镜像可以纳入版本管理今天构建用的镜像和三个月后用的镜像是同一个 tag构建结果可复现。以前那种“在我机器上能跑”的扯皮会少很多。第四是运维成本。不用再手动登录每台 Slave 去升级工具、清理磁盘、排查环境问题。镜像改一版所有新拉起的 Pod 自动用新环境。提示这套方案不是银弹。如果你的团队只有两三个项目、每天构建次数个位数那老老实实单机 Jenkins 更省心。上 K8s 方案的前提是构建量大、环境要求杂、或者你本来就有 K8s 集群可以复用。1.3 适合谁来参考这篇内容这篇内容适合三类人一是已经会用 Jenkins 但被构建环境问题折磨过的运维或开发二是手里有 K8s 集群、想把 CI 也搬上去的团队三是想搞清楚 Jenkins 动态 Slave 原理、准备自己动手搭一套的工程师。我假设你已经对 Jenkins 的基本概念Job、Pipeline、节点有了解也对 K8s 的 Pod、Namespace、ServiceAccount 这些名词不陌生。如果你完全是新手建议先把这两块的基础过一遍再回来不然配置里的很多参数你会看不懂为什么这么填。下面我会从整体设计讲到具体配置再到踩坑排查尽量把每一步的“为什么”都讲清楚而不是甩一堆 YAML 让你抄。2. 整体架构设计与关键选型考量2.1 Master 与 Slave 的职责边界怎么划在动手之前先把职责想清楚不然后面配置会乱。Jenkins Master 负责接收 Webhook、调度任务、管理构建队列、存储构建历史和日志、提供 Web UI。它本身不跑重活所以资源可以给小一点但磁盘要留足因为构建历史和工作空间元数据都在它这儿。Jenkins Slave也就是 K8s 里的 Pod负责真正执行构建步骤比如拉代码、编译、跑测试、打镜像。它是临时的任务结束就销毁所以不需要持久化存储也不需要保证高可用。这个边界划清楚之后你会发现 Master 其实很轻一台中等配置的机器就能扛住几百个任务的调度。真正吃资源的是 Slave而 Slave 交给 K8s 去管正好各司其职。2.2 为什么选 Kubernetes Plugin 而不是自己写脚本Jenkins 官方生态里把 Slave 跑在 K8s 上主要有两条路一是用官方的Kubernetes Plugin二是自己写脚本调 K8s API 动态创建 Pod 再注册成节点。我强烈建议走官方插件这条路。原因很简单插件已经帮你处理了 Pod 模板管理、JNLP 连接、生命周期回收、并发控制这些脏活。你自己写脚本光是一个“Pod 起来了但 Jenkins 还没连上”的时序问题就够你调半天。而且官方插件和 Jenkins 的 Pipeline 语法集成得很好在 Jenkinsfile 里直接写agent { kubernetes { ... } }就能用非常顺手。自己写脚本唯一的优势是灵活但除非你有非常特殊的需求比如要接入自研的调度系统否则没必要重复造轮子。2.3 连接方式JNLP 还是 SSHSlave 和 Master 之间怎么通信也是个要提前定的点。JNLPJava Network Launch Protocol方式Slave 容器里跑一个 agent.jar主动去连 Master 的 50000 端口。这种方式对网络方向友好因为通常是 Slave 主动连 Master不需要 Master 能反向访问 Slave。K8s 环境下 Pod 的 IP 是动态的Master 想主动连 Slave 反而麻烦所以 JNLP 是主流选择。SSH 方式Master 通过 SSH 连到 Slave 上执行命令。这种方式要求 Slave 暴露 SSH 端口、配好密钥在 K8s 里还要额外做 Service 暴露比较繁琐一般不推荐。结论很明确用 JNLP。官方 Kubernetes Plugin 默认也是走 JNLP配置起来最省事。2.4 镜像从哪来自建还是用官方Slave 的镜像决定了构建环境里有什么工具。这里有两个选择一是直接用 Jenkins 官方提供的jenkins/inbound-agent镜像它只包含 JNLP 连接所需的最小环境工具链要自己在 Pipeline 里装或者挂载。优点是干净、体积小、启动快。二是自建镜像在官方 agent 基础上装好 Maven、Node、Docker CLI 等常用工具。优点是构建时不用每次装工具速度快缺点是镜像大、维护成本高。我的建议是分层基础镜像用官方的 inbound-agent然后针对不同技术栈做几个专用镜像比如agent-maven、agent-node、agent-docker。Pipeline 里按项目类型选对应镜像。这样既避免了每次装工具的耗时又不至于做一个什么都装的巨型镜像。注意如果你要在 Slave 里构建 Docker 镜像也就是 Docker in Docker镜像里需要装 Docker CLI并且 Pod 要挂载宿主机的 docker.sock 或者用 DinD 方案。这块坑比较多后面单独讲。3. 环境准备与核心组件配置实操3.1 前置条件清单动手前先确认这些东西都到位了一个可用的 K8s 集群版本建议 1.20 以上能正常kubectl get nodes看到节点。集群里有默认的 StorageClass可选如果 Master 要持久化的话需要。一个能访问集群的 kubeconfig权限要能创建 Pod、Service、ConfigMap 等资源。Jenkins Master 已经部署好可以是跑在 K8s 里的也可以是集群外的物理机或虚拟机。集群内有一个能用的镜像仓库或者能拉取公网镜像。我假设你的 Jenkins Master 已经跑起来了下面重点讲怎么把它和 K8s 接起来。3.2 给 Jenkins 配置 Kubernetes 云登录 Jenkins进入Manage Jenkins → Clouds → New cloud选择Kubernetes。这一步是整套方案的核心入口。关键配置项逐个说Kubernetes URL填 K8s API Server 的地址。如果 Jenkins 跑在集群内可以直接填https://kubernetes.default.svc如果跑在集群外填集群的 API 地址比如https://10.0.0.1:6443。Kubernetes NamespaceSlave Pod 创建在哪个命名空间。建议单独建一个jenkins-slave命名空间和业务负载隔离开方便做资源配额和权限控制。Credentials这里要填一个能操作 K8s 的凭据。最规范的做法是创建一个 ServiceAccount绑定一个只允许管理 Pod 的 Role然后拿到它的 token 配到 Jenkins 里。不要图省事直接用 cluster-admin 的 kubeconfig权限太大一旦泄露风险很高。Jenkins URL填 Master 的地址Slave 启动后会通过这个地址回连。如果 Master 在集群内填 Service 名比如http://jenkins:8080。Jenkins tunnelJNLP 的隧道地址格式是jenkins:50000或者jenkins.default.svc:50000。这个地址是 Slave 容器里 agent.jar 要连的必须保证 Slave Pod 能解析和访问。配置完点Test Connection显示 “Connected to Kubernetes” 才算通。如果报错八成是凭据权限或者网络不通先解决这个再往下走。3.3 创建 ServiceAccount 和 RBAC 权限前面提到凭据要用最小权限的 ServiceAccount这里给出具体做法。先建命名空间kubectl create namespace jenkins-slave然后创建 ServiceAccountapiVersion: v1 kind: ServiceAccount metadata: name: jenkins-agent namespace: jenkins-slave接着是 Role只给管理 Pod 和 Pod 日志的权限apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: jenkins-agent-role namespace: jenkins-slave rules: - apiGroups: [] resources: [pods, pods/exec, pods/log, pods/portforward] verbs: [create, delete, get, list, watch] - apiGroups: [] resources: [events] verbs: [get, list, watch]再绑定apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: jenkins-agent-binding namespace: jenkins-slave subjects: - kind: ServiceAccount name: jenkins-agent namespace: jenkins-slave roleRef: kind: Role name: jenkins-agent-role apiGroup: rbac.authorization.k8s.io应用之后拿到这个 ServiceAccount 的 tokenkubectl -n jenkins-slave create token jenkins-agent把输出的 token 填到 Jenkins 凭据里类型选Secret text。这样 Jenkins 就有权限在jenkins-slave命名空间里创建和删除 Pod 了。提示不同 K8s 版本拿 token 的方式略有差异。1.24 之前 ServiceAccount 会自动创建 secret可以直接从 secret 里取1.24 之后需要用kubectl create token命令动态生成。如果你的 token 会过期记得定期更新或者用长期有效的 secret 方式。3.4 配置 Pod TemplatePod Template 是这套方案里最核心的配置它定义了 Slave Pod 长什么样。在 Cloud 配置页面往下拉找到Pod Templates点Add Pod Template。Name给模板起个名字比如default-agent后面 Pipeline 里可以引用。Namespace填jenkins-slave。Labels给这个模板打标签比如jenkins-agent。Pipeline 里用agent { label jenkins-agent }就能匹配到这个模板。Usage选Use this node as much as possible表示这个模板可以被所有匹配标签的任务使用。然后是Pod Template details点开配置容器。Container 配置Namejnlp这个名字是固定的官方插件要求主容器必须叫 jnlp。Docker image填你的 agent 镜像比如jenkins/inbound-agent:latest或者自建的my-registry/agent-maven:v1。Working directory一般留空用默认的/home/jenkins/agent。Command to run留空镜像里已经配好了启动命令。Arguments to pass to the command留空。Volumes 配置如果需要在 Pod 里挂载宿主机的 docker.sock 来构建镜像这里要加一个 Host Path Volume路径填/var/run/docker.sock挂载到容器的/var/run/docker.sock。Service Account填jenkins-agent这样 Pod 里的进程就有前面配的权限。配置完保存一个基础的 Pod Template 就建好了。4. Pipeline 中动态使用 Slave 的完整实操4.1 最简 Pipeline 示例先看一个能跑起来的最小例子感受一下整个流程pipeline { agent { kubernetes { label my-agent-pod yaml apiVersion: v1 kind: Pod spec: containers: - name: jnlp image: jenkins/inbound-agent:latest resources: requests: cpu: 500m memory: 512Mi limits: cpu: 1 memory: 1Gi - name: maven image: maven:3.9-eclipse-temurin-17 command: - cat tty: true resources: requests: cpu: 1 memory: 1Gi limits: cpu: 2 memory: 2Gi } } stages { stage(Build) { steps { container(maven) { sh mvn -v sh mvn clean package -DskipTests } } } } }这个 Pipeline 干了什么它告诉 Jenkins给我创建一个 Pod里面有两个容器一个是 jnlp负责和 Master 通信一个是 maven负责跑构建。构建步骤在 maven 容器里执行。注意command: - cat和tty: true这两行。这是让 maven 容器保持运行的标准写法因为容器默认执行完命令就退出了加了这个它才会一直挂着等 Jenkins 往里发指令。这个细节很多新手会漏导致 Pod 起来又立刻退出报错还看不懂。4.2 多容器协作构建、测试、打包各司其职实际项目里一个 Pod 里往往需要多个容器配合。比如 Java 项目编译用 Maven 容器构建 Docker 镜像用 Docker 容器跑集成测试用另一个容器。pipeline { agent { kubernetes { yaml apiVersion: v1 kind: Pod spec: containers: - name: jnlp image: jenkins/inbound-agent:latest - name: maven image: maven:3.9-eclipse-temurin-17 command: [cat] tty: true - name: docker image: docker:24-cli command: [cat] tty: true volumeMounts: - name: docker-sock mountPath: /var/run/docker.sock volumes: - name: docker-sock hostPath: path: /var/run/docker.sock } } stages { stage(Compile) { steps { container(maven) { sh mvn clean package -DskipTests } } } stage(Build Image) { steps { container(docker) { sh docker build -t my-app:${BUILD_NUMBER} . } } } } }这里的关键是container(xxx)语法它指定这一步在哪个容器里执行。不同容器共享同一个 Pod 的网络和存储卷所以 maven 编译出来的产物在 docker 容器里也能看到前提是工作目录一致默认都是/home/jenkins/agent/workspace/xxx。注意挂载宿主机 docker.sock 这种方式虽然方便但安全性上有隐患因为容器里的进程实际上能操作宿主机的 Docker 守护进程。如果集群是多租户的建议改用 DinDDocker in Docker方案或者用 Kaniko、Buildah 这类不需要守护进程的镜像构建工具。4.3 资源请求与限制怎么算Pod 的 resources 配置直接决定了构建能不能稳定跑。配小了任务被 OOM Kill配大了浪费集群资源。我的经验算法是这样的先看构建任务的历史峰值。比如一个 Maven 项目编译阶段内存峰值大概 1.5G那 requests 给 1Glimits 给 2G留点余量。CPU 方面编译是 CPU 密集型requests 给 1 核limits 给 2 核让它能 burst 一下。前端项目打包webpack内存吃得更凶大项目峰值能到 4G 以上这种就要单独配一个大内存的模板。一个实用的做法是给不同项目类型建不同的 Pod Template项目类型CPU requestsCPU limits内存 requests内存 limits小型 Java500m1512Mi1Gi大型 Java121Gi3Gi前端打包122Gi4Gi镜像构建500m1512Mi1Gi这些数字不是拍脑袋来的是观察一段时间构建监控后调出来的。刚开始可以给宽松点跑一段时间看实际用量再收紧。4.4 构建缓存怎么处理Slave 是临时的每次构建都是全新环境这意味着 Maven 的本地仓库、npm 的缓存每次都要重新下载构建时间会很长。解决办法是挂载持久化卷做缓存。以 Maven 为例把~/.m2/repository挂到一个 PVC 上apiVersion: v1 kind: PersistentVolumeClaim metadata: name: maven-cache namespace: jenkins-slave spec: accessModes: - ReadWriteMany resources: requests: storage: 20Gi然后在 Pod 模板里挂载volumeMounts: - name: maven-cache mountPath: /home/jenkins/.m2/repository volumes: - name: maven-cache persistentVolumeClaim: claimName: maven-cache这样多个 Pod 可以共享同一个缓存卷第一次下载之后后续构建就快了。不过要注意并发问题多个 Pod 同时写同一个缓存卷可能会冲突Maven 本身对并发下载有一定容错但极端情况下还是可能出问题。如果构建并发很高可以考虑每个节点一个缓存卷或者用更专业的缓存方案。提示ReadWriteMany 的 PVC 需要存储插件支持不是所有 StorageClass 都支持。如果集群不支持 RWX可以考虑用 NFS 或者干脆放弃共享缓存改用每次构建前从内部镜像仓库拉依赖的方式。5. 常见问题排查与避坑经验5.1 Pod 一直 Pending 起不来这是最常见的问题Pod 创建了但一直卡在 Pending 状态。排查思路按顺序来先kubectl -n jenkins-slave describe pod pod-name看 Events 部分。如果是Insufficient cpu或Insufficient memory说明集群资源不够要么加节点要么调小 requests。如果是no nodes available to schedule pods检查节点是否有污点Taint导致 Pod 无法调度。还有一种情况是镜像拉不下来Events 里会显示ImagePullBackOff。这时候检查镜像地址是否正确、私有仓库的 secret 是否配置。如果是私有仓库需要在 Pod 模板里配imagePullSecrets。5.2 Slave 连不上 MasterPod 起来了但 Jenkins 里看不到节点上线日志里报连接超时。这个问题通常是网络方向的问题。先确认 Slave Pod 能不能解析 Master 的地址。进到 Pod 里nslookup jenkins或者curl http://jenkins:8080试试。如果解析不了说明 DNS 配置有问题检查 Pod 的dnsPolicy和dnsConfig。如果 Master 在集群外Slave 要连的是外部地址那要确保集群能访问外网并且 Master 的 50000 端口对外暴露了。JNLP 端口默认是 50000如果 Master 前面有防火墙或负载均衡记得放行。还有一个容易忽略的点Master 的Jenkins URL配置。如果配的是localhost:8080Slave 拿到这个地址后回连的就是它自己当然连不上。必须配成 Slave 能访问到的地址。5.3 构建过程中 Pod 被 Kill构建跑到一半 Pod 突然没了Jenkins 报Pod was terminated。这种情况九成是资源超限被 OOM Kill 了。kubectl -n jenkins-slave describe pod pod-name看Last State如果是Terminated, Reason: OOMKilled那就是内存 limits 给小了。调大 limits 再试。也有可能是节点资源紧张K8s 的驱逐机制把 Pod 赶走了。看节点的事件日志如果有Evicted记录说明节点内存或磁盘压力大。这种情况要么给节点加资源要么给 Pod 设置更高的优先级PriorityClass。5.4 工作空间文件丢失有时候构建步骤之间文件对不上比如编译阶段生成的文件在打包阶段找不到了。这通常是因为不同容器的工作目录不一致。Jenkins 默认的工作目录是/home/jenkins/agent/workspace/job-name所有容器共享这个目录因为 Pod 内容器共享存储卷。但如果某个容器的镜像里HOME环境变量不一样或者你在步骤里cd到了别的地方就可能出问题。解决办法是在 Pipeline 里显式指定工作目录或者用ws步骤包起来。另外如果用了dir步骤切换目录要确保后续步骤也在同一个目录下。5.5 常见问题速查表现象可能原因排查命令解决方向Pod Pending资源不足/污点/镜像拉取失败kubectl describe pod调资源/加容忍/修镜像地址Slave 不上线网络不通/DNS 问题/URL 配错kubectl logs pod -c jnlp修 Jenkins URL/检查 DNSPod 被 KillOOM/节点驱逐kubectl describe pod调大 limits/加优先级文件丢失工作目录不一致pwd确认路径统一工作目录构建慢无缓存/镜像大看构建日志耗时挂缓存卷/精简镜像5.6 几个我踩过的坑坑一jnlp 容器名不能改。官方插件硬编码了jnlp这个容器名你改成别的它就不认Slave 永远不上线。这个坑我调了一下午才发现。坑二Pod 模板的 label 和 Pipeline 的 label 要对应。Pipeline 里写label my-agentPod 模板的 Labels 里也必须包含my-agent否则匹配不上Jenkins 会一直等节点。坑三并发构建时缓存卷冲突。前面提过多个 Pod 同时写同一个 Maven 缓存卷偶尔会出现文件损坏。后来我改成每个构建用独立的子目录或者干脆用只读的缓存快照问题就没了。坑四Master 的磁盘要留够。构建日志、工作空间元数据都存在 Master 上构建量大了一个月能吃掉几十 G。建议给 Master 配个定时清理任务或者把构建历史保留策略调短一点。坑五ServiceAccount 的 token 会过期。K8s 1.24 之后动态生成的 token 默认有效期是 1 小时如果 Jenkins 配置里用的是这种 token过一段时间就会失效导致无法创建 Pod。解决办法是用kubectl create token时指定--duration或者用长期 secret 的方式。6. 性能调优与规模化实践6.1 镜像瘦身与启动加速Slave Pod 的启动时间直接影响构建体验。一个 2G 的镜像拉取可能要一两分钟而一个 200M 的镜像十几秒就搞定。优化镜像的几个方向用 alpine 或 slim 基础镜像把不常用的工具从基础镜像里剥离按需在 Pipeline 里装合并 Dockerfile 的 RUN 指令减少层数用多阶段构建把编译工具和运行时分开。另外如果集群节点多可以配置镜像预热提前把常用 agent 镜像拉到各个节点上避免每次构建都现拉。6.2 并发控制与队列管理构建并发量大的时候要防止把集群资源打满影响业务负载。两个手段一是给jenkins-slave命名空间配 ResourceQuota限制总的 CPU 和内存用量apiVersion: v1 kind: ResourceQuota metadata: name: jenkins-slave-quota namespace: jenkins-slave spec: hard: requests.cpu: 20 requests.memory: 40Gi limits.cpu: 40 limits.memory: 80Gi pods: 50二是用 Jenkins 的Throttle Concurrent Builds插件限制单个 Job 的并发数避免同一个项目同时跑太多构建。6.3 监控与告警跑起来之后要能看见状态。几个关键指标Slave Pod 的创建成功率、平均启动时间、构建成功率、资源使用率。K8s 层面可以用 Prometheus 采集 Pod 的指标Jenkins 层面可以用 Prometheus 插件暴露构建指标。两边数据合起来看就能定位是环境问题还是代码问题。告警方面至少配两个Slave 创建失败率超过阈值告警构建队列积压超过阈值告警。这两个指标异常通常意味着集群资源不够或者配置出了问题。6.4 从单集群到多集群的扩展思路团队再大一点可能会遇到单集群资源不够、或者需要按业务线隔离构建环境的情况。这时候可以考虑多集群方案Jenkins Master 配置多个 Kubernetes Cloud每个 Cloud 指向不同的集群Pipeline 里按标签选择用哪个集群。这种架构下Master 成了单点要做好高可用。另外跨集群的镜像分发、缓存共享也要提前规划不然会变成新的运维负担。7. 写在最后的一点个人体会这套方案我从最早的单机 Slave 一路踩坑踩过来中间经历过 Pod 起不来、Slave 掉线、缓存冲突各种问题现在回头看最值得分享的经验其实不是某个具体配置而是先把职责边界想清楚再动手。Master 管调度、K8s 管资源、Slave 管执行三者各司其职配置自然就顺了。很多人一上来就抄 YAML结果出了问题不知道从哪查就是因为脑子里没有这张架构图。另外别追求一步到位。先把最简的 Pipeline 跑通确认 Slave 能正常创建和回收再逐步加缓存、加多容器、加资源限制。每加一个东西都验证一遍出问题也好定位。我见过太多人一次性配一大堆结果跑不起来排查起来像大海捞针。最后提醒一句这套方案的价值在于弹性和隔离如果你的场景根本用不上这两个特性那它带来的复杂度就是纯负担。技术选型永远要看场景别为了用而用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑