基于 Kubernetes cloud-provider 库编写外部 cloud-controller-manager(CCM)的官方指南与示例解析
导读
本文围绕 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 给出了一份高度凝练的四步清单,结合仓库代码可以还原出完整的落地动作:
-
把外部仓库放在
k8s.io命名空间下,例如k8s.io/cloud-provider-<provider>。命名空间约定是为了与k8s.io/cloud-provider等模块保持一致的 import 路径习惯。 -
在外部仓库的 CCM 目录中创建
main.go,以 basic_main.go 为最小工作样例。这份文件本身就是为"被每个云厂商复制到自己仓库后改写"而准备的——文件头部注释直接写明:"This file should be written by each cloud provider." -
如果你有在 CCM 内增删控制器的需求,不要自己发明注册机制,应参考 cmd/cloud-controller-manager/main.go,其中演示了如何对默认控制器集合做删除与追加(详见下文"控制器扩展机制")。
cmd/cloud-controller-manager/README.md也印证了分工:不需要增删控制器的厂商看k8s.io/cloud-provider/sample;需要增删控制器的厂商看cmd/cloud-controller-manager的示例。 -
从你的外部仓库构建并发布 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 命令与控制器注册框架(NewCloudControllerManagerCommand、DefaultInitFuncConstructors);k8s.io/cloud-provider/app/config:CompletedConfig类型,承载最终生效的组件配置;k8s.io/cloud-provider/names:CCM 控制器名称常量与别名映射;k8s.io/cloud-provider/options:NewCloudControllerManagerOptions提供的完整命令行选项;k8s.io/component-base/cli与cli/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 解析、启动、优雅退出与错误码传递。
fss 与 controllerAliases 两个参数正是为"增删控制器"场景预留的扩展点:第三方控制器可把自己的 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
}
对 CloudNodeLifecycleController、ServiceLBController、NodeRouteController 同样处理。函数上方的注释指出关键后果:
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 工厂,逻辑如下:
- 从
config.ComponentConfig.KubeCloudShared.CloudProvider读取Name与CloudConfigFile; - 调用
cloudprovider.InitCloudProvider(name, cloudConfigFile)真正加载实现——该函数按注册表(由厂商通过cloudprovider.RegisterCloudProvider()注入)解析名字并读取配置文件; - 对返回结果做非空保护;
- 执行 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):
- 构造自己的控制器结构体并把其 options 的配置指针接好;
- 用
options.AddFlags(fss.FlagSet(kcmnames.NodeIpamController))把新增控制器的命令行参数注册到命名 flag 组(kcmnames来自k8s.io/kubernetes/cmd/kube-controller-manager/names); - 以
app.ControllerInitFuncConstructor{ InitContext: app.ControllerInitContext{ ClientName: "node-controller" }, Constructor: <你的启动包装函数> }形式写入controllerInitializers[kcmnames.NodeIpamController]。这里 ClientName 复用"node-controller",注释说明它是 node、node lifecycle、node ipam 等所有 node 系控制器的共享身份; - 追加别名
controllerAliases["nodeipam"] = kcmnames.NodeIpamController,让用户在 flag/配置中可用--controllers=nodeipam之类的短名引用。
Constructor 的签名要求是:接收 InitContext、CCM 的 *cloudcontrollerconfig.CompletedConfig 与 cloudprovider.Interface,返回一个内部 app.InitFunc。cmd/cloud-controller-manager/nodeipamcontroller.go 中 StartNodeIpamControllerWrapper 即这一模式的落地实例,它还展示了第三方控制器的典型"前置检查"模式,例如:
- 仅当
KubeCloudShared.AllocateNodeCIDRs为 true 时才真正启动(否则静默返回不启用); - 若
CIDRAllocatorType为CloudAllocator但 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 都列出了严格的负面清单,共三条:
- 不要把
k8s.io/cmd/cloud-controller-managervendor 进你的仓库。它是仓库内部示例,不是对外 API;正确的依赖对象是k8s.io/cloud-provider。 - 不要直接改动本仓库
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")。 - 不要在 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"):
- 在你的 provider 包(例如厂商仓库中的实现包)里通过
init()调用cloudprovider.RegisterCloudProvider()完成注册; - 在厂商自己 main.go 中以 blank import 方式引入该包(示例注释
import _ "k8s.io/legacy-cloud-providers/gce"),使注册副作用生效; - 此后
cloudprovider.InitCloudProvider(name, file)就能按名字找到你的实现。
这与 basic_main.go 中 cloudInitializer 使用 cloudprovider.InitCloudProvider 的调用形成完整闭环,也解释了"外部仓库自行构建二进制 + 自行注册实现"为何能替代旧的 vendor 拷贝模式。
小结
综合来看,sample 目录提供了一条从 Kubernetes 1.20 起官方认可的最小落地路径:把 main.go 建立在 k8s.io/cloud-provider 的 options、app、names 组合之上,通过 cloudInitializer 注入自研 provider 并处理好 ClusterID 与 RBAC,即可产出独立的 CCM 二进制;需要更多控制权时,再参照 cmd/cloud-controller-manager 对 DefaultInitFuncConstructors 增删并注册额外 flag。自 1.31 起 legacy provider 导入通道关闭后,这条"外部化"路径不再是可选项,而是所有云厂商接入 Kubernetes 云能力的标准姿势。
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 StartedRust0627
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