Kubernetes Cloud Controller Manager 扩展机制实战:基于 cmd/cloud-controller-manager 示例构建可增删控制器的自研 CCM
导读
本文以 Kubernetes 官方仓库中 cmd/cloud-controller-manager/README.md 及其同目录示例代码为核心,系统讲解"云厂商如何在自研 Cloud Controller Manager(CCM)中复用官方扩展点、按需增删控制器"。你将掌握:自 Kubernetes 1.20 起正确的 CCM 构建姿势(不再 vendor 官方 cmd/cloud-controller-manager)、k8s.io/cloud-provider 提供的默认控制器集合与别名机制、向 CCM 注入全新控制器(示例为 NodeIPAM)的完整编码路径,以及删除不需要的默认控制器(如 cloud-node-lifecycle)的具体做法——最终可直接在自己的云厂商仓库中复刻这套模式。
为什么需要这份 CCM 示例目录
cmd/cloud-controller-manager 在 Kubernetes 代码库中并不是一个普通组件目录,而是一份"活的参考实现"。根据其 README.md 的说明:
This directory provides an example of how to leverage CCM extension mechanism.
从 Kubernetes 1.20 起,官方要求所有云厂商的 CCM 实现不得再复制(copy over)或 vendor k8s.io/kubernetes/cmd/cloud-controller-manager 的代码。官方给出的替代方案是把这块代码"抽出"为公共库(即 staging/src/k8s.io/cloud-provider,它经由发布流程镜像到 k8s.io/cloud-provider 独立模块),云厂商只需要在自己的外部仓库里引用这些库并实现自己的 main 入口即可。
该目录正是为"有扩展诉求"的云厂商准备的示范:演示如何在不改动官方代码的前提下,通过 CCM 的**控制器扩展机制(controller extension mechanism)**向控制器管理器注入(或移除)控制器。
目录里有什么:一份可对照的最小扩展骨架
cmd/cloud-controller-manager/ 目录由以下文件组成(完整文件清单可查看 目录列表):
| 文件 | 职责 |
|---|---|
| README.md | 说明目录目的、适用范围与禁止事项 |
| main.go | 主入口,示范删除/新增控制器的装配逻辑 |
| nodeipamcontroller.go | 一个完整的外部控制器(NodeIPAM)参考实现 |
| providers.go | 演示云厂商如何(通过空 import)注入自有 provider |
| OWNERS | 代码评审责任人清单 |
虽然 main.go 注释明确说明"当前文件演示了云厂商应如何复用 CCM,且使用了 fake 参数,请按需修改",但它完整覆盖了"默认控制器集合 → 删除 → 新增 → 组装 → 运行"的全过程,是目前仓库内最能说明扩展机制主干的代码。
三条铁律:Things you should NOT do
README 中"Things you should NOT do"一节直接给出三条红线,这也是所有云厂商接入时的纪律性约束:
- 不要 vendor
k8s.io/cmd/cloud-controller-manager(注:原文此处即指k8s.io/kubernetes/cmd/cloud-controller-manager,即本文所在目录)。vendor 意味着把官方示例当作你的代码基线,后续官方演进时你将被迫持续同步甚至分叉。 - 不要直接修改本仓库中
k8s.io/cmd/cloud-controller-manager下的任何内容。该目录只读地作为参考与示例存在,任何云厂商专属逻辑都不应反向写回。 - 不要在这里做具体的云厂商(cloud provider)改造。示例里用的是 fake 参数与占位结构,具体 provider 行为应全部落在外部队列中。
配套的 staging/src/k8s.io/cloud-provider/sample/README.md 还补充了两点工程约束:你的外部仓库应当位于 k8s.io 命名空间下(如 k8s.io/cloud-provider-<provider>),并且从 1.31 起,既有云厂商也无法再从 k8s.io/legacy-cloud-provider/<provider> 导入 in-tree 遗留 provider——进一步推动所有云厂商走向 out-of-tree。
先判断分支:何时用 sample,何时用本示例
这是 README 中最关键的"决策分叉",决定了你的工程走哪条路径:
分支 A:没有增删控制器的需求 → 使用 k8s.io/cloud-provider/sample
如果你只需要启动官方默认提供的那几个 CCM 控制器、没有任何增删定制,不要参考本目录,而应直接以 staging/src/k8s.io/cloud-provider/sample/basic_main.go 为模板。该文件是一个"最小可运行(minimum working)"的 CCM 主程序,它的 main() 只有四步:
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)
}
它与本目录 main.go 的区别一目了然:不向 controllerInitializers 做任何增删,只把默认集合原样传回(其 controllerInitializers() 内部还可对默认控制器的 InitContext.ClientName 做自定义,例如把 CloudNode 控制器的客户端标识改为 "mycloud-external-cloud-node-controller"——此时 API Server 自举的 RBAC 将不再适用,需要另行安装对应 RBAC,见 basic_main.go 注释)。
分支 B:需要新增/删除控制器 → 参考本目录示例
若你有诸如"去掉不需要的 cloud-node-lifecycle""增加自定义的 NodeIPAM 控制器"这类诉求,才需要进入本目录继续阅读。这正是本 README 与配套 main.go 存在的意义。
逐步拆解 main.go:CCM 控制器扩展的完整装配流程
main.go 展示了扩展机制用到的全部关键 API,下面按执行顺序拆解。
1. 创建并校验全局选项
ccmOptions, err := options.NewCloudControllerManagerOptions()
if err != nil {
klog.Fatalf("unable to initialize command options: %v", err)
}
options.NewCloudControllerManagerOptions() 来自 k8s.io/cloud-provider/options,一次性构造出 CCM 的全部命令行配置项(cloud provider 名、cloud config 文件、CIDR 分配、kubeconfig 等)。失败即 Fatal,避免带着残缺配置启动。
2. 取得默认控制器集合与别名表
controllerInitializers := app.DefaultInitFuncConstructors
controllerAliases := names.CCMControllerAliases()
app.DefaultInitFuncConstructors:由k8s.io/cloud-provider/app导出的默认控制器构造器 map。其完整定义在 staging/src/k8s.io/cloud-provider/app/controllermanager.go#L441-L470:
var DefaultInitFuncConstructors = map[string]ControllerInitFuncConstructor{
names.CloudNodeController: {
InitContext: ControllerInitContext{ClientName: "node-controller"},
Constructor: StartCloudNodeControllerWrapper,
},
names.CloudNodeLifecycleController: {
InitContext: ControllerInitContext{ClientName: "node-controller"},
Constructor: StartCloudNodeLifecycleControllerWrapper,
},
names.ServiceLBController: {
InitContext: ControllerInitContext{ClientName: "service-controller"},
Constructor: StartServiceControllerWrapper,
},
names.NodeRouteController: {
InitContext: ControllerInitContext{ClientName: "route-controller"},
Constructor: StartRouteControllerWrapper,
},
}
即默认提供 cloud-node、cloud-node-lifecycle、service(LB)、route 四类控制器;ControllerInitFuncConstructor 由 InitContext(携带 ClientName)与 Constructor(返回 InitFunc 的函数)两部分构成,类型定义在 controllermanager.go#L404-L411。
names.CCMControllerAliases():返回控制器短名→正式名的别名映射(实现见 staging/src/k8s.io/cloud-provider/names/controller_names.go#L60),例如允许用短名nodeipam指代正式控制器名,便于--controllers命令行参数书写与命令行约定检查。
3. 删除不需要的默认控制器
示例在注释中演示了"移除不需要的控制器":
// Here is an example to remove the controller which is not needed.
// e.g. remove the cloud-node-lifecycle controller which current cloud provider does not need.
//delete(controllerInitializers, "cloud-node-lifecycle")
本质就是 delete(controllerInitializers, <控制器名>)——从 map 中删掉对应 key 后,命令装配阶段就不会再启动该控制器。默认集合中恰好包含哪些 key 可对照上面的 DefaultInitFuncConstructors 定义核对。
4. 注入自定义控制器:以 NodeIPAM 为例
接着,示例把"kube-controller-manager 侧"的 NodeIPAM 控制器搬进 CCM,演示完整的注入三步曲:
nodeIpamController := nodeIPAMController{}
nodeIpamController.nodeIPAMControllerOptions.NodeIPAMControllerConfiguration = &nodeIpamController.nodeIPAMControllerConfiguration
fss := cliflag.NamedFlagSets{}
nodeIpamController.nodeIPAMControllerOptions.AddFlags(fss.FlagSet(kcmnames.NodeIpamController))
controllerInitializers[kcmnames.NodeIpamController] = app.ControllerInitFuncConstructor{
InitContext: app.ControllerInitContext{
ClientName: "node-controller",
},
Constructor: nodeIpamController.StartNodeIpamControllerWrapper,
}
controllerAliases["nodeipam"] = kcmnames.NodeIpamController
对应 main.go#L58-L75。要点:
- 先注册该控制器专属的命令行 flags(放入名为
kcmnames.NodeIpamController的 flag 分组),并把配置默认值应用进去; - 然后以
kcmnames.NodeIpamController为 key 写回controllerInitializers,其中Constructor指向自定义启动封装器StartNodeIpamControllerWrapper; InitContext.ClientName复用"node-controller"——这是所有 node 系列控制器的共享身份。代码注释(main.go#L68-L69)专门说明:node、node-lifecycle、node-ipam 共享node-controller这一客户端身份是刻意设计,因此它们自动复用 API Server 侧已为node-controller配置好的 RBAC 权限;- 最后注册短名别名:
controllerAliases["nodeipam"] = kcmnames.NodeIpamController。这里kcmnames.NodeIpamController(即"node-ipam-controller")来自 cmd/kube-controller-manager/names/controller_names.go#L69,kcmnames别名对应 importk8s.io/kubernetes/cmd/kube-controller-manager/names。
提示:示例刻意 import 了 kube-controller-manager 的 names 与 NodeIPAM 配置包。这证明 CCM 扩展机制并不局限于
cloud-provider体系内的控制器——只要满足ControllerInitFuncConstructor的签名约定,任何控制器逻辑都可以被装配进 CCM。
5. 组装命令并运行
command := app.NewCloudControllerManagerCommand(ccmOptions, cloudInitializer, controllerInitializers, controllerAliases, fss, wait.NeverStop)
code := cli.Run(command)
os.Exit(code)
app.NewCloudControllerManagerCommand 接收六个参数:全局选项、云初始化器回调(cloudInitializer)、改造后的控制器集合、别名表、flag 分组集合以及 stop channel;cli.Run 负责解析参数、启动 HTTP/指标服务并阻塞运行,返回值直接作为进程退出码。
6. 云初始化器 cloudInitializer 与 ClusterID 强校验
cloudInitializer(main.go#L82-L101)是连接"配置"与"具体云"的关键回调,逻辑与 sample 中完全一致:
func cloudInitializer(config *cloudcontrollerconfig.CompletedConfig) cloudprovider.Interface {
cloudConfig := config.ComponentConfig.KubeCloudShared.CloudProvider
cloud, err := cloudprovider.InitCloudProvider(cloudConfig.Name, cloudConfig.CloudConfigFile)
if err != nil {
klog.Fatalf("Cloud provider could not be initialized: %v", err)
}
if cloud == nil {
klog.Fatalf("Cloud provider is nil")
}
if !cloud.HasClusterID() {
if config.ComponentConfig.KubeCloudShared.AllowUntaggedCloud {
klog.Warning("detected a cluster without a ClusterID. A ClusterID will be required in the future. Please tag your cluster to avoid any future issues")
} else {
klog.Fatalf("no ClusterID found. A ClusterID is required for the cloud provider to function properly. This check can be bypassed by setting the allow-untagged-cloud option")
}
}
return cloud
}
关键行为:根据 --cloud-provider 与 --cloud-config 解析出的名字与配置文件,调用 cloudprovider.InitCloudProvider 实例化云接口;随后强制校验集群是否带 ClusterID(cloud.HasClusterID())——没有 ClusterID 时,若设置了 allow-untagged-cloud 选项则降级为 Warning,否则直接 Fatal。这一硬校验保证了 CCM 中依赖 ClusterID 的控制器(如 route、LB)不会在错误配置下静默运行。
新增控制器的示例实现:nodeipamcontroller.go 深入
nodeipamcontroller.go 是该目录中唯一的完整外部控制器实现,定义了自定义控制器与框架约定的全部对接方式。
自定义控制器的数据结构
type nodeIPAMController struct {
nodeIPAMControllerConfiguration nodeipamconfig.NodeIPAMControllerConfiguration
nodeIPAMControllerOptions nodeipamcontrolleroptions.NodeIPAMControllerOptions
}
它把配置对象(nodeipamconfig.NodeIPAMControllerConfiguration,来自 pkg/controller/nodeipam/config)与命令行选项对象(.../options.NodeIPAMControllerOptions)封装在一起——这是"通过框架注册 flags → Validate → ApplyTo"的标准套路的载体。
启动器封装:向框架暴露统一的 Constructor 签名
func (nodeIpamController *nodeIPAMController) StartNodeIpamControllerWrapper(initContext app.ControllerInitContext, completedConfig *cloudcontrollerconfig.CompletedConfig, cloud cloudprovider.Interface) app.InitFunc {
allErrors := nodeIpamController.nodeIPAMControllerOptions.Validate()
if len(allErrors) > 0 {
klog.Fatal("NodeIPAM controller values are not properly set.")
}
nodeIpamController.nodeIPAMControllerOptions.ApplyTo(&nodeIpamController.nodeIPAMControllerConfiguration)
return func(ctx context.Context, controllerContext genericcontrollermanager.ControllerContext) (controller.Interface, bool, error) {
return startNodeIpamController(ctx, initContext, completedConfig, nodeIpamController.nodeIPAMControllerConfiguration, controllerContext, cloud)
}
}
对应 nodeipamcontroller.go#L54-L64。启动前先 Validate() 参数(失败即 Fatal),再 ApplyTo 把选项落地到配置,最后返回一个闭包形态的 InitFunc——框架在真正启动控制器时才会调用它,从而把"装配"与"运行"解耦。这正是 Constructor 类型要返回 app.InitFunc 的原因。
启动条件与错误路径
真正的启动函数 startNodeIpamController(nodeipamcontroller.go#L66-L144)对多种配置错误做了显式处理:
- 若
KubeCloudShared.AllocateNodeCIDRs未开启,直接返回(nil, false, nil)——不启动也不算失败,NodeIPAM 由集群其它组件负责; - 若
--cidr-allocator-type为CloudAllocator而 cloud 为空(--cloud-provider未设置或为external),返回明确错误:--cidr-allocator-type is set to 'CloudAllocator' but cloud provider is not configured; - ClusterCIDR 解析失败、多于 1 个 CIDR 却非双栈、或 CIDR 数超过 2 个(双栈上限)都会拒绝启动;
- ServiceCIDR 与 SecondaryServiceCIDR 若都给出,则必须满足双栈约束。
节点 CIDR mask 的默认值与配置规则
文件顶部给出了两个默认常量(nodeipamcontroller.go#L42-L47):
defaultNodeMaskCIDRIPv4 = 24
defaultNodeMaskCIDRIPv6 = 64
setNodeCIDRMaskSizes(nodeipamcontroller.go#L168-L235)围绕这三个 CLI 参数组织了一套组合约束,可整理为下表:
| 场景 | 约束 |
|---|---|
| 双栈集群 | 不允许使用旧参数 --node-cidr-mask-size;必须使用 IP 族专属的 --node-cidr-mask-size-ipv4 / --node-cidr-mask-size-ipv6(或用默认值 24/64) |
单栈集群且设置了 --node-cidr-mask-size |
该参数作为唯一基准;同时再设置 IPv4/IPv6 专属参数属于非法组合 |
| 单栈 IPv4 集群 | --node-cidr-mask-size-ipv6 非法 |
| 单栈 IPv6 集群 | --node-cidr-mask-size-ipv4 非法 |
全部通过后,才构造 pkg/controller/nodeipam 的真正 nodeipamcontroller.NewNodeIpamController(...) 并 go 异步运行,返回 (nil, true, nil) 表示"已成功接管"。可以推断:NodeIPAM 在 CCM 中主要服务于使用云厂商 CIDR 分配能力的集群,因此它对 ClusterCIDR 双栈与 allocator 类型的校验最严格。
提供商注入入口 providers.go 与 in-tree 遗留说明
providers.go 是一个几乎为空的参考文件,但它记录了一个关键的迁移知识点:out-of-tree 云厂商不需要 import 任何 in-tree provider。文件注释(providers.go#L22-L29)说明:
- 若你需要使用遗留 provider(如
k8s.io/legacy-cloud-providers/gce),其注入方式是通过该包内的init()调用cloudprovider.RegisterCloudProvider()完成注册; - 你的 CCM 只需以空导入方式引入该包即可触发注册,例如
import _ "k8s.io/legacy-cloud-providers/gce"; - main.go#L43 中的注释也保留了同样的空导入示例。
同时要提醒:正如 staging/src/k8s.io/cloud-provider/sample/README.md 所指出的,从 1.31 起该选项对既有云厂商同样关闭——遗留 in-tree provider 体系正在整体退场,新代码一律走 out-of-tree 注册。
一个可验证的落点:默认控制器集合被测试锁定
如果你怀疑"默认控制器就是那四个",仓库里的单元测试可以直接背书。staging/src/k8s.io/cloud-provider/app/controllermanager_test.go 遍历 DefaultInitFuncConstructors 并断言其 key 集合恰好等于 CloudNodeController、ServiceLBController、NodeRouteController、CloudNodeLifecycleController 四者。这意味着:任何第三方在复制 main.go 的装配代码时,其"base 集合"是稳定且可预期的——你增删的只是这个固定集合的超集或子集。
总结:把官方扩展机制落到你自己的 CCM
对照 cmd/cloud-controller-manager/README.md 的意图,一次完整的自研 CCM 落地可以归纳为四步:
- 判定诉求:无增删控制器需求 → 以 staging/src/k8s.io/cloud-provider/sample/basic_main.go 为最小模板;有增删需求 → 以本目录 main.go 为骨架;
- 外围仓库:在
k8s.io/cloud-provider-<provider>建立 main 包,引用k8s.io/cloud-provider(对应本仓库 staging/src/k8s.io/cloud-provider)而非 vendor 官方 CCM 目录; - 装配控制器:
delete移除不需要的默认控制器;对自定义控制器实现ControllerInitFuncConstructor(含ClientName身份与Constructor),并注册 flags、别名; - 实现回调:编写
cloudInitializer完成cloudprovider.InitCloudProvider+ ClusterID 强校验,然后cli.Run(command)启动。
整条链路的所有 API 都能在 k8s.io/cloud-provider/app、k8s.io/cloud-provider/names 以及本示例目录三处互相印证,可作为你在自己 CCM 仓库中逐行对照的权威参考。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0625
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00