K8S CCM简介

一、核心目标与核心概念澄清

1. 核心目标

实现 K8s 中 type: LoadBalancer 类型的 Service 按需对接自定义的多套云平台(如 "一云""二云"),不同 Service 可通过注解指定对接的云平台及负载策略,且仅 LoadBalancer 类型 Service 触发自定义逻辑,不影响其他类型 Service。

2. 关键概念修正(易混淆点)

易混淆表述 精准定义
"修改配置让所有 Service 走负载" K8s 仅对 type: LoadBalancer 的 Service 调用云提供商插件(CCM)逻辑,ClusterIP/NodePort/ExternalName 类型 Service 完全走原生逻辑,无需额外配置隔离
"Service 通过污点走自定义云" 污点(Taint)用于节点调度,与 Service 对接云平台无关;Service 通过 annotations 传递参数(如云平台名称、负载策略),由自定义 CCM 解析后调用对应云接口
"kube-controller-manager 配置影响所有 Service" --cloud-provider 参数仅作用于需要云基础设施交互的场景(仅 LoadBalancer 类型 Service),其他 Service 不受影响

二、全流程开发步骤

1. 前期准备

  • 技术栈:Go 1.19+(CCM 开发)、K8s 1.24+ 集群、私有镜像仓库(如 Harbor)、已实现多云负载均衡接口(REST/gRPC,含创建 / 删除 / 更新 / 查询 LB 能力)。
  • 核心依赖:k8s.io/apimachineryk8s.io/client-gok8s.io/cloud-provider 等 K8s 核心库。

2. 自定义 CCM 插件开发(核心环节)

CCM(Cloud Controller Manager)是 K8s 对接云厂商的核心组件,需基于 K8s 标准接口开发,实现 "解析 Service 注解 → 区分云平台 → 调用对应云接口 → 回填 LB IP" 的逻辑。

2.1 工程结构标准化

plaintext

复制代码
my-cloud-controller-manager/
├── cmd/
│   └── manager/
│       └── main.go          # CCM 入口,注册云提供商并启动
├── pkg/
│   ├── cloudprovider/       # 云提供商核心实现
│   │   ├── mycloud/
│   │   │   ├── client.go    # 多云接口客户端(对接一云/二云)
│   │   │   ├── loadbalancer.go # LoadBalancer 接口实现(核心逻辑)
│   │   │   └── provider.go  # 注册自定义云提供商
│   └── utils/               # 工具函数
│       ├── node.go          # 提取节点 IP
│       └── service.go       # 解析 Service 注解
├── deploy/                  # 部署配置
│   ├── rbac.yaml            # CCM 权限配置
│   └── ccm-deployment.yaml  # CCM 部署文件
├── Dockerfile               # 镜像构建文件
├── go.mod                   # Go 依赖管理
└── go.sum
2.2 核心代码实现
(1)多云接口客户端封装(pkg/cloudprovider/mycloud/client.go)

封装对接 "一云""二云" 的接口调用逻辑,支持传递注解参数(如 LB 规格、带宽):

go

运行

复制代码
package mycloud

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
    "time"

    "k8s.io/klog/v2"
)

// 定义 LB 创建响应结构(与云接口一致)
type LBCreateResponse struct {
    LBId string `json:"lbId"`
    IP   string `json:"ip"`
    Err  string `json:"err"`
}

// 多云客户端结构体
type MyLBClient struct {
    cloud1BaseURL string // 一云接口地址
    cloud2BaseURL string // 二云接口地址
    httpClient    *http.Client
}

// 初始化客户端
func NewMyLBClient(cloud1URL, cloud2URL string) *MyLBClient {
    return &MyLBClient{
        cloud1BaseURL: cloud1URL,
        cloud2BaseURL: cloud2URL,
        httpClient: &http.Client{Timeout: 30 * time.Second},
    }
}

// 调用一云创建 LB 接口
func (c *MyLBClient) CreateCloud1LB(serviceName string, nodeIPs []string, port, nodePort int, spec, bandwidth string) (*LBCreateResponse, error) {
    reqBody := map[string]interface{}{
        "name":      serviceName,
        "nodes":     nodeIPs,
        "port":      port,
        "nodePort":  nodePort,
        "spec":      spec,
        "bandwidth": bandwidth,
    }
    return c.callLBAPI(c.cloud1BaseURL+"/api/v1/lb/create", "POST", reqBody)
}

