13.2. 平台工程与多集群:Crossplane、Backstage 与 vCluster

平台工程不是「给 Kubernetes 套一个 UI」。它的目标是把组织里重复了上百次的部署、申请、排障动作用少数几条铺好的路收敛起来,让应用团队自助完成,平台团队只在路径之外介入。本章按「基础设施编排 → 开发者入口 → 多租户 → 多集群与边缘 → 成本」这条链路,把 Crossplane、Backstage、vCluster 这些项目放回它们各自该在的位置。

层级:

L3 · 平台

难度:

进阶

预计:

80 分钟

前置:

CRD 与 Operator、Karmada、RBAC

你将学到

  • 平台工程的边界:Golden Path 该覆盖什么、哪些问题不该由平台解决

  • Crossplane、Cluster API、Terraform / OpenTofu、Pulumi 在编排链路里的分工

  • Backstage、KubeVela、Radius 三种「开发者入口」的抽象层级差异

  • vCluster、Capsule、HNC 三档隔离强度,以及选择依据

  • Karmada、Fleet、Liqo、KubeEdge、OpenYurt 在多集群与边缘场景的定位

  • 用 OpenCost / Kubecost 把账单摊到 namespace 与工作负载

  • 一个 vCluster 最小实验:验证虚拟集群到底隔离了什么

13.2.1. 平台工程:把「能部署」变成「铺好的路」

内部开发者平台(IDP)是一个产品,用户是应用团队。它的验收指标不是「接入了多少集群」,而是新服务从创建到上线的时间、自助率、以及平台团队收到的工单量。判断一条路径是否值得铺,看四个默认值:默认安全(镜像扫描、最小权限、NetworkPolicy 开箱即有)、默认可观测(日志、指标、追踪不用申请)、默认可回滚(每次变更都有版本与回退路径)、有逃生舱(Golden Path 覆盖不到的合法需求能显式申请例外,而不是被逼去绕开平台)。

Golden Path 是「铺好的路」,不是「唯一的路」。把路修得足够顺,让 80% 的常规需求走上去;剩下 20% 保留人工评审通道。反过来,如果平台把 Kubernetes 藏得连 kubectl describe 都看不到,故障时应用团队只能等平台团队,这就把分布式系统的排障成本变成了排队成本。

角色

负责

交付物

基础设施团队

集群、网络、云账号、节点池

Cluster API 清单、Terraform 模块

平台团队

抽象 API、模板、门禁、成本模型

XRD / Composition、Backstage 模板、策略

应用团队

业务镜像、副本与资源声明、SLO

一份 Claim 或 Application 清单

警告

三个常见的反模式

  1. 门户只做 YAML 表单:把 kubectl apply 换成网页提交,抽象没变,平台团队反而多维护一个前端。

  2. 平台团队变成工单队列:所有变更都要平台审批,自助率接近零,Golden Path 名存实亡。

  3. 抽象泄漏无人兜底:封装层出错时没有日志、没有事件、没有文档,用户只能猜。

13.2.2. 基础设施编排:四种定位

这五个工具经常被放在一张表里比较,但它们回答的是不同问题。

工具

状态存哪

收敛方式

主要管理对象

Terraform

state 文件(本地或远端后端)

plan / apply,一次性执行

云账号、网络、托管服务

OpenTofu

同 Terraform(兼容 state 与 provider)

同上,MPL 许可

同上

Pulumi

state 后端

同上,用通用语言写

同上,可复用 SDK 与测试

Cluster API

etcd(CRD)

控制器持续调和

集群与机器本身

Crossplane

etcd(CRD)

控制器持续调和

云资源,以 K8s API 暴露

Terraform / OpenTofu / Pulumi 是「置备」工具:读取代码、对比 state、算出 diff、执行。漂移只在下一次 plan 时被发现,所以必须把 plan 放进 CI,并且给 state 加锁。OpenTofu 是 Terraform 改许可证后由 Linux Foundation 托管的分支,HCL、provider 生态与 state 格式保持兼容,迁移主要是换二进制与 registry 地址。Pulumi 的差异在于用 TypeScript、Go、Python 直接写逻辑,适合已经有工程化测试能力的团队。

