Kubernetes部署Jenkins核心避坑指南:PVC权限、RBAC与Ingress实战

发布时间:2026/6/23 10:03:02
Kubernetes部署Jenkins核心避坑指南:PVC权限、RBAC与Ingress实战 1. 为什么在 Kubernetes 上装 Jenkins 不是“把 Docker 镜像跑起来”那么简单Jenkins 跑在 Kubernetes 上表面看只是换了个运行环境——从物理机/虚拟机迁移到容器编排平台。但实际动手时90% 的人卡在第一步kubectl apply -f jenkins.yaml后kubectl get pods里 Jenkins Pod 一直卡在ContainerCreating或CrashLoopBackOffkubectl describe pod里满屏FailedMount、ImagePullBackOff、Init:Error。这不是配置写错了而是根本没理解 Kubernetes 对“有状态应用”的约束逻辑。Jenkins 的核心矛盾在于它天生是个强状态、弱隔离、高权限、多依赖的系统。它要持久化JENKINS_HOME含所有 job 配置、插件、构建历史要挂载宿主机的 Docker socket才能用 Docker-in-Docker 或 Docker-outside-Docker 构建镜像要访问集群外的 Git 服务器、Maven 仓库、Harbor 镜像库还要能被外部用户通过浏览器访问。而 Kubernetes 的默认设计哲学是无状态优先、最小权限原则、网络隔离默认开启、存储需显式声明。所以“安装 Jenkins on Kubernetes”这个动作本质不是部署一个应用而是为一个传统单体 CI 工具在云原生基础设施上重建一套符合其运行逻辑的支撑体系。这包括用PersistentVolumeClaimPVC替代hostPath或emptyDir解决JENKINS_HOME持久化问题用ServiceAccountRoleBinding替代root权限解决 Jenkins Master 访问 Kubernetes API 的权限问题比如动态创建 Agent Pod用Ingress或LoadBalancer Service替代NodePort解决安全暴露与域名路由问题用ConfigMap/Secret管理values.yaml中的敏感配置如管理员密码、Git 凭据而非硬编码进 YAML用initContainer预处理文件权限Jenkins 官方镜像以jenkins用户运行但 PVC 挂载后目录属主常为root导致启动失败。我第一次在 Ubuntu 22.04 集群上部署时就栽在 initContainer 权限上。Pod 日志只显示Permission deniedkubectl exec -it jenkins-pod -- ls -l /var/jenkins_home发现目录属主是root:root而 Jenkins 进程以 UID 1001 运行。查了整整三小时才发现官方 Helm Chart 的securityContext.runAsUser默认值是1001但 PVC 绑定的 PV底层是 NFS 或 Ceph默认创建目录属主是root。解决方案不是改 Jenkins UID而是加一个 initContainer用chown 1001:1001 /var/jenkins_home预处理。这个细节所有“Jenkins 菜鸟教程”和“Kubernetes 面试宝典”都不会提但它决定了你能不能看到 Jenkins 登录页。提示不要迷信 Helm Chart 的“一键安装”。Helm 只是 YAML 模板渲染器它不判断你的集群是否已启用 RBAC、StorageClass 是否存在、节点是否有足够内存。真正的安装是从kubectl get nodes和kubectl get storageclass开始的。2. 选型决策Helm Chart vs 手写 YAML —— 为什么我坚持手写前 3 个 YAML 文件社区里最常被推荐的是 Jenkins 官方 Helm Chartjenkinsci/kubernetes-plugin项目下的jenkinsChart。它确实省事helm repo add jenkins https://charts.jenkins.io helm install my-jenkins jenkins/jenkins5 分钟出界面。但我在给一家金融客户做 CI/CD 架构评审时发现他们线上环境的 Jenkins 因 Chart 升级导致插件全丢——Chart 的values.yaml里controller.JenkinsImage指向jenkins/jenkins:lts而某次lts标签被覆盖新镜像未预装客户定制的 SonarQube 插件和 LDAP 认证模块CI 流水线全部中断。这件事让我彻底放弃 Helm Chart 作为生产环境首选。原因有三2.1 Helm 的抽象层掩盖了真实依赖关系Helm Chart 把Deployment、Service、Ingress、PVC、Secret全部揉在一个模板里。当你需要调试kubectl logs jenkins-pod报错failed to resolve host name mirrors.tuna.tsinghua.edu.cn时你得先helm get manifest my-jenkins导出原始 YAML再逐行比对env和dnsConfig字段。而手写 YAML每个文件职责单一jenkins-deployment.yaml只管容器定义jenkins-service.yaml只管服务暴露jenkins-pvc.yaml只管存储申请。排查时kubectl get pvc看存储状态kubectl get svc看服务端口逻辑链路清晰。2.2 版本锁定与灰度发布不可控Helm 的helm upgrade是原子操作要么全成功要么全回滚。但 Jenkins 插件升级、JDK 版本切换、Java 项目构建工具Maven/Gradle版本变更都需要灰度验证。手写 YAML 可以轻松实现先部署jenkins-v2-deployment.yaml到jenkins-v2命名空间用kubectl port-forward svc/jenkins-v2 8080:8080本地测试确认无误后再删掉旧版。Helm 的--version参数只能锁 Chart 版本锁不住底层镜像的:lts标签漂移。2.3 安全策略无法精细化控制金融客户要求 Jenkins Master禁止访问公网防止插件自动下载、禁止挂载 Docker socket禁用 DinD、所有外连必须走企业代理。Helm Chart 的values.yaml里没有spec.template.spec.dnsConfig.nameservers字段也没有spec.template.spec.containers[0].env下的http_proxy全局配置入口。你得 fork Chart 改源码不如直接在jenkins-deployment.yaml里加dnsConfig: nameservers: - 10.10.10.10 # 企业内网 DNS env: - name: http_proxy value: http://proxy.internal:3128 - name: https_proxy value: http://proxy.internal:3128所以我的实操路径是前 3 个 YAML 文件PVC、Deployment、Service必须手写确保核心逻辑可控后续 Ingress、RBAC、ConfigMap 用 Helm 管理提升复用效率。这既规避了 Helm 的黑盒风险又保留了模板化优势。注意kubectl get nodes no ready这类错误90% 是节点资源不足CPU/Memory Request 超过节点容量或 CNI 插件异常。手写 YAML 时在resources.requests里明确写死memory: 2Gi、cpu: 1比 Helm Chart 默认的512Mi更易定位问题。3. 核心 YAML 文件详解从 PVC 到 Deployment 的 7 个关键字段解析下面是我在线上环境稳定运行 18 个月的jenkins-deployment.yaml核心片段。它不是照搬官网示例而是基于 Ubuntu 22.04 containerd Ceph RBD 存储的真实配置。我会逐字段解释“为什么这么写”以及“不这么写会怎样”。3.1persistentVolumeClaimPVC 名称必须与 StorageClass 匹配且需提前验证 PV 可用性volumes: - name: jenkins-home persistentVolumeClaim: claimName: jenkins-home-pvc # 必须与 jenkins-pvc.yaml 中的 metadata.name 一致为什么重要claimName是 PVC 的唯一标识。如果写成claimName: jenkins-pvc而实际 PVC 文件名为jenkins-home-pvc.yamlkubectl apply会报persistentvolumeclaim jenkins-pvc not found。更隐蔽的坑是kubectl get pvc显示Bound但kubectl get pv显示对应 PV 的STATUS是Failed常见于 Ceph RBD 存储池配额超限。此时 Jenkins Pod 会卡在ContainerCreatingkubectl describe pod显示Unable to attach or mount volumes。实操技巧部署前必做三步验证kubectl get storageclass确认默认 StorageClass 存在如rook-ceph-blockkubectl apply -f jenkins-pvc.yaml后等kubectl get pvc jenkins-home-pvc -o wide输出STATUS为Boundkubectl get pv $(kubectl get pvc jenkins-home-pvc -o jsonpath{.spec.volumeName}) -o wide确认 PVSTATUS为Bound且CLAIM字段指向default/jenkins-home-pvc。3.2securityContextrunAsUser和fsGroup必须同时设置否则挂载失败securityContext: runAsUser: 1001 fsGroup: 1001为什么重要Jenkins 官方镜像以 UID 1001 运行但 PVC 挂载的目录如/var/jenkins_home在底层存储NFS/Ceph上创建时属主常为root。Linux 内核要求当容器以非 root 用户运行时挂载卷的目录属主必须与runAsUser匹配或fsGroup能覆盖目录组权限。只设runAsUser不设fsGroup会导致mkdir: cannot create directory /var/jenkins_home/jobs: Permission denied。避坑经验Ubuntu 22.04 的 containerd 默认启用userns-remap会进一步加剧权限问题。解决方案不是关掉 user namespace而是在securityContext中强制指定fsGroup: 1001并确保 PVC 的volumeMode为Filesystem而非Block。3.3volumeMountssubPath是避免覆盖整个 PVC 目录的关键volumeMounts: - name: jenkins-home mountPath: /var/jenkins_home subPath: jenkins-home # 关键只挂载子目录不覆盖 PVC 根为什么重要如果不加subPath整个 PVC 挂载点如/var/lib/ceph/rbd/data...会直接映射到/var/jenkins_home。Jenkins 启动时会清空该目录下所有非自有文件导致 PVC 中其他应用的数据丢失。加subPath: jenkins-home后只挂载 PVC 内部的jenkins-home/子目录安全隔离。实操验证kubectl exec -it jenkins-pod -- ls -l /var/jenkins_home应输出类似drwxr-xr-x 2 jenkins jenkins 4096 Jun 10 08:22 jobs drwxr-xr-x 2 jenkins jenkins 4096 Jun 10 08:22 plugins如果看到root:root说明fsGroup未生效或subPath错误。3.4envJAVA_OPTS必须包含-Djenkins.install.runSetupWizardfalseenv: - name: JAVA_OPTS value: -Djenkins.install.runSetupWizardfalse -Dhudson.model.DirectoryBrowserSupport.CSP -Dfile.encodingUTF-8为什么重要Jenkins 2.300 默认开启 Setup Wizard安装向导首次访问会强制跳转到/setup页面。但在 Kubernetes 中这个页面需要交互式输入管理员密码、插件选择无法自动化。-Djenkins.install.runSetupWizardfalse强制跳过让 Jenkins 直接进入登录页并从JENKINS_HOME/secrets/initialAdminPassword读取密码。关联配置initialAdminPassword必须通过Secret注入。jenkins-secret.yaml内容如下apiVersion: v1 kind: Secret metadata: name: jenkins-secret type: Opaque data: jenkins-admin-password: cGFzc3dvcmQxMjM # echo -n password123 | base64然后在 Deployment 的env中引用- name: JENKINS_ADMIN_PASSWORD valueFrom: secretKeyRef: name: jenkins-secret key: jenkins-admin-password3.5livenessProbe和readinessProbe探针路径必须带/login否则健康检查永远失败livenessProbe: httpGet: path: /login port: http initialDelaySeconds: 120 periodSeconds: 30 readinessProbe: httpGet: path: /login port: http initialDelaySeconds: 60 periodSeconds: 10为什么重要很多教程用/或/api/json作为探针路径。但 Jenkins 在启动过程中/会重定向到/login而/api/json需要认证。httpGet探针不跟随重定向也不携带 Cookie导致curl -I http://pod-ip:/返回302探针判定为失败反复重启 Pod。/login是公开路径返回200 OK且 Jenkins 启动完成后该路径必然可用。参数依据initialDelaySeconds: 120是因为 Jenkins 在JENKINS_HOME首次初始化时如插件安装、索引重建可能耗时超过 90 秒。periodSeconds: 30避免过于频繁的探测增加负载。3.6resourcesRequest 和 Limit 必须严格匹配否则触发 OOMKilledresources: requests: memory: 2Gi cpu: 1 limits: memory: 4Gi cpu: 2为什么重要Kubernetes 调度器根据requests分配节点资源limits控制容器上限。如果requests.memory设为512MiHelm Chart 默认值而 Jenkins 实际需要 1.5Gi 内存Pod 会被调度到内存紧张的节点运行中因 OOM 被 Kill。kubectl describe pod会显示OOMKilledkubectl logs jenkins-pod --previous看到java.lang.OutOfMemoryError: Java heap space。调优方法在测试环境用kubectl top pod jenkins-pod观察内存峰值。Jenkins Master 的合理范围是小型团队10 人requests.memory: 2Gi中型团队10-50 人requests.memory: 4Gi大型团队50 人requests.memory: 8Gi并启用 Jenkins 的--httpPort-1 --ajp13Port8009用 AJP 协议卸载 HTTP 负载。3.7imagePullPolicy生产环境必须设为IfNotPresent禁用Alwayscontainers: - name: jenkins image: harbor.internal/jenkins/jenkins:lts-centos8 imagePullPolicy: IfNotPresent为什么重要imagePullPolicy: Always会导致每次 Pod 启动都尝试拉取镜像即使本地已有。当 Harbor 仓库网络抖动或镜像标签被覆盖时ImagePullBackOff错误频发。IfNotPresent仅在本地无镜像时拉取配合企业 Harbor 的镜像预热策略kubectl apply -f jenkins-image-preload.yaml可将启动时间从 90 秒降至 15 秒。安全加固镜像地址必须用私有 Harbor如harbor.internal/jenkins/jenkins:lts-centos8禁用jenkins/jenkins:lts这类公共镜像。后者可能被投毒且无安全漏洞扫描报告。4. 权限体系重构ServiceAccount RoleBinding 是 Jenkins 访问 Kubernetes API 的唯一合法路径Jenkins 要实现真正的云原生 CI/CD必须能动态创建和管理构建 Agent即 Kubernetes Plugin 的核心能力。这意味着 Jenkins Master 进程必须能调用 Kubernetes API执行kubectl get pods、kubectl create pod等操作。但 Kubernetes 默认禁止任何 Pod 访问 API Server除非显式授权。很多人图省事在jenkins-deployment.yaml中加hostNetwork: true或privileged: true甚至挂载/var/run/docker.sock。这是严重违反安全基线的做法。正确的路径是用 Kubernetes 原生的 RBAC 机制授予 Jenkins 最小必要权限。4.1 创建专用 ServiceAccount禁用 default 账户# jenkins-sa.yaml apiVersion: v1 kind: ServiceAccount metadata: name: jenkins-sa namespace: default为什么不用 defaultdefaultServiceAccount 在每个命名空间自动创建其 Token 被注入到所有 Pod 的/var/run/secrets/kubernetes.io/serviceaccount/token。如果 Jenkins 使用 default它就拥有了当前命名空间的全部权限取决于绑定的 Role且一旦泄露攻击者可横向移动。专用jenkins-sa可精准控制权限范围。4.2 定义 Role只允许操作 Jenkins 自身命名空间内的 Pod 和 ConfigMap# jenkins-role.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: jenkins-role namespace: default rules: - apiGroups: [] resources: [pods, pods/exec, pods/log, configmaps] verbs: [get, list, watch, create, delete] - apiGroups: [] resources: [services] verbs: [get, list, watch]权限精简逻辑pods/execAgent Pod 启动后Jenkins 需要kubectl exec进入容器执行构建命令pods/logkubectl logs jenkins-pod查看最后 100 条日志的功能依赖此权限configmapsJenkins Pipeline 脚本中kubernetes { configMapName: maven-settings }需要读取 ConfigMap不开放secrets凭据应通过 Jenkins Credentials Plugin 管理而非直接读取 K8s Secret不开放nodeskubectl get nodes no ready是运维命令Jenkins 无需调用。4.3 绑定 Role 与 ServiceAccountRoleBinding 必须指定 namespace# jenkins-rolebinding.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: jenkins-rolebinding namespace: default roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: jenkins-role subjects: - kind: ServiceAccount name: jenkins-sa namespace: default关键点namespace: default必须与 ServiceAccount 和 Role 的 namespace 严格一致。RoleBinding 是 namespace-scoped 资源不能跨命名空间绑定。如果 Jenkins 部署在ci命名空间所有 YAML 文件的namespace字段都要改为ci。4.4 在 Deployment 中引用 ServiceAccount# jenkins-deployment.yaml 片段 spec: serviceAccountName: jenkins-sa # 关键必须与 jenkins-sa.yaml 中的 name 一致 automountServiceAccountToken: true # 默认 true确保 token 挂载到 /var/run/secrets/kubernetes.io/serviceaccount/验证权限是否生效部署后kubectl exec -it jenkins-pod -- cat /var/run/secrets/kubernetes.io/serviceaccount/token获取 Token然后curl -k -H Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token) \ https://kubernetes.default.svc/api/v1/namespaces/default/pods如果返回200 OK和 Pod 列表说明权限配置成功如果返回403 Forbidden检查 RoleBinding 的subjects是否拼写错误。提示kubectl logs jenkins-pod查看最后 100 条日志本质就是 Jenkins 调用pods/logAPI。如果日志查不到90% 是 RoleBinding 权限缺失而非网络问题。5. 网络暴露与安全加固Ingress TLS Basic Auth 的三层防护实践Jenkins 默认监听8080端口但直接用NodePort暴露到公网是危险的。NodePort范围是30000-32767容易被扫描攻击且无法实现 HTTPS 加密和细粒度访问控制。企业级部署必须通过Ingress实现第一层TLS 加密Lets Encrypt 或企业 CA第二层Basic Auth 认证拦截未授权访问第三层Ingress Controller 的 WAF 规则防 SQL 注入、XSS。5.1 Ingress 资源定义host 和 path 必须与 Jenkins 配置一致# jenkins-ingress.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: jenkins-ingress annotations: nginx.ingress.kubernetes.io/auth-type: basic nginx.ingress.kubernetes.io/auth-secret: jenkins-auth nginx.ingress.kubernetes.io/auth-realm: Authentication Required nginx.ingress.kubernetes.io/ssl-redirect: true nginx.ingress.kubernetes.io/force-ssl-redirect: true spec: ingressClassName: nginx tls: - hosts: - jenkins.internal.company.com secretName: jenkins-tls-secret rules: - host: jenkins.internal.company.com http: paths: - path: / pathType: Prefix backend: service: name: jenkins-service port: number: 8080关键配置解析tls.secretName: jenkins-tls-secret该 Secret 必须包含tls.crt和tls.key由kubectl create secret tls jenkins-tls-secret --certtls.crt --keytls.key创建nginx.ingress.kubernetes.io/auth-secret: jenkins-authBasic Auth 的用户密码文件需用htpasswd生成htpasswd -c auth jenkins-admin # 生成 auth 文件 kubectl create secret generic jenkins-auth --from-fileauthpath: /且pathType: Prefix确保 Jenkins 的所有子路径如/job/my-pipeline/build、/pluginManager/都能被正确路由。5.2 Jenkins 系统配置JENKINS_URL必须与 Ingress host 一致在jenkins-deployment.yaml的env中添加- name: JENKINS_URL value: https://jenkins.internal.company.com - name: JENKINS_OPTS value: --prefix/为什么重要Jenkins 生成的构建链接、邮件通知中的 URL、Git webhook 回调地址都依赖JENKINS_URL。如果设为http://localhost:8080用户点击邮件里的链接会打不开GitLab 的 webhook 会因HTTP 301重定向失败。--prefix/确保所有 URL 路径以/开头与 Ingress 的path: /匹配。5.3 安全加固禁用 Script Security 和 Groovy Sandbox 的风险Jenkins Pipeline 允许执行 Groovy 脚本这是强大之处也是最大攻击面。默认开启的 Script Security沙箱会拦截sh rm -rf /等危险操作但高级攻击者可通过反射绕过。生产环境必须在Manage Jenkins Configure Global Security中勾选Enable script security for Pipelines在Script Approval页面定期审核Pending Signatures拒绝method java.lang.Runtime exec java.lang.String等高危签名用Jenkinsfile的options { disableConcurrentBuilds() }防止构建队列堆积。5.4 故障排查kubectl get nodes no ready与 Jenkins 无关但常被误判搜索热词中高频出现kubectl get nodes no ready很多人以为这是 Jenkins 安装失败的原因。实际上这是Kubernetes 节点状态异常与 Jenkins 无关。典型原因节点 kubelet 服务宕机sudo systemctl status kubelet节点磁盘压力df -h /var/lib/kubelet满了CNI 插件异常kubectl get pods -n kube-system | grep calico状态非 Running。Jenkins Pod 无法调度到NotReady节点但错误根源在节点本身。解决步骤kubectl get nodes -o wide确认哪些节点 NotReadyssh到该节点sudo journalctl -u kubelet -n 100查日志修复节点后kubectl uncordon node-name解除调度封锁。最后分享一个小技巧Jenkins 安装后第一时间在Manage Jenkins System Log Add new log recorder中创建kubernetes-plugin日志记录器级别设为FINE。当 Agent Pod 创建失败时kubectl logs jenkins-pod | grep -A 5 -B 5 Failed to create pod能直接定位是 RBAC 权限问题还是 YAML 模板语法错误。