// 调用二云创建 LB 接口
func (c *MyLBClient) CreateCloud2LB(serviceName string, nodeIPs []string, port, nodePort int, spec, bandwidth string) (*LBCreateResponse, error) {
    reqBody := map[string]interface{}{
        "name":      serviceName,
        "nodes":     nodeIPs,
        "port":      port,
        "nodePort":  nodePort,
        "spec":      spec,
        "bandwidth": bandwidth,
    }
    return c.callLBAPI(c.cloud2BaseURL+"/api/v1/lb/create", "POST", reqBody)
}

// 通用接口调用方法
func (c *MyLBClient) callLBAPI(url, method string, reqBody interface{}) (*LBCreateResponse, error) {
    reqBytes, err := json.Marshal(reqBody)
    if err != nil {
        return nil, err
    }
    req, err := http.NewRequest(method, url, bytes.NewBuffer(reqBytes))
    if err != nil {
        return nil, err
    }
    req.Header.Set("Content-Type", "application/json")

    resp, err := c.httpClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var lbResp LBCreateResponse
    if err := json.NewDecoder(resp.Body).Decode(&lbResp); err != nil {
        return nil, err
    }
    if lbResp.Err != "" {
        return nil, fmt.Errorf(lbResp.Err)
    }
    return &lbResp, nil
}

// 补充删除/更新/查询接口(略)
(2)LoadBalancer 接口实现(pkg/cloudprovider/mycloud/loadbalancer.go)

实现 K8s 标准 LoadBalancer 接口,核心逻辑:解析注解 → 区分云平台 → 调用对应接口 → 回填 IP:

go

运行

复制代码
package mycloud

import (
    "context"
    "fmt"

    "github.com/your-name/my-cloud-controller-manager/pkg/utils"
    "k8s.io/api/core/v1"
    "k8s.io/cloud-provider"
    "k8s.io/cloud-provider/pkg/framework"
    "k8s.io/klog/v2"
)

type MyLoadBalancer struct {
    lbClient   *MyLBClient
    kubeClient framework.ControllerClient
}

func NewMyLoadBalancer(lbClient *MyLBClient, kubeClient framework.ControllerClient) *MyLoadBalancer {
    return &MyLoadBalancer{lbClient: lbClient, kubeClient: kubeClient}
}

// 核心方法:创建 LoadBalancer
func (m *MyLoadBalancer) CreateLoadBalancer(
    ctx context.Context,
    clusterName string,
    service *v1.Service,
    nodes []*v1.Node,
) (*v1.LoadBalancerStatus, error) {
    // 1. 校验:仅带指定注解的 LoadBalancer Service 触发自定义逻辑
    enable, ok := service.Annotations["mycloud.com/enable"]
    if !ok || enable != "true" {
        return nil, fmt.Errorf("my-cloud not enabled for service %s", service.Name)
    }

    // 2. 解析注解参数(云平台/规格/带宽)
    provider := service.Annotations["mycloud.com/provider"] // cloud1/cloud2
    lbSpec := service.Annotations["mycloud.com/lb-spec"]
    lbBandwidth := service.Annotations["mycloud.com/lb-bandwidth"]
    if provider == "" {
        return nil, fmt.Errorf("missing mycloud.com/provider annotation")
    }
    if lbSpec == "" {
        lbSpec = "standard"
    }
    if lbBandwidth == "" {
        lbBandwidth = "5M"
    }

    // 3. 提取节点 IP 和端口
    nodeIPs := utils.ExtractNodeIPs(nodes)
    if len(nodeIPs) == 0 {
        return nil, fmt.Errorf("no node IPs extracted")
    }
    port := service.Spec.Ports[0].Port
    nodePort := service.Spec.Ports[0].NodePort
    if nodePort == 0 {
        return nil, fmt.Errorf("nodePort not allocated for service %s", service.Name)
    }

    // 4. 分支逻辑:调用对应云平台接口
    var lbResp *LBCreateResponse
    var err error
    switch provider {
    case "cloud1":
        lbResp, err = m.lbClient.CreateCloud1LB(service.Name, nodeIPs, int(port), int(nodePort), lbSpec, lbBandwidth)
    case "cloud2":
        lbResp, err = m.lbClient.CreateCloud2LB(service.Name, nodeIPs, int(port), int(nodePort), lbSpec, lbBandwidth)
    default:
        return nil, fmt.Errorf("unsupported provider: %s", provider)
    }
    if err != nil {
        return nil, err
    }

    // 5. 记录 LB ID 到 Service 注解,方便后续删除/更新
    utils.SetLBIdAnnotation(service, lbResp.LBId)
    _ = m.kubeClient.Update(ctx, service) // 忽略更新失败(仅日志告警)

    // 6. 回填 LB IP 到 Service 的 EXTERNAL-IP
    return &v1.LoadBalancerStatus{
        Ingress: []v1.LoadBalancerIngress{{IP: lbResp.IP}},
    }, nil
}

