首页
/ 基于 Kubernetes cloud-provider 库编写外部 cloud-controller-manager(CCM)的官方指南与示例解析

基于 Kubernetes cloud-provider 库编写外部 cloud-controller-manager(CCM)的官方指南与示例解析

2026-09-07 22:32:02作者:滑思眉Philip

导读

本文围绕 Kubernetes 仓库中 staging/src/k8s.io/cloud-provider/sample 目录展开,它是 Kubernetes 官方(staging 库)为所有云提供商准备的"如何从 1.20 开始正确构建自己的 cloud-controller-manager(CCM)"最小可运行示例。读完本文,你将掌握:云提供商外部化 CCM 的目录与仓库组织方式、基于 basic_main.go 编写自有 main.go 的完整步骤与关键函数拆解、在不复用 cmd/cloud-controller-manager 的前提下增删控制器(如 NodeIPAM)的扩展机制,以及必须规避的错误做法。

背景:为什么 1.20 起云提供商必须外部化 CCM

sample 目录的定位

cloud-controller-manager 在 Kubernetes 中负责运行所有依赖云厂商能力的控制器循环,例如节点初始化/生命周期、Service 负载均衡器、节点路由等。历史上云厂商往往直接拷贝或 vendor 进 k8s.io/kubernetes/cmd/cloud-controller-manager,这导致每个厂商维护一份与控制面同构的主程序,升级与同步成本极高。

sample 目录的 README 明确其目的:

Begin with 1.20, all cloud providers should not copy over or vendor in k8s.io/kubernetes/cmd/cloud-controller-manager. Inside this directory, some sample code will be provided to demonstrate how cloud providers should leverage cloud-controller-manager.

也就是说,从 Kubernetes 1.20 起,cmd/cloud-controller-manager 不再是被拷贝复用的"模板",而是仓库内部的一个示例实现;云厂商应改为直接依赖 staging/src/k8s.io/cloud-provider 这个发布到 k8s.io/cloud-provider 的独立模块。该模块定义了 cloud provider 接口与"把云提供商实现注入 Kubernetes"的初始化机制(见该模块 README 中 "This repository defines the cloud-provider interface and mechanism to initialize a cloud-provider implementation into Kubernetes")。

staging 库与只读约定

staging/src/k8s.io/cloud-provider 是一个发布前镜像(staged repository):代码真正的修改提交在 k8s.io/kubernetes,随后被自动同步成独立的 k8s.io/cloud-provider 外部仓库。因此 sample 目录的自述文件反复强调一条铁律:不要直接在 vendor/k8s.io/cloud-provider/sample 下改动,那里的内容完全由 k8s.io/kubernetes/staging/src/k8s.io/cloud-provider/sample 驱动生成;云厂商的个性化逻辑应写在自己仓库中。

官方推荐的实施步骤

sample 的 README 给出了一份高度凝练的四步清单,结合仓库代码可以还原出完整的落地动作:

  1. 把外部仓库放在 k8s.io 命名空间下,例如 k8s.io/cloud-provider-<provider>。命名空间约定是为了与 k8s.io/cloud-provider 等模块保持一致的 import 路径习惯。

  2. 在外部仓库的 CCM 目录中创建 main.go,以 basic_main.go 为最小工作样例。这份文件本身就是为"被每个云厂商复制到自己仓库后改写"而准备的——文件头部注释直接写明:"This file should be written by each cloud provider."

  3. 如果你有在 CCM 内增删控制器的需求,不要自己发明注册机制,应参考 cmd/cloud-controller-manager/main.go,其中演示了如何对默认控制器集合做删除与追加(详见下文"控制器扩展机制")。cmd/cloud-controller-manager/README.md 也印证了分工:不需要增删控制器的厂商看 k8s.io/cloud-provider/sample;需要增删控制器的厂商看 cmd/cloud-controller-manager 的示例。

  4. 从你的外部仓库构建并发布 CCM 二进制。README 特别提示:对于存量云提供商,自 1.31 起已不再提供从 k8s.io/legacy-cloud-providers/<provider> 导入 legacy 内嵌实现这一选项,因此长期路径必须是自建外部 provider。

