Rancher二次开发实战:自定义API从CRD到聚合服务的完整落地
2026/9/14 15:55:41 网站建设 项目流程

接到一个挺典型的二开需求:要给 Rancher 做一套自定义的创建 API,业务方不想直接去怼 Rancher 原生 API,而是希望走我们自己的封装层,在资源创建前统一做参数校验、默认值注入、甚至跨命名空间批量创建。把需求摊开一看,表面上是"加个接口",实际上涉及 Rancher API 的暴露机制、Schema 注册、权限模型这一整条链路。这篇文章就把我整个落地过程整理出来,包括架构选型、核心代码、以及几个比较隐蔽的坑,给后面要做 Rancher 二次开发自定义 API 的朋友做个参考。

1. 为什么要绕一层自定义 API:原生接口在真实业务里的三个短板

1.1 业务校验与默认策略无法内聚

Rancher 原生 API 本质上是 K8s API 的代理加一层封装,它会把你的请求直接翻译成对底层集群资源的操作。这意味着原生 API 只保证"资源能创建成功",至于"这个字段是否符合我们平台的规范""这个 annotation 是不是必须打上""namespace 是否存在白名单里"这类平台级策略,原生 API 完全不关心。

我们的实际场景是内部 PaaS 平台要对接多个 Rancher 实例,用户在界面上提交一个"应用"申请,平台后端要把这一条申请翻译成一整套 Rancher 资源:命名空间、工作负载、服务、配置字典,可能还要带上网络策略。如果每个资源都单独调原生 API,那么事务一致性全靠平台层自己编排,任何一个中间步骤失败都要写一堆补偿逻辑。更麻烦的是,如果业务规则变了——比如安全规范要求所有工作负载必须打上某个标签——你得上线新代码才能改,而且改的是平台层的胶水代码。

自定义创建 API 解决的就是这个内聚问题:把"一次业务申请"映射成"一组 Rancher 资源操作",把所有校验和默认策略收口到这一个接口里,对上游只暴露一个语义清晰的创建入口。

1.2 原生 API 的响应结构对业务不友好

Rancher 原生 API 返回的是标准 K8s 资源对象,字段非常多,嵌套深,很多字段对业务方没有意义。业务方通常只想拿到"创建成功了没有、资源叫什么名字、在哪个 namespace、需要多久能用"。如果直接代理原生 API,前端还要二次加工响应体才能展示。

自定义 API 可以按业务需要裁剪返回结构,比如只返回一个申请单 ID 加资源清单摘要,把内部细节全藏住。这在前后端分离、多团队协作的场景下特别实用,接口文档也更好写。

1.3 聚合多个后端动作的一致性需求

还有一类场景是"一个接口背后要干好几件事"。举个具体例子:创建一个工作负载的同时,可能要创建对应的一条 Ingress 规则,还要给某个监控系统注册一个健康检查地址。这些操作过去是前端按顺序调好几个 Rancher API,一旦第二三个请求失败,前端很难处理回滚。通过自定义创建 API 把整个动作包进一个后端事务,由服务端统一保证最终一致性,体验会好很多。

判断一个需求是否真的需要"自定义 API",我一般就看一条:上游拿到这个接口后,是否还需要同时对接 Rancher 的其他原生接口才能完成一个完整业务动作。如果是,那就值得封装;如果只是单纯换个请求格式,那用 API 网关做个转发就行,不值得做二次开发。

2. 动手前先把 Rancher API 的家底摸清楚:自研接口挂载的两种思路

2.1 Rancher API 层的演进:从 Norman 到 Steve

早期 Rancher 的 API 框架叫Norman,Rancher 2.6 之后逐步切到了Steve框架。Steve 的核心设计思路是"一切资源皆 Schema",它把 Kubernetes 的 CRD、内置资源、甚至 Rancher 自己的管理对象(如集群、项目)都统一抽象成 schema 来管理和暴露。

