首页
/ Kubernetes Cloud Controller Manager 扩展机制实战:基于 cmd/cloud-controller-manager 示例构建可增删控制器的自研 CCM

Kubernetes Cloud Controller Manager 扩展机制实战:基于 cmd/cloud-controller-manager 示例构建可增删控制器的自研 CCM

2026-09-06 18:55:29作者:胡唯隽

导读

本文以 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"一节直接给出三条红线,这也是所有云厂商接入时的纪律性约束:

  1. 不要 vendor k8s.io/cmd/cloud-controller-manager(注:原文此处即指 k8s.io/kubernetes/cmd/cloud-controller-manager,即本文所在目录)。vendor 意味着把官方示例当作你的代码基线,后续官方演进时你将被迫持续同步甚至分叉。
  2. 不要直接修改本仓库中 k8s.io/cmd/cloud-controller-manager 下的任何内容。该目录只读地作为参考与示例存在,任何云厂商专属逻辑都不应反向写回。
  3. 不要在这里做具体的云厂商(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()
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 四类控制器;ControllerInitFuncConstructorInitContext(携带 ClientName)与 Constructor(返回 InitFunc 的函数)两部分构成,类型定义在 controllermanager.go#L404-L411

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#L69kcmnames 别名对应 import k8s.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 强校验

cloudInitializermain.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 实例化云接口;随后强制校验集群是否带 ClusterIDcloud.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 的原因。

启动条件与错误路径

真正的启动函数 startNodeIpamControllernodeipamcontroller.go#L66-L144)对多种配置错误做了显式处理:

  • KubeCloudShared.AllocateNodeCIDRs 未开启,直接返回 (nil, false, nil)——不启动也不算失败,NodeIPAM 由集群其它组件负责;
  • --cidr-allocator-typeCloudAllocator 而 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

setNodeCIDRMaskSizesnodeipamcontroller.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 集合恰好等于 CloudNodeControllerServiceLBControllerNodeRouteControllerCloudNodeLifecycleController 四者。这意味着:任何第三方在复制 main.go 的装配代码时,其"base 集合"是稳定且可预期的——你增删的只是这个固定集合的超集或子集。

总结:把官方扩展机制落到你自己的 CCM

对照 cmd/cloud-controller-manager/README.md 的意图,一次完整的自研 CCM 落地可以归纳为四步:

  1. 判定诉求:无增删控制器需求 → 以 staging/src/k8s.io/cloud-provider/sample/basic_main.go 为最小模板;有增删需求 → 以本目录 main.go 为骨架;
  2. 外围仓库:在 k8s.io/cloud-provider-<provider> 建立 main 包,引用 k8s.io/cloud-provider(对应本仓库 staging/src/k8s.io/cloud-provider)而非 vendor 官方 CCM 目录;
  3. 装配控制器delete 移除不需要的默认控制器;对自定义控制器实现 ControllerInitFuncConstructor(含 ClientName 身份与 Constructor),并注册 flags、别名;
  4. 实现回调:编写 cloudInitializer 完成 cloudprovider.InitCloudProvider + ClusterID 强校验,然后 cli.Run(command) 启动。

整条链路的所有 API 都能在 k8s.io/cloud-provider/appk8s.io/cloud-provider/names 以及本示例目录三处互相印证,可作为你在自己 CCM 仓库中逐行对照的权威参考。

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