Cluster API 管的是集群本身:Cluster 描述一个集群,MachineDeployment 描述一组机器,KubeadmControlPlane 描述控制平面,具体云资源由 infrastructure provider(CAPA、CAPV、CAPZ)创建。clusterctl 负责引导管理集群与迁移。它不负责应用放置,这件事交给 Karmada 或 Fleet。

Crossplane 把云资源变成 CRD,用 Kubernetes 的 RBAC、审计、GitOps 与调和循环管理它们。平台团队写 CompositeResourceDefinition(XRD)定义 API,写 Composition 声明这个 API 由哪些托管资源组成,应用团队只提交一个命名空间级的 Claim:

xrd-database.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata: {name: xdatabases.platform.example.com}
spec:
  group: platform.example.com
  names: {kind: XDatabase, plural: xdatabases}
  claimNames: {kind: Database, plural: databases}   # 应用团队看到的是 Database
  versions:
    - name: v1alpha1
      served: true
      referenceable: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                parameters:
                  type: object
                  properties:
                    size: {type: string, enum: [small, medium]}
                    region: {type: string}
                  required: [size, region]
claim-orders-db.yaml
apiVersion: platform.example.com/v1alpha1
kind: Database
metadata: {name: orders-db, namespace: team-a}
spec:
  parameters: {size: small, region: cn-north-1}
  compositionSelector: {matchLabels: {provider: aws}}

Composition 把 Database 映射成一组托管资源(RDSInstance、DBSubnetGroup、SecurityGroup),用 patches 把 small 翻译成实例规格,再由 provider-aws 真正调用云 API 并持续校正漂移。应用团队不需要知道 RDS 的参数名,也不需要云账号凭据——凭据在 ProviderConfig 里,权限由 RBAC 控制。

两者的分界线是引导悖论:用 Terraform 创建集群、安装 Crossplane、配置云凭据,再把集群内的云资源交给 Crossplane。不要让 Crossplane 管理它自己所在集群的引导资源,否则一次误删就无法自愈。

架构图

图:门户只负责入口与模板,真正的约束在 Golden Path API 与 GitOps 仓库上;放置与容灾交给多集群控制面。

13.2.3. 开发者门户:Backstage、KubeVela 与 Radius

Backstage 是 Spotify 开源的portal 框架,不是一个装完就能用的产品。核心是软件目录:catalog-info.yaml 声明 Component、System、Domain、API、Resource,由目录插件从 Git 拉取并建立关系图。软件模板(Scaffolder)用一套 action 把「新建服务」变成流水线:生成仓库、注册目录、开 CI、建 Argo CD Application。TechDocs 把 Markdown 渲染成文档站,Kubernetes 插件把集群里的 Deployment 与目录实体关联起来。代价是你要维护一个 React + Node 应用、跟进插件升级,并且持续治理目录数据——目录里有一半实体是孤儿,门户就失去了可信度。

KubeVela 走的是应用交付抽象:OAM 的 Application 由 Component 与 Trait 组成,平台团队用 Definition(CUE、Helm、Terraform)定义能力,用户在 Application 里组合;workflow 步骤负责多集群下发、灰度与人工确认,底层可以接 Karmada 或 OCM。它自带 VelaUX 作为界面,但定位是「交付层」,适合已经有 GitOps、只缺统一应用模型的环境。

Radius 以应用为中心:Application 下挂 Container、Route 与连接关系,底层运行时可以是 Kubernetes,也可以是 Azure Container Apps;环境能力用 recipe(Bicep / Terraform)描述,应用清单不绑定具体云资源。它建立在 Dapr 之上,适合「同一份应用定义要落到多种运行时」的场景。

项目

抽象层级

是否改应用清单

适合谁

Backstage

目录 + 模板 + 文档

否,新增 catalog-info.yaml