对二次开发来说,这个演进带来的最大变化是:你不需要像老版本那样硬改 Rancher Server 的主程序才能加接口了。在 Steve 框架下,只要新增一个 CRD 并在 Rancher 里注册对应的 schema,系统自动就会为这个资源生成一套标准的 RESTful API。这不是 hack,而是官方支持、社区常用的扩展路径。

2.2 自研 API 的两条路线对比

我梳理下来,挂在 Rancher 上的自定义 API 基本就两条路线,各有适用场景。

路线实现思路优点缺点适用场景
CRD + Schema 注册自定义资源接入 Steve,自动获得标准 CRUD API与 Rancher 原生 API 风格完全一致,鉴权/审计无缝继承只能提供资源型 API,复杂业务编排能力弱新资源模型,比如"应用模板""部署单"这类你要持久化的对象
旁路 Gateway 聚合独立服务部署在 Rancher 前端,由它调 Rancher 原生 API业务逻辑完全独立,可以自由编排多个后端动作,事务可控需要自己处理认证透传、权限控制,不享受 Rancher 的审计能力已有业务平台,需要封装多资源联动操作

我在这次项目里两种都用到了。底层确实定义了一个"部署单"的 CRD 来持久化业务状态,走的是路线一;但真正暴露给上游的是路线二的聚合服务,因为"一次创建要折叠多个资源操作",Steve 的资源型 API 表达能力满足不了这个需求。

2.3 为什么最终选择聚合服务作为对外 API

说一下选型时的心路历程。最初我确实想偷懒,直接把 CRD 注册进去,让上游像调 K8s API 一样调 Rancher,一次性拿到 CURD 能力。但马上发现一个问题:业务方提交的数据和 CRD 结构对不上。CRD 里存的是"申请单",而业务方关心的是"我要创建的工作负载长什么样"。如果强行走单一 Schema,就得让 CRD 变成一个大而全的"申请单"对象,里面 embedding 工作负载、服务、路由的全部字段,Schema 会变得非常臃肿,校验也不好写。

聚合服务的思路就清晰多了:对外接口是POST /v1beta1/applications,请求体完全按业务语义设计;服务内部把这个请求拆解成多个 Rancher API 调用。这个方案还有一个额外的好处——可以随时换底层实现。比如后期我们计划从 Rancher 迁移到原生 K8s 多集群管理,只需要改这个聚合服务的内部实现,上游接口完全不用动。

3. 核心实现第一步:用 CRD 承载业务状态,注册进 Rancher Schema

3.1 定义"部署单" CRD 的结构

虽然对外 API 是聚合服务,但我依然需要一个持久化载体来记录"这个业务申请当前到底创建到哪一步了",否则聚合服务一重启,进行中的创建动作就全丢了。于是我先定义了一个轻量 CRD,名字叫ApplicationOrder,放在paas.internal.example.com这个 group 下。

apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: applicationorders.paas.internal.example.com spec: group: paas.internal.example.com names: kind: ApplicationOrder listKind: ApplicationOrderList plural: applicationorders singular: applicationorder shortNames: - apporder scope: Namespaced versions: - name: v1 served: true storage: true schema: openAPIV3Schema: type: object properties: spec: type: object required: ["appName", "tenantId", "namespaceBase"] properties: appName: type: string tenantId: type: string namespaceBase: type: string targetWorkload: type: object x-kubernetes-preserve-unknown-fields: true replicas: type: integer minimum: 1 maximum: 20 status: type: object properties: phase: type: string enum: ["Pending", "Provisioning", "Ready", "Failed"] message: type: string rancherResources: type: array items: type: object properties: kind: type: string name: type: string namespace: type: string

有一点要注意:targetWorkload这里我用了x-kubernetes-preserve-unknown-fields,目的是保留业务方传入的工作负载扩展字段,不全量展开到 CRD 结构里。实际写的时候别偷懒全用这个字段,核心用于路由和校验的字段一定要显式声明,否则校验逻辑得靠 webhook 补,麻烦很多。

3.2 在 Rancher 中注册 Schema 的正确姿势