// 实现删除/更新/查询等接口(略)
func (m *MyLoadBalancer) DeleteLoadBalancer(ctx context.Context, clusterName string, service *v1.Service) error {
    // 解析 LB ID → 调用对应云平台删除接口 → 删除注解
    return nil
}

func (m *MyLoadBalancer) UpdateLoadBalancer(ctx context.Context, clusterName string, service *v1.Service, nodes []*v1.Node) error {
    // 解析 LB ID → 调用对应云平台更新接口
    return nil
}
(3)注册自定义云提供商(pkg/cloudprovider/mycloud/provider.go)

go

运行

复制代码
package mycloud

import (
    "io"

    "k8s.io/cloud-provider"
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/rest"
    "k8s.io/klog/v2"
)

type MyCloudProvider struct {
    loadBalancer *MyLoadBalancer
    initialized  bool
}

// 初始化云提供商
func NewMyCloudProvider(config io.Reader) (cloudprovider.Interface, error) {
    // 1. 加载 K8s 集群内配置
    kubeConfig, err := rest.InClusterConfig()
    if err != nil {
        return nil, err
    }
    kubeClient, err := kubernetes.NewForConfig(kubeConfig)
    if err != nil {
        return nil, err
    }

    // 2. 初始化多云客户端(替换为实际接口地址)
    lbClient := NewMyLBClient("http://cloud1-api:8080", "http://cloud2-api:8080")

    // 3. 初始化 LoadBalancer 实现
    loadBalancer := NewMyLoadBalancer(lbClient, framework.NewControllerClient(kubeClient, nil))

    return &MyCloudProvider{
        loadBalancer: loadBalancer,
        initialized:  true,
    }, nil
}

// 实现 cloudprovider.Interface 接口(仅暴露 LoadBalancer 能力)
func (m *MyCloudProvider) Initialize(_ cloudprovider.ControllerClientBuilder, _ <-chan struct{}) {}
func (m *MyCloudProvider) LoadBalancer() (cloudprovider.LoadBalancer, bool) {
    return m.loadBalancer, m.initialized
}
func (m *MyCloudProvider) Instances() (cloudprovider.Instances, bool) { return nil, false }
func (m *MyCloudProvider) Zones() (cloudprovider.Zones, bool)         { return nil, false }
func (m *MyCloudProvider) ProviderName() string                       { return "my-cloud" }
func (m *MyCloudProvider) HasClusterID() bool                         { return true }

// 注册云提供商(init 函数自动执行)
func init() {
    cloudprovider.RegisterCloudProvider("my-cloud", NewMyCloudProvider)
    klog.Info("my-cloud provider registered")
}
(4)CCM 入口文件(cmd/manager/main.go)

go

运行

复制代码
package main

import (
    "flag"
    "os"

    _ "github.com/your-name/my-cloud-controller-manager/pkg/cloudprovider/mycloud"
    "k8s.io/cloud-provider/cmd/cloud-controller-manager/app"
    "k8s.io/cloud-provider/cmd/cloud-controller-manager/app/options"
    "k8s.io/klog/v2"
)

