ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Rancher二次开发实战:自定义API从CRD到聚合服务的完整落地

Rancher二次开发实战:自定义API从CRD到聚合服务的完整落地 接到一个挺典型的二开需求要给 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 框架叫NormanRancher 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 Schema3.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。但是我实测下来通常这个词背后有几个前提CRD 必须能被当前 Rancher Server 访问到的集群里发现。如果 Rancher 开启了embedded模式也就是内置 K3s 集群跑 Rancher Server注册 CRD 建议直接打到 local 集群。注册后可能需要等片刻/v1/paas.internal.example.com/applicationorders才能访问。如果你急着要它立刻生效可以用下面的命令手动触发避过缓存等待时间# 找到 Rancher Server 的 pod然后重启 cattle-cluster-agent 或直接触发 schema 刷新 kubectl -n cattle-system rollout restart deployment/cattle-cluster-agentSchema 就绪后用 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 基础好。整体调用数据流是这样的上游调用POST /v1beta1/applications请求体包含应用名、租户 ID、副本数、镜像地址、路由规则等。聚合服务先做业务层校验租户白名单、资源配额预估、命名规范。服务生成一个ApplicationOrderCR状态Pending写入控制平面 namespace。服务调 Rancher API 创建 namespace如果不存在、创建工作负载、创建 Service、创建 Ingress。每完成一个子步骤更新ApplicationOrder的status.rancherResources。全部成功后状态置为Ready返回给上游一个包含资源清单摘要的响应。这里最关键的一点是聚合服务永远以ApplicationOrder状态为准。如果中途崩溃服务重启后扫描Pending或Provisioning状态的申请单按幂等逻辑继续执行或回滚。上游发起的是一次 HTTP 请求但后端会通过这个状态对象保证最终一致。4.2 创建 Rancher 资源的幂等控制聚合服务调 Rancher 原生 API 的时候最大的隐患是网络抖动导致请求发出去了但响应超时然后你重试又创建了一遍。Rancher API 本身不是天然幂等的工作负载同名会报冲突但如果第一次实际成功、只是响应丢了第二次可能返回的就不是冲突而是 409 之外的错误。我的兜底方案分两层第一层所有被创建的资源名字都用有规则的确定性命名。比如工作负载名称 app-{sha256(appNametenantId)[: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, StatefulSetT002c-abcde:p-10002ns-tenant-t002DeploymentT003c-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_PASSWORDadmin123456 \ rancher/rancher:v2.8.5等容器日志里出现Bootstrap Password说明启动完成然后通过 web 界面设置 admin 密码再生成一个全局 API Key 给聚合服务用。6.2 常见错误与排查链路在实际联调过程中我总结了三个出现频率最高的问题问题一调用自定义 CRD 的 API 返回 404排查链路先确认 CRD 已经在集群中创建再用 Rancher API 的 schema 列表确认注册是否完成。如果 CRD 存在但/v1/下没有对应 schema90% 的情况是 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.phase和status.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启动时扫描Provisioning和Pending状态的申请单重新走续跑逻辑。续跑逻辑和首次创建逻辑共用同一个ensureNamespace / ensureWorkload函数这样天然避免重复创建。实测下来这个机制非常重要。我们线上有一次 Rancher 升级导致集群连接中断聚合服务里积压了十几个Provisioning状态的申请单恢复后 worker 全部自动续跑完成业务方没有感知到任何异常。做成这整套自定义创建 API 之后我最大的一点体会是别急着写代码先把 Rancher 的 Schema 与项目权限模型吃透。很多人第一次做 Rancher 二开潜意识里把它当成一个普通的 HTTP 服务来处理结果调起 API 来到处踩坑——资源创建成功但 UI 看不到、项目归属不对、权限莫名其妙 403其实根子都在没有理解 Rancher 背后的资源抽象与权限设计。最后再分享一个压箱底的小技巧联调用curl调试 Rancher API 时响应里如果带了links字段它的self链接就是那个资源的规范化访问路径拿它去和 UI 里的资源详情对照排查资源归属问题会快很多。
返回列表