最小工作示例精解:basic_main.go

整个 sample 目录只有两个文件:README 与 basic_main.go。后者约 98 行,却完整串联了 CCM 从"初始化配置"到"装配控制器"再到"启动命令"的全部关键路径,下面逐段拆解。

1. import 集合:模块依赖全景

basic_main.go 的 import 段落(L23-L40)本身就是一个"官方推荐依赖清单",主要包括:

  • k8s.io/apimachinery/pkg/util/wait:提供 wait.NeverStop 这类永不停止的信号通道;
  • k8s.io/cloud-provider:cloud provider 接口与注册表(cloudprovider.InitCloudProvider 所在包);
  • k8s.io/cloud-provider/app:CCM 命令与控制器注册框架(NewCloudControllerManagerCommandDefaultInitFuncConstructors);
  • k8s.io/cloud-provider/app/configCompletedConfig 类型,承载最终生效的组件配置;
  • k8s.io/cloud-provider/names:CCM 控制器名称常量与别名映射;
  • k8s.io/cloud-provider/optionsNewCloudControllerManagerOptions 提供的完整命令行选项;
  • k8s.io/component-base/clicli/flag:标准 CLI 运行框架与 NamedFlagSets
  • 一组带下划线(blank import)的注册副作用导入
    • _ "k8s.io/component-base/logs/json/register"——注册可选的 JSON 日志格式;
    • _ "k8s.io/component-base/metrics/prometheus/clientgo"——加载全部 prometheus client-go 插件;
    • _ "k8s.io/component-base/metrics/prometheus/version"——注册版本指标。

这些 blank import 解释了为什么云厂商 main.go 只要"抄"这些导入,就能免费获得与社区 CCM 一致的日志与指标行为。

2. main():装配命令并运行

核心逻辑只有四步(L42-L52):

func main() {
	ccmOptions, err := options.NewCloudControllerManagerOptions()
	if err != nil {
		klog.Fatalf("unable to initialize command options: %v", err)
	}

	fss := cliflag.NamedFlagSets{}
	command := app.NewCloudControllerManagerCommand(ccmOptions, cloudInitializer, controllerInitializers(), names.CCMControllerAliases(), fss, wait.NeverStop)
	code := cli.Run(command)
	os.Exit(code)
}
  • NewCloudControllerManagerOptions():构造带默认值的全套 CCM 选项(含 --cloud-provider--cloud-config--allow-untagged-cloud 等);
  • cliflag.NamedFlagSets{}:承载命名分组的 flag 集合,供第三方控制器(如 NodeIPAM)追加自己的命令行参数;
  • app.NewCloudControllerManagerCommand(...):这是装配的核心 API,参数依次为:选项对象、云提供商初始化函数 cloudInitializer控制器初始化器集合 controllerInitializers()控制器别名表 names.CCMControllerAliases()、自定义 flag 集合,以及 stop 通道;
  • cli.Run(command) + os.Exit(code):统一走 component-base 的 CLI 运行框架,负责 flag 解析、启动、优雅退出与错误码传递。

fsscontrollerAliases 两个参数正是为"增删控制器"场景预留的扩展点:第三方控制器可把自己的 flag 注册进 fss,并可把自己的名字挂到别名表上。

3. controllerInitializers():统一替换各控制器的 ClientName

controllerInitializers()app.DefaultInitFuncConstructors(社区 CCM 的默认控制器集合)为起点,逐一把四个内置控制器的 ClientName 改写成云厂商自己的名字:

if constructor, ok := controllerInitializers[names.CloudNodeController]; ok {
	constructor.InitContext.ClientName = "mycloud-external-cloud-node-controller"
	controllerInitializers[names.CloudNodeController] = constructor
}

CloudNodeLifecycleControllerServiceLBControllerNodeRouteController 同样处理。函数上方的注释指出关键后果:

If custom ClientNames are used, as below, then the controller will not use the API server bootstrapped RBAC, and instead will require it to be installed separately.

即:一旦自定义 ClientName,控制器便不再使用 API server 自动 bootstrap 的 RBAC,你必须在集群中单独安装与这些名字匹配的 RBAC(Role/ClusterRole/RoleBinding),否则控制器会因为没有权限而无法工作。这是厂商落地时最容易踩的坑之一。

4. cloudInitializer():加载云提供商并强制 ClusterID

cloudInitializer()NewCloudControllerManagerCommand 要求的 cloudprovider.Interface 工厂,逻辑如下:

  1. config.ComponentConfig.KubeCloudShared.CloudProvider 读取 NameCloudConfigFile
  2. 调用 cloudprovider.InitCloudProvider(name, cloudConfigFile) 真正加载实现——该函数按注册表(由厂商通过 cloudprovider.RegisterCloudProvider() 注入)解析名字并读取配置文件;
  3. 对返回结果做非空保护;
  4. 执行 ClusterID 强校验:若 !cloud.HasClusterID(),则当 KubeCloudShared.AllowUntaggedCloud 为真时仅打印警告("detected a cluster without a ClusterID. A ClusterID will be required in the future…"),否则直接 klog.Fatalf 终止进程,提示可通过 --allow-untagged-cloud 选项绕过。

这意味着:新写的云提供商必须实现 HasClusterID(),并引导用户为集群打上 ClusterID 标签;--allow-untagged-cloud 只是一个过渡逃生门。CCM 主程序 cmd/cloud-controller-manager/main.go#L93-L99 中复现了完全一致的逻辑,可作为对照。

需要增删控制器时:参考 cmd/cloud-controller-manager 的扩展机制

如果你的云厂商不需要默认集合之外/之内的控制器,basic_main.go 已经够用。若需要(例如想要一个 cloud 化的 IPAM 分配器),cmd/cloud-controller-manager/main.go 给出了完整的增删示范。

删除一个默认控制器