func main() {
    opts := options.NewCloudControllerManagerOptions()
    cmd := app.NewCloudControllerManagerCommand(opts)

    // 默认参数配置
    flag.Set("cloud-provider", "my-cloud")   // 自定义云提供商名称
    flag.Set("leader-elect", "false")        // 单实例无需选主(生产环境建议开启)
    flag.Set("controllers", "service")       // 仅启用 Service 控制器
    flag.Set("v", "4")                       // 日志级别

    if err := cmd.Execute(); err != nil {
        klog.Fatalf("run CCM failed: %v", err)
        os.Exit(1)
    }
}
2.3 工具函数实现(pkg/utils/)
  • node.go:提取节点内网 / 公网 IP;
  • service.go:解析 / 设置 Service 注解(如 LB ID)。

3. 镜像构建与部署

3.1 构建镜像

dockerfile

复制代码
# 编译阶段
FROM golang:1.20-alpine AS builder
WORKDIR /workspace
COPY . .
ENV GOPROXY=https://goproxy.cn,direct
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o my-cloud-controller-manager ./cmd/manager

# 运行阶段
FROM alpine:3.18
COPY --from=builder /workspace/my-cloud-controller-manager /usr/bin/
RUN apk add --no-cache ca-certificates
ENTRYPOINT ["/usr/bin/my-cloud-controller-manager"]

构建并推送镜像:

bash

运行

复制代码
docker build -t harbor.your-domain.com/my-ccm:v1 .
docker push harbor.your-domain.com/my-ccm:v1
3.2 部署 CCM 到 K8s 集群
(1)RBAC 权限配置(deploy/rbac.yaml)

yaml