CRD 定义好只是第一步,要让 Rancher 的 Steve 框架认这个 CRD 并生成 API,需要让 Rancher 感知它。在新版 Rancher 里,Steve 会自动监听集群里的 CRD 变化,所以你只要把 CRD 应用到目标集群,Rancher 通常就会自动生成它的 schema。

但是我实测下来,'通常'这个词背后有几个前提:

  1. CRD 必须能被当前 Rancher Server 访问到的集群里发现。
  2. 如果 Rancher 开启了embedded模式,也就是内置 K3s 集群跑 Rancher Server,注册 CRD 建议直接打到 local 集群。
  3. 注册后可能需要等片刻,/v1/paas.internal.example.com/applicationorders才能访问。

如果你急着要它立刻生效,可以用下面的命令手动触发避过缓存等待时间:

# 找到 Rancher Server 的 pod,然后重启 cattle-cluster-agent 或直接触发 schema 刷新 kubectl -n cattle-system rollout restart deployment/cattle-cluster-agent

Schema 就绪后,用 Rancher API 的 schema 端点确认:

curl -sk https://<RANCHER_SERVER>/v1/paas.internal.example.com/applicationorders \ -H "Authorization: Bearer <RANCHER_API_KEY>"

能返回正常列表结构,说明 CRD 接入成功,此时 Rancher 已经为它生成了POST/GET/PUT/PATCH/DELETE全套标准操作。这套能力如果只用于内部状态记录,起点已经足够。

3.3 注册 Schema 过程中的一个踩坑点

我在这边遇到一个比较隐蔽的问题:CRD 的scope用了Namespaced,但业务上申请单应该跨 namespace 管理,它其实不隶属于某个具体的业务 namespace。我一开始把申请单直接放到了业务 namespace 里,结果上游业务方在某个 namespace 下查不到历史申请单,因为申请单跟着业务 namespace 走了,而业务 namespace 的生命周期有时候比申请单短。

后来我改了策略:ApplicationOrder统一放到 Rancher 的default项目下的一个专用 namespace,比如叫paas-control-plane,业务 namespace 只负责实际工作负载,申请单集中在控制平面 namespace 管理。这个调整本身不难,难的是 Rancher 项目的资源可见性边界——如果你把 CRD 资源建在某个项目下,项目的 RBAC 会影响谁能看到它,所以控制平面资源必须放在所有用户都有只读权限的项目里。

4. 聚合服务实现:业务 API 层如何编排多个 Rancher 原生调用

4.1 服务整体架构和数据流

聚合服务我用 Go 写,因为 Rancher 官方 client-go 生态比较成熟,而且我们团队 Go 基础好。整体调用数据流是这样的:

  1. 上游调用POST /v1beta1/applications,请求体包含应用名、租户 ID、副本数、镜像地址、路由规则等。
  2. 聚合服务先做业务层校验:租户白名单、资源配额预估、命名规范。
  3. 服务生成一个ApplicationOrderCR,状态Pending,写入控制平面 namespace。
  4. 服务调 Rancher API 创建 namespace(如果不存在)、创建工作负载、创建 Service、创建 Ingress。
  5. 每完成一个子步骤,更新ApplicationOrderstatus.rancherResources
  6. 全部成功后状态置为Ready,返回给上游一个包含资源清单摘要的响应。

这里最关键的一点是:聚合服务永远以ApplicationOrder状态为准。如果中途崩溃,服务重启后扫描PendingProvisioning状态的申请单,按幂等逻辑继续执行或回滚。上游发起的是一次 HTTP 请求,但后端会通过这个状态对象保证最终一致。

4.2 创建 Rancher 资源的幂等控制

聚合服务调 Rancher 原生 API 的时候,最大的隐患是网络抖动导致"请求发出去了但响应超时",然后你重试又创建了一遍。Rancher API 本身不是天然幂等的,工作负载同名会报冲突,但如果第一次实际成功、只是响应丢了,第二次可能返回的就不是冲突而是 409 之外的错误。

我的兜底方案分两层:

第一层:所有被创建的资源,名字都用有规则的确定性命名。比如工作负载名称 =app-{sha256(appName+tenantId)[:8]}-{shortId},这样重试时可以先 GET 一次,若已存在则不再重复创建。