在构造 command 之前直接对初始化器集合执行 delete 即可(代码中以注释形式给出示例,cmd/cloud-controller-manager/main.go#L54-L56):

controllerInitializers := app.DefaultInitFuncConstructors
// e.g. remove the cloud-node-lifecycle controller which current cloud provider does not need.
// delete(controllerInitializers, "cloud-node-lifecycle")

被删除项对应的 key 来自控制器名常量(删除场景下直接用字符串别名即可)。

添加一个外部控制器:NodeIPAM 完整示例

添加流程以 NodeIPAM 控制器为例(cmd/cloud-controller-manager/main.go#L62-L75):

  1. 构造自己的控制器结构体并把其 options 的配置指针接好;
  2. options.AddFlags(fss.FlagSet(kcmnames.NodeIpamController)) 把新增控制器的命令行参数注册到命名 flag 组(kcmnames 来自 k8s.io/kubernetes/cmd/kube-controller-manager/names);
  3. app.ControllerInitFuncConstructor{ InitContext: app.ControllerInitContext{ ClientName: "node-controller" }, Constructor: <你的启动包装函数> } 形式写入 controllerInitializers[kcmnames.NodeIpamController]。这里 ClientName 复用 "node-controller",注释说明它是 node、node lifecycle、node ipam 等所有 node 系控制器的共享身份;
  4. 追加别名 controllerAliases["nodeipam"] = kcmnames.NodeIpamController,让用户在 flag/配置中可用 --controllers=nodeipam 之类的短名引用。

Constructor 的签名要求是:接收 InitContext、CCM 的 *cloudcontrollerconfig.CompletedConfigcloudprovider.Interface,返回一个内部 app.InitFunccmd/cloud-controller-manager/nodeipamcontroller.goStartNodeIpamControllerWrapper 即这一模式的落地实例,它还展示了第三方控制器的典型"前置检查"模式,例如:

  • 仅当 KubeCloudShared.AllocateNodeCIDRs 为 true 时才真正启动(否则静默返回不启用);
  • CIDRAllocatorTypeCloudAllocator 但 cloud provider 为空,则直接报错 "--cidr-allocator-type is set to 'CloudAllocator' but cloud provider is not configured"
  • 解析 ClusterCIDR/ServiceCIDR,做单栈/双栈合法性校验(超过 2 个 CIDR、非双栈却传多个 CIDR 等情况都会失败);
  • 依据双栈与否决定 --node-cidr-mask-size[-ipv4/-ipv6] 的默认与约束(默认 IPv4 掩码 24、IPv6 掩码 64,双栈下禁止使用旧式 --node-cidr-mask-size 单一标志等)。

这些守卫逻辑是"外部控制器能否在 CCM 框架内正确自检"的范例。

控制器名称与别名速查

默认控制器名常量与 CCM 别名定义在 staging/src/k8s.io/cloud-provider/names/controller_names.go

别名(用户可见) 控制器常量(代码内) ClientName(默认上下文)
cloud-node names.CloudNodeController 可由厂商在 controllerInitializers() 中改写
service names.ServiceLBController 同上
route names.NodeRouteController 同上
cloud-node-lifecycle names.CloudNodeLifecycleController 同上

main() 中把 names.CCMControllerAliases() 作为别名表传入 NewCloudControllerManagerCommand,即可让这些短别名在命令行/配置层面生效。自定义 ClientName 时请牢记"独立安装 RBAC"的要求。

必须避免的错误做法(Things you should NOT do)

sample 目录与 cmd/cloud-controller-manager/README.md 都列出了严格的负面清单,共三条:

  1. 不要把 k8s.io/cmd/cloud-controller-manager vendor 进你的仓库。它是仓库内部示例,不是对外 API;正确的依赖对象是 k8s.io/cloud-provider
  2. 不要直接改动本仓库 vendor/k8s.io/cloud-provider/sample。它由 staging/src/k8s.io/cloud-provider/sample 同步驱动,属于只读镜像,改动会丢失并破坏发布流程。同理适用于整个 vendor/k8s.io/cloud-provider(见 staging/src/k8s.io/cloud-provider/README.md 的 "Things you should NOT do")。
  3. 不要在 sample 文件里做任何特定云厂商的改动。示例保持 provider-agnostic,厂商逻辑应写在自己的 k8s.io/cloud-provider-<provider> 仓库中。

如何将云提供商实现注入外部 main.go

cmd/cloud-controller-manager/providers.go 是一个刻意留空的参考文件,它演示了 out-of-tree 的注入思路(文件注释明确说 "Importing all in-tree cloud-providers is not required when implementing an out-of-tree cloud-provider"):

  1. 在你的 provider 包(例如厂商仓库中的实现包)里通过 init() 调用 cloudprovider.RegisterCloudProvider() 完成注册;
  2. 在厂商自己 main.go 中以 blank import 方式引入该包(示例注释 import _ "k8s.io/legacy-cloud-providers/gce"),使注册副作用生效;
  3. 此后 cloudprovider.InitCloudProvider(name, file) 就能按名字找到你的实现。

这与 basic_main.go 中 cloudInitializer 使用 cloudprovider.InitCloudProvider 的调用形成完整闭环,也解释了"外部仓库自行构建二进制 + 自行注册实现"为何能替代旧的 vendor 拷贝模式。

小结

综合来看,sample 目录提供了一条从 Kubernetes 1.20 起官方认可的最小落地路径:把 main.go 建立在 k8s.io/cloud-provideroptionsappnames 组合之上,通过 cloudInitializer 注入自研 provider 并处理好 ClusterID 与 RBAC,即可产出独立的 CCM 二进制;需要更多控制权时,再参照 cmd/cloud-controller-managerDefaultInitFuncConstructors 增删并注册额外 flag。自 1.31 起 legacy provider 导入通道关闭后,这条"外部化"路径不再是可选项,而是所有云厂商接入 Kubernetes 云能力的标准姿势。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388