复制代码
apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-cloud-controller-manager
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: my-cloud-controller-manager-role
rules:
- apiGroups: [""]
  resources: ["services", "nodes", "events"]
  verbs: ["get", "list", "watch", "update", "patch", "create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: my-cloud-controller-manager-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: my-cloud-controller-manager-role
subjects:
- kind: ServiceAccount
  name: my-cloud-controller-manager
  namespace: kube-system
(2)CCM 部署文件(deploy/ccm-deployment.yaml)

yaml

复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-cloud-controller-manager
  namespace: kube-system
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-ccm
  template:
    metadata:
      labels:
        app: my-ccm
    spec:
      serviceAccountName: my-cloud-controller-manager
      tolerations:
      - key: node-role.kubernetes.io/control-plane
        operator: Exists
        effect: NoSchedule
      containers:
      - name: my-ccm
        image: harbor.your-domain.com/my-ccm:v1
        imagePullPolicy: Always
        command:
        - /usr/bin/my-cloud-controller-manager
        - --cloud-provider=my-cloud
        - --leader-elect=false
        - --controllers=service
        - --v=4
        resources:
          limits:
            cpu: 100m
            memory: 128Mi
          requests:
            cpu: 50m
            memory: 64Mi
(3)执行部署

bash

运行

复制代码
kubectl apply -f deploy/rbac.yaml
kubectl apply -f deploy/ccm-deployment.yaml
# 验证 CCM 运行状态
kubectl get pods -n kube-system -l app=my-ccm
kubectl logs -n kube-system <my-ccm-pod-name> # 查看是否输出 "my-cloud provider registered"

4. K8s 集群配置

修改 kube-controller-manager 静态 Pod 配置(/etc/kubernetes/manifests/kube-controller-manager.yaml),添加参数指定自定义云提供商:

yaml

复制代码
spec:
  containers:
  - name: kube-controller-manager
    command:
    - kube-controller-manager
    # 新增/修改以下参数
    - --cloud-provider=my-cloud
    - --external-cloud-volume-plugin=my-cloud
    # 保留原有其他参数...

修改后 kube-controller-manager 会自动重启,配置生效。

5. 测试自定义 LoadBalancer Service

5.1 部署测试应用

yaml

复制代码
# nginx-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-demo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx-demo
  template:
    metadata:
      labels:
        app: nginx-demo
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80
5.2 创建自定义 LoadBalancer Service

yaml

复制代码
# nginx-lb-cloud1.yaml(对接一云)
apiVersion: v1
kind: Service
metadata:
  name: nginx-lb-cloud1
  annotations:
    mycloud.com/enable: "true"
    mycloud.com/provider: "cloud1"
    mycloud.com/lb-spec: "standard"
    mycloud.com/lb-bandwidth: "10M"
spec:
  selector:
    app: nginx-demo
  ports:
  - port: 80
    targetPort: 80
  type: LoadBalancer

# nginx-lb-cloud2.yaml(对接二云)
apiVersion: v1
kind: Service
metadata:
  name: nginx-lb-cloud2
  annotations:
    mycloud.com/enable: "true"
    mycloud.com/provider: "cloud2"
    mycloud.com/lb-spec: "high-performance"
    mycloud.com/lb-bandwidth: "20M"
spec:
  selector:
    app: nginx-demo
  ports:
  - port: 80
    targetPort: 80
  type: LoadBalancer

# 普通 ClusterIP Service(不受影响)
apiVersion: v1
kind: Service
metadata:
  name: nginx-clusterip
spec:
  selector:
    app: nginx-demo
  ports:
  - port: 80
    targetPort: 80
  type: ClusterIP
5.3 验证结果

bash

运行

复制代码
# 创建 Service
kubectl apply -f nginx-deploy.yaml
kubectl apply -f nginx-lb-cloud1.yaml
kubectl apply -f nginx-lb-cloud2.yaml
kubectl apply -f nginx-clusterip.yaml

# 查看 Service 状态
watch kubectl get svc
# 预期结果:
# - nginx-lb-cloud1/nginx-lb-cloud2 的 EXTERNAL-IP 会变为对应云平台返回的 LB IP;
# - nginx-clusterip 为 ClusterIP 类型,EXTERNAL-IP 为空,完全不受影响。

# 访问测试
curl http://<nginx-lb-cloud1-EXTERNAL-IP> # 触发一云负载策略
curl http://<nginx-lb-cloud2-EXTERNAL-IP> # 触发二云负载策略

三、关键控制逻辑与容错

1. 仅 LoadBalancer 类型 Service 触发自定义逻辑

K8s 仅对 type: LoadBalancer 的 Service 调用 CCM 的 LoadBalancer 接口,ClusterIP/NodePort 等类型完全走原生逻辑,无需额外隔离。

2. 精细化控制(仅指定注解的 LB Service 生效)

通过 mycloud.com/enable: "true" 注解开关,仅显式声明的 LB Service 触发自定义逻辑,其他 LB Service 会返回错误(EXTERNAL-IP 处于 pending 状态),可按需兼容原生云厂商逻辑。

3. 容错处理

  • 接口调用超时 / 失败:CCM 会重试并记录日志,确保 LB 创建 / 删除逻辑幂等;
  • 注解解析失败:返回明确错误,避免无效调用;
  • Service 注解更新失败:仅日志告警,不影响核心 LB 逻辑。

四、核心总结

  1. 开发核心:基于 K8s 标准 CCM 框架开发插件,实现 LoadBalancer 接口,通过解析 Service 注解区分多云平台,调用对应接口生成 LB IP;
  2. 部署核心:CCM 部署到 kube-system 命名空间,配置 RBAC 权限,修改 kube-controller-manager 指定自定义云提供商;
  3. 控制核心:K8s 天然区分 Service 类型,仅 type: LoadBalancer 触发 CCM 逻辑,通过注解开关可进一步精细化控制;
  4. 隔离核心:自定义 CCM 仅实现 LoadBalancer 接口,其他接口返回未实现,确保集群其他功能(如节点管理、存储)不受影响。
相关推荐
源码技术栈2 小时前
新一代云原生 HIS 系统:技术架构全解析 + 核心业务功能全景展示
云原生·云计算·源码·saas·云his·his·多医院
星环处相逢2 小时前
Kubernetes PV 与 PVC 深度解析:从基础存储到动态部署实战
云原生·容器·kubernetes
ICT董老师2 小时前
在Ubuntu 22.04上使用GitLab和Jenkins部署CI/CD的完整过程
ubuntu·ci/cd·kubernetes·gitlab·jenkins
2501_940414082 小时前
我用这套云原生工作流,把上线时间从1天压到3分钟
云原生
鲨莎分不晴2 小时前
云计算技术架构与原理深度解析:从虚拟化到云原生的演进之路
云原生·架构·云计算
ζั͡山 ั͡有扶苏 ั͡✾2 小时前
K8s 集群内存压力检测和智能 Pod 驱逐工具
云原生·容器·kubernetes
腾讯数据架构师2 小时前
k8s兼容昆仑芯p800
人工智能·云原生·容器·kubernetes·cube-studio·昆仑芯
阿杰 AJie3 小时前
Docker 常用指令和使用方法
docker·容器·eureka