需要统一入口与文档的中大型组织

KubeVela

应用交付(OAM)

是,改用 Application

多集群交付、需要灰度与工作流

Radius

应用 + 依赖关系

是,改用 Radius 资源

跨 K8s 与托管运行时的团队

备注

门户不等于平台

没有 Golden Path、没有配额与门禁、没有成本数据的门户,只是更漂亮的 kubectl。先有 API 与策略,再有界面;顺序反了,门户会变成第二个工单系统。

13.2.4. 多租户:三档隔离强度

多租户要回答的是「两个团队共享一套基础设施时,谁能看见谁、谁能影响谁」。三档方案对应三种隔离成本。

方案

API 隔离

CRD 版本冲突

网络/配额

额外控制面

HNC

无,同一 apiserver

仍存在

配额可继承,网络需自建

无

Capsule

无,同一 apiserver

仍存在

Tenant 模板统一注入

无

vCluster

独立 apiserver + 存储

完全隔离

需 NetworkPolicy 补齐

每租户一套

HNC(Hierarchical Namespace Controller)给 namespace 建立父子关系,父级的 RBAC、NetworkPolicy、ResourceQuota 会传播到整棵子树,HierarchicalResourceQuota 让配额在子树内共享。它解决的是「一个团队十几个环境」的配置重复,不解决 API 隔离。

Capsule 引入 Tenant 对象:平台团队创建 Tenant 并指定 owner,租户只能在自己被分配的 namespace 前缀下建 namespace,配额、LimitRange、NetworkPolicy、节点选择器、可用 StorageClass 由 Tenant 统一注入。它仍然是共享 apiserver,所以 CRD 版本、准入 webhook、apiserver 故障半径都是共享的。几十个团队、彼此信任度中等时,这是性价比最高的一档。

vCluster 在宿主集群的一个 namespace 里跑一套独立的控制面(apiserver + kine/etcd),租户拿到的是自己的 kubeconfig。Pod、Service、PVC 由 syncer 同步到宿主 namespace,CRD、webhook、RBAC、版本则完全留在虚拟侧——这意味着租户可以自行安装 Operator、升级 Kubernetes 小版本,而不会污染邻居。代价是每套控制面的资源开销、syncer 的额外延迟,以及调试时要同时看两个集群。

警告

vCluster 不是安全边界

虚拟集群隔离的是 API 对象,不是内核。Pod 仍跑在宿主节点上,共享内核、容器运行时与网络插件。生产上必须叠加 NetworkPolicy、Pod Security Admission、租户专属节点池(配合污点与亲和),并对 hostPath、特权容器做准入限制。

13.2.5. 多集群与边缘:按「控制什么」分类

项目

控制对象

工作方式

典型场景

Karmada

应用放置与故障转移

联邦控制面 + 成员集群

多云、多地域容灾

Fleet

Git 仓库到大量集群的同步

GitRepo → Bundle → BundleDeployment

上千个边缘/零售站点

Liqo

集群间资源借用

对等 peering + 虚拟节点

突发容量、跨云借算力

KubeEdge

边缘节点与设备

cloudcore / edgecore,边缘自治

工厂、车载、离线站点

OpenYurt

边缘节点运维

yurthub 缓存 + 节点池

存量 K8s 延伸到边缘

Karmada 已在多集群编排:Karmada中展开:模板进 karmada-apiserver,PropagationPolicy 决定放置,Work 下发到成员集群,集群失联时按容忍时间重调度。

Fleet(Rancher)的模型更接近「GitOps 分发器」:注册下游集群后,一个 GitRepo 指向仓库路径,Fleet 把它渲染成 Bundle,再按 BundleDeployment 分发到匹配的集群,支持 Helm 与 Kustomize。它不提供跨集群重调度,集群失联时只是标记 NotReady,适合「每个站点一份配置、数量极大」的场景。

Liqo 建立集群间的对等关系,把远端集群的剩余容量映射成本地的虚拟节点,命名空间可以声明 offloading 策略,把 Pod 调度过去。它解决的是容量借用的临时需求,而不是长期的多集群治理。