第二层:整个创建顺序先 namespace,再工作负载,然后 Service,最后 Ingress。这个顺序不能乱,因为工作负载的域名和 Service 名称有依赖。伪代码逻辑大概是:

func ensureNamespace(ctx context.Context, client *rancherClient, ns string) error { _, err := client.Namespace.Get(ns, metav1.GetOptions{}) if err == nil { return nil // 已存在,幂等跳过 } _, err = client.Namespace.Create(ctx, &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{Name: ns}, }, metav1.CreateOptions{}) if err != nil && !apierrors.IsAlreadyExists(err) { return err } return nil }

ensure前缀的函数名在代码里到处都是,我在 review 时也会特意跟同事强调:只要是往 Rancher 写资源,都必须走这种"先查后建"的模式,禁止裸Create

4.3 组装工作负载创建请求的细节

创建 Deployment 的请求体,除了常规字段,还要重点处理两件事:rancher的项目选择和 Rancher UI 展示用的元数据。

Rancher 的项目是一个逻辑分组,Rancher API 在创建命名空间时一般要求传projectId,格式是c-xxxxx:p-xxxxx。如果漏掉,资源也会创建成功,但你在 Rancher UI 里会发现它跑到一个叫system项目下的奇怪空间去了,后续管理会比较混乱。所以创建 namespace 的时候我一定显式带上 projectId:

{ "type": "namespace", "name": "app-tenant-01", "projectId": "c-abcde:p-12345" }

工作负载的 annotation 也很重要,有些是 Rancher 自己加的。比如field.cattle.io/creatorId会影响 UI 显示创建人。聚合服务内部使用一个专用服务账号,所以我把它固定成field.cattle.io/creatorId: "paas-platform",这样 UI 上能清楚区分哪些资源是平台创建的,避免租户以为是自己的操作出了岔子来找你排查。

4.4 调用 Rancher API 的认证细节

聚合服务调用 Rancher API,我推荐用 Rancher 的API Key,而不是 Kubeconfig 的 Service Account Token。原因是 API Key 可以直接用Authorization: Bearer头,而且能关联到 Rancher 的本地用户或权限模板,方便审计和吊销。

import ( normanClient "github.com/rancher/norman/types" client "github.com/rancher/rancher/pkg/client/generated/cluster/v2" ) func NewRancherClient(server, token string) *client.Client { opts := &client.ClientOptions{ URL: server, Token: token, Timeout: 30 * time.Second, RetryConfig: &normanClient.RetryConfig{ Max: 3, WaitMin: time.Second, WaitMax: 5 * time.Second, }, } c, err := client.NewClient(opts) if err != nil { panic(err) } return c }

这里给RetryConfig的配置非常关键。不加重试的话,Rancher Server 一次撑不住大量并发创建请求,返回 5xx 就直接失败,体验非常糟糕。但加了重试也要小心:GET 和幂等操作可以放心重试,非幂等 POST 一定要配合我上面说的 ensure 逻辑,不然重试会制造重复资源

5. 把权限和路由规则理清:自定义 API 如何继承 Rancher 的鉴权体系

5.1 接口鉴权策略:双 Token 还是单 Token

自定义 API 上线后,最容易被挑战的就是安全问题。我当时面临一个选择:上游平台是直接拿用户的 Rancher API Key 来调我的聚合服务,还是聚合服务自身用一个固定的平台账号,再做一层业务鉴权?

两种方案我都试过,最终选了后者:聚合服务用固定平台账号访问 Rancher,业务鉴权在聚合服务内部通过租户维度完成

理由是:如果每个上游请求都带一个 Rancher API Key,聚合服务就得维护一套密钥中间态,而且上游用户可能根本没有 Rancher 账号(因为上游平台有自己的一套账号体系)。反过来,用一个高权限的 Rancher 平台账号,虽然在 Rancher 侧看不到具体是哪个业务用户做的创建,但没关系,我们可以在ApplicationOrder里记录tenantId和请求来源 userId,审计时以业务日志为准。

这里要提醒一句,如果你想做的接口是给 Rancher UI 内部用户用的,那就应该用第一套方案,让 Rancher 权限模型直接决定谁能调用。如果接口是给外部业务平台对接用的,就别去硬套 Rancher 的账号体系,独立鉴权更干净。

5.2 路由和命名空间权限的边界控制

聚合服务里有一个挺容易忽略的点:Rancher 的 project 是权限边界,namespace 是资源隔离边界。自定义 API 创建资源时,必须严格按租户映射关系把资源放进正确的 project/namespace。

我维护了一张租户路由表,大致结构是:

租户ID目标 Project目标 namespaceBase允许创建的工作负载类型
T001c-abcde:p-10001ns-tenant-t001Deployment, StatefulSet
T002c-abcde:p-10002ns-tenant-t002Deployment
T003c-fghij:p-20001ns-tenant-t003/*Deployment, DaemonSet

聚合服务在拿到请求后,第一步不是调 Rancher,而是查这张路由表确认租户有没有权限、有没有配额。这个逻辑一定不能放在崩溃恢复的代码路径之后,必须在入口处就拦截,否则非法请求可能已经在资源创建了一半才被发现。

5.3 Rancher 项目 RBAC 的映射关系

我在开发过程中曾经踩过一个权限相关的坑:聚合服务用平台账号在 project A 下创建 namespace,但在 project B 下创建工作负载,结果 Rancher 返回 403。排查半天才发现,Rancher 的项目角色绑定是区分项目的,一个用户在一个项目下的权限,不会自动传导到另一个项目。所以平台账号必须在所有需要管理的项目下都有项目成员角色,权限级别至少到"项目成员",能创建 namespace 和 workload。

解决办法不是一个个项目去 UI 上添加,而是先把平台账号提升为集群成员或全局管理员,再由聚合服务在代码里限定它的操作范围。虽然账号权限大,但配合业务层的租户路由表,实际能操作的范围仍被严格约束在已配置的 tenant 路由内。

6. 验证与坑位复盘:上线前必须搞定的几个关键测试

6.1 本地开发环境的搭建

Rancher 二次开发最费时间的是本地联调环境。我是用 Rancher Desktop 起了一个单节点的 K8s 集群,然后在里面装 Rancher Server。实测下来,有一个经验供参考:如果只是调试 API 层,不需要装完整 Rancher Server,可以直接用rancher/rancher镜像以--embedded模式跑,把所有依赖都装在一个 K3s 集群里,节省很多资源。

启动命令大致如下:

docker run -d --name rancher-server \ --privileged \ -p 8443:443 \ -e CATTLE_BOOTSTRAP_PASSWORD=admin123456 \ rancher/rancher:v2.8.5

等容器日志里出现Bootstrap Password说明启动完成,然后通过 web 界面设置 admin 密码,再生成一个全局 API Key 给聚合服务用。

6.2 常见错误与排查链路

在实际联调过程中,我总结了三个出现频率最高的问题:

问题一:调用自定义 CRD 的 API 返回 404

排查链路:先确认 CRD 已经在集群中创建,再用 Rancher API 的 schema 列表确认注册是否完成。如果 CRD 存在但/v1/下没有对应 schema,90% 的情况是 Rancher 的 Steve 缓存还没刷新,重启cattle-cluster-agent即可。还有一个偏门原因:CRD 的 group 名与已有 Rancher schema 冲突,比如用了management.cattle.io这种保留 group,是绝对不会注册成功的。

问题二:创建 namespace 时传了 projectId 但返回 422

这个通常是 projectId 格式错误。Rancher 的 projectId 必须是集群ID:项目ID的组合,不能只传项目 ID 那一段。这个错误信息在 Rancher API 里的描述是很模糊的,只报invalid projectId,不看源码根本不知道要拼集群 ID。如果你遇到 422,先用 GET/v3/projects拿到正确的 projectId 再组装。

问题三:创建 Deployment 后,Rancher UI 里看不到

这个多半是 namespace 与项目没正确关联。Rancher UI 的默认视图是按项目聚合的,如果 namespace 是在创建后才被移动到项目下,工作负载虽然存在,但出现在"未分配"或者是系统项目里。正确的做法是创建 namespace 的时候就把projectId传上,然后再创建工作负载。

6.3 自动化测试的一个方法

接口上线前,我建议至少写一套针对幂等逻辑的自动化测试。方法其实很取巧:同一个创建请求连续发两次,断言第二次不报错,且最终资源只有一个。这个测试比任何单元测试都能更快暴露"重试会导致重复创建"的问题。

func TestCreateApplicationIdempotent(t *testing.T) { // 第一次创建 resp, err := createApplication(ctx, validReq) require.NoError(t, err) // 模拟超时重试,再次提交完全相同的请求 resp2, err := createApplication(ctx, validReq) require.NoError(t, err) // 资源唯一性断言 workloads, _ := listWorkloads(ctx, resp2.Namespace, resp2.AppName) assert.Equal(t, 1, len(workloads.Items)) }

这个测试在本地会命中一个常见的实现缺陷:如果你是用随机后缀生成工作负载名称,第二次请求会生成一个完全不同的新名字,然后资源就变成两个。用确定性命名之后,这个测试才真正有约束力。

7. 线上部署时补的几个细节:探活、审计与优雅退出

7.1 服务健康检查与 Rancher API 抖动

聚合服务部署后,探活路径我直接绑定到了 Rancher API 的连通性上:/healthz里除了检查自身进程状态,还会用一个低耗时 GET 请求探一下 Rancher API。这个设计起初是为了方便排障,后来发现它还能提前暴露网络分区。但如果 Rancher Server 重启或升级,探活失败会导致 Pod 被频繁重启。所以我给健康检查接口单独加了缓存:30 秒内第一次探测失败时,不立即标记不健康,而是继续用上一次成功的结果做短暂兜底

7.2 审计日志的字段规范

因为聚合服务是平台侧对接的唯一入口,必须承担起审计职责。每条创建请求我都会在日志里记录:请求 ID、上游用户 ID、租户 ID、目标集群、目标 namespace、结果摘要,以及耗时。这里有一个细节值得分享:一定要在创建ApplicationOrder之后立刻把status.phasestatus.message写入审计日志,而不是在接口返回之后才统一记录,因为接口返回阶段可能已经被超时中断,日志就丢了。

我个人建议给每一个上游请求生成一个requestID贯穿始终:

type AuditEntry struct { RequestID string UserID string TenantID string ClusterID string Namespace string Action string TargetKind string TargetName string Result string DurationMs int64 Error string CreatedAt time.Time }

7.3 优雅退出和任务恢复

最后说一下服务重启的场景。聚合服务在把ApplicationOrder持久化之后、还没完成 Rancher API 调用之前,进程可能挂掉。所以我实现了一个worker启动时扫描ProvisioningPending状态的申请单,重新走续跑逻辑。续跑逻辑和首次创建逻辑共用同一个ensureNamespace / ensureWorkload函数,这样天然避免重复创建。

实测下来这个机制非常重要。我们线上有一次 Rancher 升级导致集群连接中断,聚合服务里积压了十几个Provisioning状态的申请单,恢复后 worker 全部自动续跑完成,业务方没有感知到任何异常。


做成这整套自定义创建 API 之后我最大的一点体会是:别急着写代码,先把 Rancher 的 Schema 与项目权限模型吃透。很多人第一次做 Rancher 二开,潜意识里把它当成一个普通的 HTTP 服务来处理,结果调起 API 来到处踩坑——资源创建成功但 UI 看不到、项目归属不对、权限莫名其妙 403,其实根子都在没有理解 Rancher 背后的资源抽象与权限设计。最后再分享一个压箱底的小技巧:联调用curl调试 Rancher API 时,响应里如果带了links字段,它的self链接就是那个资源的规范化访问路径,拿它去和 UI 里的资源详情对照,排查资源归属问题会快很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询