KubeEdge 把 Kubernetes 延伸到边缘:cloudcore 在云侧,edgecore 在边缘节点,边缘断网时依靠本地缓存继续运行(边缘自治),EdgeMesh 处理边缘节点间的服务发现,设备通过 DMI 与 Mapper 接入。OpenYurt 走非侵入路线,用 yurthub 在节点上缓存 apiserver 请求实现边缘自治,用节点池与单元化(unit)控制升级与故障域,适合把存量集群直接延伸到边缘。

13.2.6. 成本治理:把账单摊到团队

OpenCost 从 Prometheus 读取 Pod 的 requests、节点价格与云厂商价目,按 namespace、controller、label 计算分摊,通过 /allocation API 输出。它区分三种成本:工作负载直接成本、集群 idle 成本、共享成本(如 DNS、监控),后两者需要显式分摊规则,否则各部门看到的数字加起来不等于账单。Kubecost 建立在同一套模型上,补了 UI、多集群聚合(Thanos)、预算告警与 rightsizing 建议。

三个容易踩的坑:

  • 按 requests 还是按 usage:按 requests 分摊稳定、可预测,但会奖励「申请大、用得少」;按 usage 更真实,却随采样波动。生产上通常以 requests 为主、usage 作为效率指标并列展示。

  • GPU 不可压缩:一张卡只能给一个 Pod,空闲也是全价。GPU 成本必须按整卡归属,再单独统计利用率。

  • 只看账单不看闭环:可见性 → 配额 → rightsizing(VPA 建议、Karpenter 选型)→ 竞价实例,四步缺一不可。只压 requests 会换来 OOM 与驱逐,成本转移到稳定性上。

备注

单位成本比总成本有用

总账单受业务增长影响,无法评价平台好坏。用「每千次请求成本」「每租户成本」「每训练任务 GPU·小时」这类单位成本,才能判断优化是否真的生效。

13.2.7. 动手实验:vCluster 最小实验

实验:在共享集群里开一个虚拟集群,验证隔离边界

目标:创建一个 vCluster,确认 CRD、RBAC、命名空间在虚拟侧独立,工作负载被同步到宿主 namespace,最后验证 sleep 与清理。

前置:一个可用的宿主集群(3 节点以上)、kubectl、helm、vcluster CLI;宿主有默认 StorageClass。

13.2.7.1. 步骤 1 · 创建虚拟集群

vcluster create team-a -n team-a --connect=false
kubectl -n team-a get pods
NAME                                     READY   STATUS    RESTARTS   AGE
team-a-0                                1/1     Running   0          40s
team-a-syncer-6d9c7f4b8-x2m4p           1/1     Running   0          40s
coredns-5d78c9869d-8kzqv                1/1     Running   0          35s

team-a-0 是虚拟控制面(apiserver + kine),team-a-syncer 负责把虚拟侧的对象同步到宿主。--connect=false 避免 CLI 直接改写当前 kubeconfig;需要时用 vcluster connect team-a -n team-a 拿独立上下文。

13.2.7.2. 步骤 2 · 在虚拟集群里创建工作负载

vcluster connect team-a -n team-a -- bash -c '
  kubectl create deployment web --image=nginx:1.27 --replicas=2
  kubectl get ns
  kubectl get crd | wc -l
'

虚拟侧只看到自己的 default、kube-system 等少量命名空间,CRD 数量也是个位数。回到宿主执行 kubectl -n team-a get pods,会看到两个带同步后缀的 nginx Pod,它们带 vcluster.loft.sh/managed-by=team-a 标签:

kubectl -n team-a get pods -l vcluster.loft.sh/managed-by=team-a

13.2.7.3. 步骤 3 · 验证 CRD 与 RBAC 隔离

vcluster connect team-a -n team-a -- bash -c '
  kubectl apply -f - <<EOF
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata: {name: demos.example.com}
spec:
  group: example.com
  names: {kind: Demo, plural: demos}
  scope: Namespaced
  versions:
    - name: v1
      served: true
      storage: true
      schema: {openAPIV3Schema: {type: object}}
EOF
  kubectl get crd demos.example.com
'
kubectl get crd demos.example.com   # 宿主侧应返回 NotFound

虚拟侧 Established 为 True,宿主侧查不到——这就是 vCluster 与传统 namespace 隔离最本质的区别:租户可以自行安装 Operator 与 CRD。

13.2.7.4. 步骤 4 · sleep 与清理

vcluster sleep team-a -n team-a
kubectl -n team-a get pods          # 控制面副本缩到 0,PVC 保留
vcluster wakeup team-a -n team-a
vcluster delete team-a -n team-a

delete 会连同宿主 namespace 里的同步对象一起清理;如果还挂了 PVC,先确认 StorageClass 的回收策略,避免留下孤儿卷。

13.2.7.5. 验证清单

  • vcluster list 显示 team-a 为 Running,kubectl -n team-a get pods 有控制面与 syncer

  • 虚拟侧 kubectl get ns 看不到宿主的业务命名空间

  • 虚拟侧创建的 CRD 在宿主侧 NotFound

  • 宿主的同步 Pod 带 vcluster.loft.sh/managed-by=team-a 标签

  • vcluster sleep 后控制面副本为 0,wakeup 后恢复

13.2.8. 常见问题与排错

现象

可能原因

处理方式

虚拟集群创建后 Pod 一直 Pending

宿主资源不足,或控制面 requests 超过节点余量

kubectl -n team-a describe pod team-a-0;给 vcluster 配 PriorityClass 与更低 requests

虚拟侧 Pod ContainerCreating

宿主没有对应的 StorageClass,或 PVC 无法置备

在虚拟侧指定宿主存在的 StorageClass;确认 syncer 有 PVC 同步权限

同步的工作负载在宿主看不到

syncer 未就绪,或 sync.toHost 被关闭

kubectl -n team-a logs deploy/team-a-syncer;检查 vcluster.yaml 的 sync 段

虚拟侧拉不到私有镜像

imagePullSecret 不会自动跨边界同步

在虚拟侧重建 Secret,或用 sync.toHost.secrets 显式同步

域名解析失败

虚拟侧 Service 的 DNS 名与宿主侧不同

跨边界访问用宿主侧的 synced Service 名;虚拟侧内部用短名

vcluster connect 后命令仍作用于宿主

kubeconfig 未切换,或 KUBECONFIG 环境变量覆盖

kubectl config current-context 确认;用 --print 输出后手动指定

13.2.9. 进阶挑战

  1. 用 Crossplane 的 provider-kubernetes 写一个 Composition,让 Database Claim 同时产出一个 Deployment 与一个 Secret,并观察删除 Claim 时的级联回收顺序。

  2. 在宿主集群上用 Capsule 建两个 Tenant,比较「Capsule 共享 apiserver」与「vCluster 独立 apiserver」在 CRD 版本冲突上的差异。

  3. 用 OpenCost 的 /allocation API 导出 team-a 一周的成本,按 vcluster.loft.sh/managed-by 标签做 showback 报表,并指出 idle 成本该如何分摊。

13.2.10. 小结

  • 平台工程交付的是 Golden Path 与自助 API,门户只是入口;衡量指标是上线时间与自助率。

  • Terraform / Pulumi 负责置备,Cluster API 负责集群生命周期,Crossplane 用 CRD + 调和把云资源纳入 Kubernetes,三者可组合而不互斥。

  • 隔离强度从 HNC 到 Capsule 再到 vCluster 递增,成本也递增;vCluster 隔离 API 对象,不隔离内核。

  • 多集群工具按「控制什么」区分:Karmada 管放置,Fleet 管分发,Liqo 借容量,KubeEdge 与 OpenYurt 管边缘。

  • 成本治理的关键是分摊规则与单位成本,而不是总账单。

13.2.11. 延伸阅读