在 VMware vSphere 上创建集群

本文档说明如何通过向 ACP global 管理集群应用 Cluster API 清单来创建 VMware vSphere 业务集群。该操作不会创建 global 集群本身。文档涵盖一个最小支持拓扑:一个 datacenter、每个节点一个 NIC,以及通过 VSphereMachineConfigPool 进行静态 IP 分配。本页中的 provider 行为和字段已针对 VMware vSphere Provider v1.0.16 进行了验证。

使用场景

在以下场景中使用本文档:

  • 您希望在环境中创建第一个基线 VMware vSphere 业务集群。
  • 您希望在初始验证阶段使用一个 datacenter 和每个节点一个 NIC。
  • 您希望在启用高级放置或网络功能之前,先保持首次部署尽可能简单。

本文档适用于以下部署模型:

  • CAPV 直接连接到 vCenter。
  • 控制平面和工作节点都使用 VSphereMachineConfigPool 进行静态 IP 分配和数据盘配置。
  • ClusterResourceSet 自动下发 vSphere CPI 组件。
  • 首次验证使用一个 datacenter 和每个节点一个 NIC。

本文档不适用于以下场景:

  • 依赖 vSphere Supervisor 或 vm-operator 的部署。
  • 不使用 VSphereMachineConfigPool 的部署。

本文档面向当前平台环境编写。kube-ovn 下发路径依赖于消费 Cluster 资源注解的平台控制器,因此该工作流并不打算作为平台上下文之外通用的独立 CAPV 部署指南。

如何使用本页

  1. 完成基础设施和参数检查清单。
  2. 按照下面的操作步骤准备基线 manifest 文件。
  3. 在应用之前,仅添加所需的 创建时拓扑变体
  4. 应用完整的 manifest 集合并完成验证检查。
  5. 集群运行后,如需扩容、不可变模板替换或运行时拓扑变更,请参考 在 VMware vSphere 上管理节点

基线是验证参考。如果目标集群需要多个可选拓扑功能,请先逐个引入并验证每一项 manifest 变更,然后再进行组合。

先决条件

在开始之前,请确保满足以下条件:

  1. 您已完成 VMware vSphere 基础设施准备
  2. global 集群可以访问 vCenter。
  3. 目标模板、网络、datastore 和 vCenter resource pool 均可用。
  4. 外部控制平面 LoadBalancer 满足 端点契约。VMware vSphere Provider v1.0.16 不会部署 Self-built VIP。
  5. 所有必需的静态 IP 地址都已分配且未被使用。
  6. 已启用 ClusterResourceSet=true
  7. 平台中已存在有效的 public-registry-credential Secret,并且永久 Registry 地址可从业务节点访问。
  8. 永久平台 Registry 中包含所需的 Kube-OVN、CoreDNS 和 vSphere CPI 标签。
  9. 平台可以处理安装网络插件所需的集群注解。

关键对象

VMware vSphere Cluster API 资源依赖关系

顶层 Cluster 引用 VSphereCluster 以及控制平面或 worker 控制器。这些控制器再引用不可变的 VSphereMachineTemplate 对象。每个模板引用一个 VSphereMachineConfigPool,其插槽为运行时 MachineVSphereMachineVSphereVM 对象提供主机名、静态网络配置以及持久磁盘。

阶段输入应用预期结果首先诊断
端点和 RegistryLoadBalancer、永久 Registry、版本集验证外部依赖。端点和 Registry 可从所需网络访问。LoadBalancer 健康状态、DNS、Registry API 和 ACP 端口规则。
集群根对象网络 CIDR、vCenter 身份、端点应用 ClusterVSphereCluster两个资源都存在,并报告 progressing 条件。ClusterVSphereCluster 条件。
CPI 下发vCenter CPI 配置和凭证应用 ClusterResourceSet 资源。在 workload 集群 API 可访问后,会收到 vSphere CPI。ClusterResourceSetBinding 和 CPI Pod。
节点插槽主机名、静态 IP、datastore、持久磁盘应用两个 VSphereMachineConfigPool 对象。每个 pool 都为 Ready,并暴露预期插槽。pool 条件和 status.configStatuses
控制平面VM 模板、kubeadm 配置、Kubernetes 版本应用 VSphereMachineTemplateKubeadmControlPlane三个控制平面 Machine 变为 Ready。KCP、Machine、VSphereMachineVSphereVM 和端点健康状况。
Workerworker 模板和 bootstrap 配置应用 KubeadmConfigTemplateMachineDeploymentworker Machine 和 Node 变为 Ready。MachineDeployment rollout、VM 状态、CPI 和 CNI。

ClusterResourceSet

ClusterResourceSetglobal 集群中的一个 Cluster API 资源。在 workload API server 可访问后,它会将所引用的 ConfigMapSecret 资源应用到 workload 集群。

在此工作流中,ClusterResourceSet 用于自动下发 vSphere CPI 资源。

vSphere CPI 组件

vSphere CPI 组件通过 ClusterResourceSet 下发到 workload 集群。它将 workload 节点连接到 vSphere 基础设施,以便集群能够报告基础设施身份并完成 cloud provider 初始化。

machine config pool

machine config pool 是 VSphereMachineConfigPool 自定义资源。在基线工作流中:

  • 一个 machine config pool 用于控制平面节点。
  • 一个 machine config pool 用于 worker 节点。

每个节点插槽都包含主机名、datacenter、静态 IP 分配以及可选的数据盘定义。

对于网络配置,请区分以下字段:

  • networkName 是 vCenter 网络或 port group 名称。
  • deviceName 是 guest operating system 内部的 NIC 名称。

如果设置了 deviceName,CAPV 会将该值写入生成的 guest-network metadata。若未设置,当前实现通常会按 NIC 顺序使用 eth0eth1eth2 等 NIC 名称。

还要区分以下值格式:

  • 节点 IP 地址与前缀长度一起使用,例如 10.10.10.11/24
  • gateway 字段只包含 gateway IP 地址,例如 10.10.10.1

VM 模板要求

此工作流使用的 VM 模板应满足以下最低要求:

  1. 使用目标平台环境所需的 operating system。
  2. 包含 cloud-init
  3. 包含 VMware Tools 或 open-vm-tools
  4. 包含 containerd
  5. 包含 kubeadm bootstrap 所需的基础组件。
  6. /root/images/ 下包含预先导出的 container image tar 文件。在 kubeadm 运行之前,capv-load-local-images.sh 会将这些文件导入到 containerd 中,从而使节点引导不依赖从远程 Registry 拉取镜像。
  7. 这些 tar 文件包含 OS 模板提供的 kubeadm control-plane 镜像,包括 etcd 和 kube-apiserver,并且其镜像引用必须与 clusterConfiguration.imageRepository、Kubernetes 版本以及组件镜像标签生成的结果完全一致。尽管 etcd 和 kube-apiserver 以 static Pod 方式运行,它们仍然需要 container image。缺少或不匹配的本地引用会导致运行时改为使用已配置的 Registry。
  8. /root/images/*.tar 文件 必须 包含 sandbox(pause)镜像,其引用必须与 /etc/containerd/config.toml 中配置的 sandbox_image 值(containerd v1)或 sandbox 值(containerd v2)完全匹配。例如,如果 containerd 配置为 sandbox_image = "registry.example.com/tkestack/pause:3.10",则某个 tar 文件中必须包含该精确镜像引用。不匹配会导致 containerd 从网络拉取 sandbox 镜像,这会破坏本地预加载的目的,并在 air-gapped 环境中失败。

静态 IP 配置、主机名注入和其他初始化设置都依赖 cloud-init。节点 IP 上报依赖 guest tools。

本地文件布局

业务集群命名

业务 cluster_name 不能global。该名称已保留给 global 集群,重复使用会导致 workload 集群的资源与 cpaas-system 中的 global 集群资源发生冲突。global- 前缀保留给 global 集群 DR 工作流拥有的资源;请参见 常见先决条件。不要将 global- 用于 workload 集群资源,因为故障切换操作可能会像选择 global 集群资源一样选择这些资源。

按照约定,CAPI Cluster 和 provider cluster 资源(VSphereCluster)应保持为完全相同的 <cluster_name> 命名,并将非根级 CAPI 和 provider 资源(KubeadmControlPlaneKubeadmConfigTemplateMachineDeploymentVSphereMachineTemplateVSphereMachineConfigPool 等)以前缀 <cluster_name>- 命名——例如,示例 manifest 使用 <cluster_name>-kcp<cluster_name>-md-0。这是一条建议而不是控制器强制规则,但它可以避免多个 workload 集群位于 cpaas-system 时发生同命名空间冲突,并使资源归属在操作期间更加清晰。

创建本地工作目录并按以下布局存放 manifest:

capv-cluster/
├── 00-namespace.yaml
├── 01-vsphere-credentials-secret.yaml
├── 10-cluster.yaml
├── 15-vsphere-cpi-clusterresourceset.yaml
├── 16-vspheremachineconfigpool-control-plane.yaml
├── 17-vspheremachineconfigpool-worker.yaml
├── 18-failure-domains.yaml  # only when failure domains are enabled
├── 20-control-plane.yaml
└── 30-workers-md-0.yaml

使用以下命令创建目录:

mkdir -p ./capv-cluster
cd ./capv-cluster

步骤

验证环境

global 集群运行以下命令,以验证最低先决条件:

kubectl get ns
kubectl get minfo -l cpaas.io/module-name=cluster-api-provider-vsphere
kubectl get minfo -l cpaas.io/module-name=cluster-api-provider-kubeadm
kubectl -n cpaas-system get deploy capi-controller-manager -o jsonpath='{.spec.template.spec.containers[0].args}'
kubectl -n cpaas-system get secret public-registry-credential -o name
kubectl -n cpaas-system get cluster global \
  -o jsonpath='{.metadata.annotations.cpaas\.io/registry-address}{"\n"}'

导出由 Cluster 注解返回的永久平台 Registry 地址,以及 CPI 和 kubeadm manifest 使用的镜像 Registry。如果这两个占位符指向同一个 Registry,则两个变量都使用相同的值。

export CLUSTER_REGISTRY_ADDRESS="<registry_address>"
export IMAGE_REGISTRY_ADDRESS="<image_registry>"
curl -sk "https://${CLUSTER_REGISTRY_ADDRESS}/v2/acp/chart-cpaas-kube-ovn/tags/list"
curl -sk "https://${IMAGE_REGISTRY_ADDRESS}/v2/tkestack/coredns/tags/list"
curl -sk "https://${IMAGE_REGISTRY_ADDRESS}/v2/ait/cloud-provider-vsphere/tags/list"

/v2/ 片段仅是 Registry HTTP API URL 的一部分。在 workload Cluster manifest 中,将 Registry 注解设置为 <registry_address>,不要包含 scheme 或 path。

确认以下结果:

  • global 集群可访问。
  • Alauda Container Platform Kubeadm Provider 和 Alauda Container Platform VMware vSphere Infrastructure Provider 正在运行。
  • 控制器参数包含 ClusterResourceSet=true
  • public-registry-credential Secret 存在;此操作步骤不会打印其内容。
  • 永久平台 Registry 中包含所需的 Kube-OVN、CoreDNS 和 vSphere CPI 标签。
  • VM 模板已通过 可执行本地镜像验证,包括精确的 etcd、kube-apiserver 和 sandbox 镜像引用。它们以 static Pod 方式部署并不会取消对镜像的要求。

在继续之前,还请验证以下项目:

  • vCenter server 地址可访问。
  • vCenter 用户名和密码有效。
  • thumbprint 正确。
  • 模板名称正确。
  • 模板可在目标 datacenter 中解析。
  • 如果 VM 以 fullClone 方式克隆,则模板系统盘大小不能大于后续 manifest 中使用的 diskGiB 值。如果 CAPV 完成的是 linkedClone,则系统盘大小保持为模板大小,diskGiB 会被忽略。
  • 模板中已安装 VMware Tools 或 open-vm-tools
  • 外部 LoadBalancer 满足 规划控制平面端点 中关于 Layer 4 listener、backend、健康检查、可达性和归属的要求。

创建命名空间和 vCenter 凭证 Secret

创建用于存放 workload 集群对象的命名空间。

本工作流将 workload 集群对象存放在 cpaas-system 命名空间中。在下面的 manifest 和命令中,请将所有 <namespace> 占位符替换为 cpaas-system

00-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: <namespace>

创建 VSphereCluster.spec.identityRef 引用的 vCenter 凭证 Secret。

01-vsphere-credentials-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: <credentials_secret_name>
  namespace: <namespace>
type: Opaque
stringData:
  username: "<vsphere_username>"
  password: "<vsphere_password>"

应用这两个 manifest:

kubectl apply -f 00-namespace.yaml
kubectl apply -f 01-vsphere-credentials-secret.yaml

创建 ClusterVSphereCluster 对象

创建基础集群 manifest,其中包含 workload 集群网络设置、控制平面端点以及 vCenter 连接设置。将 cpaas.io/registry-address 设置为永久平台 Registry,格式为 <registry_address>(仅 <host>:<port>)。

10-cluster.yaml
apiVersion: cluster.x-k8s.io/v1beta1
kind: Cluster
metadata:
  name: <cluster_name>
  namespace: <namespace>
  labels:
    cluster.x-k8s.io/cluster-name: <cluster_name>
    cluster-type: VSphere
    addons.cluster.x-k8s.io/vsphere-cpi: "enabled"
  annotations:
    capi.cpaas.io/resource-group-version: infrastructure.cluster.x-k8s.io/v1beta1
    capi.cpaas.io/resource-kind: VSphereCluster
    cpaas.io/sentry-deploy-type: Baremetal
    cpaas.io/alb-address-type: ClusterAddress
    cpaas.io/network-type: kube-ovn
    cpaas.io/kube-ovn-version: <kube_ovn_version>
    cpaas.io/kube-ovn-join-cidr: <kube_ovn_join_cidr>
    cpaas.io/registry-address: <registry_address>
spec:
  clusterNetwork:
    pods:
      cidrBlocks:
      - <pod_cidr>
    services:
      cidrBlocks:
      - <service_cidr>
  controlPlaneRef:
    apiVersion: controlplane.cluster.x-k8s.io/v1beta1
    kind: KubeadmControlPlane
    name: <cluster_name>-kcp
  infrastructureRef:
    apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
    kind: VSphereCluster
    name: <cluster_name>
---
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: VSphereCluster
metadata:
  name: <cluster_name>
  namespace: <namespace>
spec:
  controlPlaneEndpoint:
    host: "<vip>"
    port: <api_server_port>
  identityRef:
    kind: Secret
    name: <credentials_secret_name>
  server: "<vsphere_server>"
  thumbprint: "<thumbprint>"

应用该 manifest:

kubectl apply -f 10-cluster.yaml

创建 vSphere CPI 下发资源

创建 ClusterResourceSet,以便在 workload API server 可访问后自动为 workload 集群提供 vSphere CPI 配置和 manifest。

INFO

在基线工作流中,VSphereCluster.spec.failureDomainSelector 故意未设置,CPI 的 vsphere.conf 中也不包含 [Labels] 块。只有在启用 failure domain 之后才需要这两者;请按照 多个 datacenter 和 failure domain 中的说明将它们一起配置。如果在未匹配 VSphereFailureDomain 对象的情况下向 vsphere.conf 添加 [Labels],CPI 会查找并不存在的 zone 和 region 标签。

INFO

vSphere CPI 的 TLS 绕过选项是 insecure-flag。请在 <cluster_name>-vsphere-cpi-config[Global] 部分保留 insecure-flag = "1",这样当 vCenter 证书为自签名或不被 workload 集群节点信任时,CPI 仍能连接。对于未设置自身 insecure-flag 的 vCenter 条目,CPI 会将全局值应用到它们。

WARNING

CPI ConfigMapSecretClusterResourceSet 资源 必须Cluster 资源位于相同命名空间。在本指南中,该命名空间是 cpaas-systemClusterResourceSet 只能匹配其所在命名空间中的集群;如果将其部署到其他命名空间,将会静默阻止资源下发。

INFO

Cluster 注解中的 kube-ovn 配置由平台控制器消费。本文档不会直接安装网络插件。

TIP

该 manifest 很长,并且在 data 字段中包含嵌套 YAML。应用前请先验证 manifest:kubectl apply --dry-run=client -f 15-vsphere-cpi-clusterresourceset.yaml

15-vsphere-cpi-clusterresourceset.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: <cluster_name>-vsphere-cpi-config
  namespace: <namespace>
data:
  data: |
    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: cloud-config
      namespace: kube-system
    data:
      vsphere.conf: |
        [Global]
        secret-name = "vsphere-cloud-secret"
        secret-namespace = "kube-system"
        service-account = "cloud-controller-manager"
        port = "443"
        insecure-flag = "1"
        datacenters = "<cpi_datacenters>"

        [VirtualCenter "<vsphere_server>"]
---
apiVersion: v1
kind: Secret
metadata:
  name: <cluster_name>-vsphere-cpi-secret
  namespace: <namespace>
type: addons.cluster.x-k8s.io/resource-set
stringData:
  data: |
    apiVersion: v1
    kind: Secret
    metadata:
      name: vsphere-cloud-secret
      namespace: kube-system
    type: Opaque
    stringData:
      <vsphere_server>.username: <vsphere_username>
      <vsphere_server>.password: <vsphere_password>
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: <cluster_name>-vsphere-cpi-manifests
  namespace: <namespace>
data:
  data: |
    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: cloud-controller-manager
      namespace: kube-system
    automountServiceAccountToken: false
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRole
    metadata:
      name: system:cloud-controller-manager
    rules:
    - apiGroups: [""]
      resources: ["events"]
      verbs: ["create", "patch", "update"]
    - apiGroups: [""]
      resources: ["nodes"]
      verbs: ["*"]
    - apiGroups: [""]
      resources: ["nodes/status"]
      verbs: ["patch"]
    - apiGroups: [""]
      resources: ["services"]
      verbs: ["list", "patch", "update", "watch"]
    - apiGroups: [""]
      resources: ["services/status"]
      verbs: ["patch"]
    - apiGroups: [""]
      resources: ["serviceaccounts"]
      verbs: ["create", "get", "list", "watch", "update"]
    - apiGroups: [""]
      resources: ["persistentvolumes"]
      verbs: ["get", "list", "update", "watch"]
    - apiGroups: [""]
      resources: ["endpoints"]
      verbs: ["create", "get", "list", "watch", "update"]
    - apiGroups: [""]
      resources: ["secrets"]
      verbs: ["get", "list", "watch"]
    - apiGroups: ["coordination.k8s.io"]
      resources: ["leases"]
      verbs: ["get", "list", "watch", "create", "update"]
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
      name: servicecatalog.k8s.io:apiserver-authentication-reader
      namespace: kube-system
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: Role
      name: extension-apiserver-authentication-reader
    subjects:
    - apiGroup: ""
      kind: ServiceAccount
      name: cloud-controller-manager
      namespace: kube-system
    - apiGroup: ""
      kind: User
      name: cloud-controller-manager
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRoleBinding
    metadata:
      name: system:cloud-controller-manager
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: ClusterRole
      name: system:cloud-controller-manager
    subjects:
    - kind: ServiceAccount
      name: cloud-controller-manager
      namespace: kube-system
    - kind: User
      name: cloud-controller-manager
    ---
    apiVersion: apps/v1
    kind: DaemonSet
    metadata:
      annotations:
        scheduler.alpha.kubernetes.io/critical-pod: ""
      labels:
        component: cloud-controller-manager
        tier: control-plane
        k8s-app: vsphere-cloud-controller-manager
      name: vsphere-cloud-controller-manager
      namespace: kube-system
    spec:
      selector:
        matchLabels:
          k8s-app: vsphere-cloud-controller-manager
      updateStrategy:
        type: RollingUpdate
      template:
        metadata:
          labels:
            component: cloud-controller-manager
            k8s-app: vsphere-cloud-controller-manager
        spec:
          securityContext:
            runAsUser: 1001
          automountServiceAccountToken: true
          # Optional: required when the CPI image is stored in a private
          # registry that needs authentication. The platform automatically
          # syncs a dockerconfigjson secret named "global-registry-auth"
          # into every namespace of the workload cluster when the
          # `global` cluster secret "public-registry-credential"
          # (data.content) is configured. If your environment does not
          # use a private registry, remove the imagePullSecrets block.
          imagePullSecrets:
          - name: global-registry-auth
          serviceAccountName: cloud-controller-manager
          hostNetwork: true
          tolerations:
          - operator: Exists
          - key: node.cloudprovider.kubernetes.io/uninitialized
            value: "true"
            effect: NoSchedule
          - key: node-role.kubernetes.io/master
            effect: NoSchedule
          - key: node.kubernetes.io/not-ready
            effect: NoSchedule
            operator: Exists
          containers:
          - name: vsphere-cloud-controller-manager
            image: <image_registry>/ait/cloud-provider-vsphere:<cpi_image_tag>
            args:
            - --v=2
            - --cloud-provider=vsphere
            - --cloud-config=/etc/cloud/vsphere.conf
            volumeMounts:
            - mountPath: /etc/cloud
              name: vsphere-config-volume
              readOnly: true
            resources:
              requests:
                cpu: 200m
          volumes:
          - name: vsphere-config-volume
            configMap:
              name: cloud-config
    ---
    apiVersion: v1
    kind: Service
    metadata:
      labels:
        component: cloud-controller-manager
      name: vsphere-cloud-controller-manager
      namespace: kube-system
    spec:
      type: NodePort
      ports:
      - port: 43001
        protocol: TCP
        targetPort: 43001
      selector:
        component: cloud-controller-manager
---
apiVersion: addons.cluster.x-k8s.io/v1beta1
kind: ClusterResourceSet
metadata:
  name: <cluster_name>-vsphere-cpi
  namespace: <namespace>
spec:
  strategy: Reconcile
  clusterSelector:
    matchLabels:
      addons.cluster.x-k8s.io/vsphere-cpi: "enabled"
  resources:
  - name: <cluster_name>-vsphere-cpi-config
    kind: ConfigMap
  - name: <cluster_name>-vsphere-cpi-secret
    kind: Secret
  - name: <cluster_name>-vsphere-cpi-manifests
    kind: ConfigMap

应用该 manifest:

kubectl apply -f 15-vsphere-cpi-clusterresourceset.yaml

创建 machine config pool

创建控制平面 machine config pool。

INFO

每个节点插槽都在 network.primary(必填)和 network.additional(可选列表)下声明其 NIC 布局。主 NIC 的 networkName 是必需的,provider 会根据 hostname 和解析后的主 NIC 地址推导 Kubernetes 节点名、kubelet serving certificate 的 DNS SAN 以及 kubelet 的 node-iphostname 必须是有效的 DNS-1123 子域名。

INFO

deviceName 是可选项。如果您不需要强制指定 guest NIC 名称,请从每个节点插槽中移除 deviceName 行。provider 会按 NIC 顺序分配 eth0eth1 等 NIC 名称。

WARNING

VSphereMachineConfigPool 中的 dns 条目会保留在静态网络配置中,但在某些受影响的 VMware 部署中,它们可能无法可靠更新 guest operating system 的 /etc/resolv.conf。因此,下面的控制平面和 worker bootstrap manifest 也会通过 kubeadm files 显式写入 /etc/resolv.conf

16-vspheremachineconfigpool-control-plane.yaml
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: VSphereMachineConfigPool
metadata:
  name: <cluster_name>-cp-pool
  namespace: <namespace>
spec:
  clusterRef:
    apiVersion: cluster.x-k8s.io/v1beta1
    kind: Cluster
    name: <cluster_name>
  datacenter: "<default_datacenter>"
  releaseDelayHours: <release_delay_hours>
  configs:
  - hostname: "<cp_node_name_1>"
    datacenter: "<master_01_datacenter>"
    network:
      primary:
        networkName: "<nic1_network_name>"
        deviceName: "<nic1_device_name>"
        ip: "<master_01_nic1_ip>/<nic1_prefix>"
        gateway: "<nic1_gateway>"
        dns:
        - "<nic1_dns_1>"
    persistentDisks:
    - name: var-cpaas
      sizeGiB: <cp_var_cpaas_size_gib>
      mountPath: /var/cpaas
      fsFormat: ext4
    - name: var-lib-containerd
      sizeGiB: <cp_var_lib_containerd_size_gib>
      mountPath: /var/lib/containerd
      fsFormat: ext4
    - name: var-lib-etcd
      sizeGiB: <cp_var_lib_etcd_size_gib>
      mountPath: /var/lib/etcd
      fsFormat: ext4
      wipeFilesystem: true
  - hostname: "<cp_node_name_2>"
    datacenter: "<master_02_datacenter>"
    network:
      primary:
        networkName: "<nic1_network_name>"
        deviceName: "<nic1_device_name>"
        ip: "<master_02_nic1_ip>/<nic1_prefix>"
        gateway: "<nic1_gateway>"
        dns:
        - "<nic1_dns_1>"
    persistentDisks:
    - name: var-cpaas
      sizeGiB: <cp_var_cpaas_size_gib>
      mountPath: /var/cpaas
      fsFormat: ext4
    - name: var-lib-containerd
      sizeGiB: <cp_var_lib_containerd_size_gib>
      mountPath: /var/lib/containerd
      fsFormat: ext4
    - name: var-lib-etcd
      sizeGiB: <cp_var_lib_etcd_size_gib>
      mountPath: /var/lib/etcd
      fsFormat: ext4
      wipeFilesystem: true
  - hostname: "<cp_node_name_3>"
    datacenter: "<master_03_datacenter>"
    network:
      primary:
        networkName: "<nic1_network_name>"
        deviceName: "<nic1_device_name>"
        ip: "<master_03_nic1_ip>/<nic1_prefix>"
        gateway: "<nic1_gateway>"
        dns:
        - "<nic1_dns_1>"
    persistentDisks:
    - name: var-cpaas
      sizeGiB: <cp_var_cpaas_size_gib>
      mountPath: /var/cpaas
      fsFormat: ext4
    - name: var-lib-containerd
      sizeGiB: <cp_var_lib_containerd_size_gib>
      mountPath: /var/lib/containerd
      fsFormat: ext4
    - name: var-lib-etcd
      sizeGiB: <cp_var_lib_etcd_size_gib>
      mountPath: /var/lib/etcd
      fsFormat: ext4
      wipeFilesystem: true

创建 worker machine config pool。

17-vspheremachineconfigpool-worker.yaml
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: VSphereMachineConfigPool
metadata:
  name: <cluster_name>-worker-pool
  namespace: <namespace>
spec:
  clusterRef:
    apiVersion: cluster.x-k8s.io/v1beta1
    kind: Cluster
    name: <cluster_name>
  datacenter: "<default_datacenter>"
  releaseDelayHours: <release_delay_hours>
  configs:
  - hostname: "<worker_node_name_1>"
    datacenter: "<worker_01_datacenter>"
    network:
      primary:
        networkName: "<nic1_network_name>"
        deviceName: "<nic1_device_name>"
        ip: "<worker_01_nic1_ip>/<nic1_prefix>"
        gateway: "<nic1_gateway>"
        dns:
        - "<nic1_dns_1>"
    persistentDisks:
    - name: var-cpaas
      sizeGiB: <worker_var_cpaas_size_gib>
      mountPath: /var/cpaas
      fsFormat: ext4
    - name: var-lib-containerd
      sizeGiB: <worker_var_lib_containerd_size_gib>
      mountPath: /var/lib/containerd
      fsFormat: ext4

应用这两个 manifest:

kubectl apply -f 16-vspheremachineconfigpool-control-plane.yaml
kubectl apply -f 17-vspheremachineconfigpool-worker.yaml

在创建 Machine 之前,请验证 pool 绑定和插槽状态:

kubectl -n <namespace> get vspheremachineconfigpool
kubectl -n <namespace> get vspheremachineconfigpool <cluster_name>-cp-pool -o yaml
kubectl -n <namespace> get vspheremachineconfigpool <cluster_name>-worker-pool -o yaml

该 pool 也是持久磁盘替换状态的权威来源。当 VM 被替换时,其插槽会从 InUse 变为 Released;引用同一个现有 pool 的替换 Machine 可以立即重用该插槽及其 VMDK。releaseDelayHours 控制的是插槽在未使用时的回收时机;它不是重用延迟。

不要在常规重试 Machine 替换时创建新的 pool 名称。不同的 VSphereMachineConfigPool.metadata.name 会创建彼此独立的插槽和 VMDK,因此反复删除和重新部署可能会暂时消耗多个完整磁盘集。在容量紧张的 datastore 上再次部署之前,请等待先前的 pool 及其 VMDK 完成 provider 管理的删除。有关清理检查和 status.configStatuses[].reclaimStatus 字段,请参见 持久磁盘生命周期

应用可选的 failure domain 对象

单 datacenter 基线拓扑请跳过此步骤。

如果已启用 failure domain,请先完成 多个 datacenter 和 failure domain,然后在创建控制平面之前应用 failure domain 对象:

kubectl apply -f 18-failure-domains.yaml
kubectl get vspherefailuredomain,vspheredeploymentzone

验证集群引用的每个 VSphereFailureDomainVSphereDeploymentZone 都已存在。在 deployment zone 可用之前不要继续。请将以下内容视为一组配置:

  • 18-failure-domains.yaml 中的 VSphereFailureDomainVSphereDeploymentZone 对象
  • 10-cluster.yaml 中的 VSphereCluster.spec.failureDomainSelector
  • 15-vsphere-cpi-clusterresourceset.yaml 中的 CPI [Labels]

当未启用 failure domain 时,不要在基线 manifest 中添加 failureDomainSelector 或 CPI [Labels] 块。

创建控制平面对象

创建 VSphereMachineTemplateKubeadmControlPlane 对象。将下面完整模板中的占位符替换为在检查清单文档中收集到的值。

Kubernetes 1.35 kubelet 设置

基线控制平面和 worker manifest 省略了 imagePullCredentialsVerificationPolicy,并且适用于 Kubernetes 1.34 或更早版本。

对于 Kubernetes 1.35 或更高版本,请在应用任一 manifest 之前,将以下字段添加到 20-control-plane.yaml30-workers-md-0.yaml 中的 KubeletConfiguration JSON:

"imagePullCredentialsVerificationPolicy": "NeverVerify",

cloneModediskGiB 都保留在模板中,因为 CAPV 会接受这两个字段。实际上,diskGiB 仅在实际克隆操作为 fullClone 时影响系统盘。如果 cloneModelinkedClone 且模板具有可用 snapshot,CAPV 会完成 linked clone,系统盘大小将保持与源模板一致。如果不存在可用 snapshot,CAPV 会回退到 fullClone,此时 diskGiB 再次生效。

系统盘和持久磁盘是分开的

VSphereMachineTemplate.spec.template.spec.diskGiB 只设置 VM 系统盘大小,并不是节点上所有磁盘的总容量。

数据盘在 VSphereMachineConfigPool.spec.configs[].persistentDisks[] 下单独声明。不要将持久磁盘大小加到 diskGiB 中;否则 VM 可能获得更大的系统盘以及单独的数据盘,从而使容量翻倍。

对于 fullClonediskGiB 必须大于或等于 OS image 模板中的系统盘大小。对于 linkedClone,系统盘保持为模板大小,diskGiB 被忽略。

20-control-plane.yaml
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: VSphereMachineTemplate
metadata:
  name: <cluster_name>-control-plane
  namespace: <namespace>
spec:
  template:
    spec:
      server: "<vsphere_server>"
      template: "<template_name>"
      cloneMode: <clone_mode>
      folder: "<vm_folder>"
      datastore: "<cp_system_datastore>"
      diskGiB: <cp_system_disk_gib>
      memoryMiB: <cp_memory_mib>
      numCPUs: <cp_num_cpus>
      os: Linux
      powerOffMode: <power_off_mode>
      network:
        devices:
        - networkName: "<nic1_network_name>"
      machineConfigPoolRef:
        apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
        kind: VSphereMachineConfigPool
        name: <cluster_name>-cp-pool
        namespace: <namespace>
---
apiVersion: controlplane.cluster.x-k8s.io/v1beta1
kind: KubeadmControlPlane
metadata:
  name: <cluster_name>-kcp
  namespace: <namespace>
spec:
  rolloutStrategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 0
  version: "<k8s_version>"
  replicas: <cp_replicas>
  machineTemplate:
    nodeDrainTimeout: 1m
    nodeDeletionTimeout: 5m
    metadata:
      labels:
        node-role.kubernetes.io/control-plane: ""
    infrastructureRef:
      apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
      kind: VSphereMachineTemplate
      name: <cluster_name>-control-plane
  kubeadmConfigSpec:
    users:
    - name: boot
      sudo: ALL=(ALL) NOPASSWD:ALL
      sshAuthorizedKeys:
      - "<ssh_public_key>"
    files:
    - path: /etc/resolv.conf
      owner: "root:root"
      append: false
      permissions: "0644"
      content: |
        nameserver <nic1_dns_1>
    - path: /etc/kubernetes/admission/psa-config.yaml
      owner: "root:root"
      permissions: "0644"
      content: |
        apiVersion: apiserver.config.k8s.io/v1
        kind: AdmissionConfiguration
        plugins:
        - name: PodSecurity
          configuration:
            apiVersion: pod-security.admission.config.k8s.io/v1
            kind: PodSecurityConfiguration
            defaults:
              enforce: "privileged"
              enforce-version: "latest"
              audit: "baseline"
              audit-version: "latest"
              warn: "baseline"
              warn-version: "latest"
            exemptions:
              usernames: []
              runtimeClasses: []
              namespaces:
              - kube-system
              - <namespace>
    - path: /etc/kubernetes/patches/kubeletconfiguration0+strategic.json
      owner: "root:root"
      permissions: "0644"
      content: |
        {
          "apiVersion": "kubelet.config.k8s.io/v1beta1",
          "kind": "KubeletConfiguration",
          "protectKernelDefaults": true,
          "streamingConnectionIdleTimeout": "5m",
          "tlsCertFile": "/etc/kubernetes/pki/kubelet.crt",
          "tlsPrivateKeyFile": "/etc/kubernetes/pki/kubelet.key"
        }
    # Generate the encryption key with: head -c 32 /dev/urandom | base64
    - path: /etc/kubernetes/encryption-provider.conf
      owner: "root:root"
      append: false
      permissions: "0644"
      content: |
        apiVersion: apiserver.config.k8s.io/v1
        kind: EncryptionConfiguration
        resources:
        - resources:
          - secrets
          providers:
          - aescbc:
              keys:
              - name: key1
                secret: <encryption_provider_secret>
    - path: /etc/kubernetes/audit/policy.yaml
      owner: "root:root"
      append: false
      permissions: "0644"
      content: |
        apiVersion: audit.k8s.io/v1
        kind: Policy
        omitStages:
        - "RequestReceived"
        rules:
        - level: None
          users:
          - system:kube-controller-manager
          - system:kube-scheduler
          - system:serviceaccount:kube-system:endpoint-controller
          verbs: ["get", "update"]
          namespaces: ["kube-system"]
          resources:
          - group: ""
            resources: ["endpoints"]
        - level: None
          nonResourceURLs:
          - /healthz*
          - /version
          - /swagger*
        - level: None
          resources:
          - group: ""
            resources: ["events"]
        - level: None
          resources:
          - group: "devops.alauda.io"
        - level: None
          verbs: ["get", "list", "watch"]
        - level: None
          resources:
          - group: "coordination.k8s.io"
            resources: ["leases"]
        - level: None
          resources:
          - group: "authorization.k8s.io"
            resources: ["subjectaccessreviews", "selfsubjectaccessreviews"]
          - group: "authentication.k8s.io"
            resources: ["tokenreviews"]
        - level: None
          resources:
          - group: "app.alauda.io"
            resources: ["imagewhitelists"]
          - group: "k8s.io"
            resources: ["namespaceoverviews"]
        - level: Metadata
          resources:
          - group: ""
            resources: ["secrets", "configmaps"]
        - level: Metadata
          resources:
          - group: "operator.connectors.alauda.io"
            resources: ["installmanifests"]
          - group: "operators.katanomi.dev"
            resources: ["katanomis"]
        - level: RequestResponse
          resources:
          - group: ""
          - group: "aiops.alauda.io"
          - group: "apps"
          - group: "app.k8s.io"
          - group: "authentication.istio.io"
          - group: "auth.alauda.io"
          - group: "autoscaling"
          - group: "asm.alauda.io"
          - group: "clusterregistry.k8s.io"
          - group: "crd.alauda.io"
          - group: "infrastructure.alauda.io"
          - group: "monitoring.coreos.com"
          - group: "operators.coreos.com"
          - group: "networking.istio.io"
          - group: "extensions.istio.io"
          - group: "install.istio.io"
          - group: "security.istio.io"
          - group: "telemetry.istio.io"
          - group: "opentelemetry.io"
          - group: "networking.k8s.io"
          - group: "portal.alauda.io"
          - group: "rbac.authorization.k8s.io"
          - group: "storage.k8s.io"
          - group: "tke.cloud.tencent.com"
          - group: "devopsx.alauda.io"
          - group: "core.katanomi.dev"
          - group: "deliveries.katanomi.dev"
          - group: "integrations.katanomi.dev"
          - group: "artifacts.katanomi.dev"
          - group: "builds.katanomi.dev"
          - group: "versioning.katanomi.dev"
          - group: "sources.katanomi.dev"
          - group: "tekton.dev"
          - group: "operator.tekton.dev"
          - group: "eventing.knative.dev"
          - group: "flows.knative.dev"
          - group: "messaging.knative.dev"
          - group: "operator.knative.dev"
          - group: "sources.knative.dev"
          - group: "operator.devops.alauda.io"
          - group: "flagger.app"
          - group: "jaegertracing.io"
          - group: "velero.io"
            resources: ["deletebackuprequests"]
          - group: "connectors.alauda.io"
          - group: "operator.connectors.alauda.io"
            resources: ["connectorscores", "connectorsgits", "connectorsocis"]
        - level: Metadata
    - path: /usr/local/bin/capv-load-local-images.sh
      owner: "root:root"
      permissions: "0755"
      content: |
        #!/bin/bash
        set -euo pipefail
        until mountpoint -q /var/lib/containerd; do
          echo "waiting for /var/lib/containerd mount"
          sleep 1
        done
        systemctl restart containerd
        until systemctl is-active --quiet containerd; do
          echo "waiting for containerd"
          sleep 1
        done
        if [ ! -d "/root/images" ]; then
          echo "ERROR: /root/images directory not found" >&2
          exit 1
        fi
        image_count=0
        for image_file in /root/images/*.tar; do
          if [ -f "$image_file" ]; then
            echo "importing image: $image_file"
            ctr -n k8s.io images import "$image_file"
            image_count=$((image_count + 1))
          fi
        done
        if [ "$image_count" -eq 0 ]; then
          echo "ERROR: no tar files found in /root/images" >&2
          exit 1
        fi
        echo "imported $image_count images"
    preKubeadmCommands:
    - hostnamectl set-hostname "{{ ds.meta_data.hostname }}"
    - echo "::1         ipv6-localhost ipv6-loopback localhost6 localhost6.localdomain6" >/etc/hosts
    - echo "127.0.0.1   {{ ds.meta_data.hostname }} {{ local_hostname }} localhost localhost.localdomain localhost4 localhost4.localdomain4" >>/etc/hosts
    - while ! ip route | grep -q "default via"; do sleep 1; done; echo "NetworkManager started"
    - /usr/local/bin/capv-load-local-images.sh
    postKubeadmCommands:
    - chmod 600 /var/lib/kubelet/config.yaml
    clusterConfiguration:
      imageRepository: <image_registry>/tkestack
      dns:
        imageTag: <dns_image_tag>
      etcd:
        local:
          imageTag: <etcd_image_tag>
      apiServer:
        extraArgs:
          admission-control-config-file: /etc/kubernetes/admission/psa-config.yaml
          audit-log-format: json
          audit-log-maxage: "30"
          audit-log-maxbackup: "10"
          audit-log-maxsize: "200"
          audit-log-mode: batch
          audit-log-path: /etc/kubernetes/audit/audit.log
          audit-policy-file: /etc/kubernetes/audit/policy.yaml
          encryption-provider-config: /etc/kubernetes/encryption-provider.conf
          kubelet-certificate-authority: /etc/kubernetes/pki/ca.crt
          profiling: "false"
          tls-cipher-suites: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
          tls-min-version: VersionTLS12
        extraVolumes:
        - hostPath: /etc/kubernetes
          mountPath: /etc/kubernetes
          name: vol-dir-0
          pathType: Directory
      controllerManager:
        extraArgs:
          bind-address: "::"
          cloud-provider: external
          flex-volume-plugin-dir: "/opt/libexec/kubernetes/kubelet-plugins/volume/exec/"
          profiling: "false"
          tls-min-version: VersionTLS12
      scheduler:
        extraArgs:
          bind-address: "::"
          profiling: "false"
          tls-min-version: VersionTLS12
    initConfiguration:
      nodeRegistration:
        criSocket: /var/run/containerd/containerd.sock
        ignorePreflightErrors:
        - ImagePull
        kubeletExtraArgs:
          cloud-provider: external
          node-labels: kube-ovn/role=master
          volume-plugin-dir: "/opt/libexec/kubernetes/kubelet-plugins/volume/exec/"
        name: '{{ local_hostname }}'
      patches:
        directory: /etc/kubernetes/patches
    joinConfiguration:
      nodeRegistration:
        criSocket: /var/run/containerd/containerd.sock
        ignorePreflightErrors:
        - ImagePull
        kubeletExtraArgs:
          cloud-provider: external
          node-labels: kube-ovn/role=master
          volume-plugin-dir: "/opt/libexec/kubernetes/kubelet-plugins/volume/exec/"
        name: '{{ local_hostname }}'
      patches:
        directory: /etc/kubernetes/patches

应用该 manifest:

kubectl apply -f 20-control-plane.yaml

创建 worker 对象

创建 worker machine template、bootstrap template 和 MachineDeployment

基线 worker kubelet patch 省略了仅 Kubernetes 1.35 才有的字段。如果所选版本是 Kubernetes 1.35 或更高版本,请像控制平面步骤中所述,在这里以及 20-control-plane.yaml 中都添加 imagePullCredentialsVerificationPolicy: NeverVerify

30-workers-md-0.yaml
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: VSphereMachineTemplate
metadata:
  name: <cluster_name>-worker
  namespace: <namespace>
spec:
  template:
    spec:
      server: "<vsphere_server>"
      template: "<template_name>"
      cloneMode: <clone_mode>
      folder: "<vm_folder>"
      datastore: "<worker_system_datastore>"
      diskGiB: <worker_system_disk_gib>
      memoryMiB: <worker_memory_mib>
      numCPUs: <worker_num_cpus>
      os: Linux
      powerOffMode: <power_off_mode>
      network:
        devices:
        - networkName: "<nic1_network_name>"
      machineConfigPoolRef:
        apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
        kind: VSphereMachineConfigPool
        name: <cluster_name>-worker-pool
        namespace: <namespace>
---
apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
kind: KubeadmConfigTemplate
metadata:
  name: <cluster_name>-worker-bootstrap
  namespace: <namespace>
spec:
  template:
    spec:
      files:
      - path: /etc/resolv.conf
        owner: "root:root"
        append: false
        permissions: "0644"
        content: |
          nameserver <nic1_dns_1>
      - path: /etc/kubernetes/patches/kubeletconfiguration0+strategic.json
        owner: "root:root"
        permissions: "0644"
        content: |
          {
            "apiVersion": "kubelet.config.k8s.io/v1beta1",
            "kind": "KubeletConfiguration",
            "protectKernelDefaults": true,
            "staticPodPath": null,
            "streamingConnectionIdleTimeout": "5m",
            "tlsCertFile": "/etc/kubernetes/pki/kubelet.crt",
            "tlsPrivateKeyFile": "/etc/kubernetes/pki/kubelet.key"
          }
      - path: /usr/local/bin/capv-load-local-images.sh
        owner: "root:root"
        permissions: "0755"
        content: |
          #!/bin/bash
          set -euo pipefail
          until mountpoint -q /var/lib/containerd; do
            echo "waiting for /var/lib/containerd mount"
            sleep 1
          done
          systemctl restart containerd
          until systemctl is-active --quiet containerd; do
            echo "waiting for containerd"
            sleep 1
          done
          if [ ! -d "/root/images" ]; then
            echo "ERROR: /root/images directory not found" >&2
            exit 1
          fi
          image_count=0
          for image_file in /root/images/*.tar; do
            if [ -f "$image_file" ]; then
              echo "importing image: $image_file"
              ctr -n k8s.io images import "$image_file"
              image_count=$((image_count + 1))
            fi
          done
          if [ "$image_count" -eq 0 ]; then
            echo "ERROR: no tar files found in /root/images" >&2
            exit 1
          fi
          echo "imported $image_count images"
      joinConfiguration:
        nodeRegistration:
          criSocket: /var/run/containerd/containerd.sock
          ignorePreflightErrors:
          - ImagePull
          kubeletExtraArgs:
            cloud-provider: external
            volume-plugin-dir: "/opt/libexec/kubernetes/kubelet-plugins/volume/exec/"
          name: '{{ local_hostname }}'
        patches:
          directory: /etc/kubernetes/patches
      preKubeadmCommands:
      - hostnamectl set-hostname "{{ ds.meta_data.hostname }}"
      - echo "::1         ipv6-localhost ipv6-loopback localhost6 localhost6.localdomain6" >/etc/hosts
      - echo "127.0.0.1   {{ ds.meta_data.hostname }} {{ local_hostname }} localhost localhost.localdomain localhost4 localhost4.localdomain4" >>/etc/hosts
      - while ! ip route | grep -q "default via"; do sleep 1; done; echo "NetworkManager started"
      - /usr/local/bin/capv-load-local-images.sh
      postKubeadmCommands:
      - chmod 600 /var/lib/kubelet/config.yaml
      users:
      - name: boot
        sudo: ALL=(ALL) NOPASSWD:ALL
        sshAuthorizedKeys:
        - "<ssh_public_key>"
---
apiVersion: cluster.x-k8s.io/v1beta1
kind: MachineDeployment
metadata:
  name: <cluster_name>-md-0
  namespace: <namespace>
spec:
  clusterName: <cluster_name>
  replicas: <worker_replicas>
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 0
      maxUnavailable: 1
  selector:
    matchLabels:
      nodepool: md-0
  template:
    metadata:
      labels:
        cluster.x-k8s.io/cluster-name: <cluster_name>
        nodepool: md-0
    spec:
      clusterName: <cluster_name>
      version: "<k8s_version>"
      bootstrap:
        configRef:
          apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
          kind: KubeadmConfigTemplate
          name: <cluster_name>-worker-bootstrap
      infrastructureRef:
        apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
        kind: VSphereMachineTemplate
        name: <cluster_name>-worker

应用该 manifest:

kubectl apply -f 30-workers-md-0.yaml

在基线工作流中,请注意以下 worker 专用规则:

  • 主 worker manifest 默认不设置 failureDomain,因为基线工作流假定只有一个 datacenter。如果需要让 worker MachineDeployment 落在特定的 VSphereDeploymentZone 中,请按照 多个 datacenter 和 failure domain 的说明添加 failureDomain
  • 某些环境会在 KubeadmConfigTemplate 中添加额外的 runtime-image 替换命令或服务重启命令。这些命令有意未包含在基线示例中。只有在您环境中的平台要求明确需要时才添加。

等待集群就绪

应用所有 manifest 后,集群创建是异步的。请使用以下命令监控进度:

kubectl -n <namespace> get cluster,kubeadmcontrolplane,machinedeployment,machine -w

在继续验证之前,请等待 KubeadmControlPlane 报告预期数量的 ready replicas,并确保所有 Machine 对象都进入 Running 阶段。

验证

使用以下命令验证集群创建流程。

  1. 检查 global 集群中的 CPI 下发资源:
    kubectl -n <namespace> get clusterresourceset
    kubectl -n <namespace> get clusterresourcesetbinding
  2. 导出 workload kubeconfig:
    kubectl -n <namespace> get secret <cluster_name>-kubeconfig -o jsonpath='{.data.value}' | base64 -d > /tmp/<cluster_name>.kubeconfig
  3. 检查 workload 集群中是否已创建 vSphere CPI daemonset:
    kubectl --kubeconfig=/tmp/<cluster_name>.kubeconfig -n kube-system get daemonset
  4. 检查 global 集群对象:
    kubectl -n <namespace> get cluster,vspherecluster,kubeadmcontrolplane,machinedeployment,machine,vspheremachine,vspherevm
  5. 检查 workload 节点:
    kubectl --kubeconfig=/tmp/<cluster_name>.kubeconfig get nodes -o wide

确认以下结果:

  • vsphere-cloud-controller-manager 出现在 workload 集群中。
  • 控制平面和 worker 节点已创建。
  • 节点最终变为 Ready

故障排查

当工作流失败时,首先使用以下命令:

kubectl -n <namespace> describe cluster <cluster_name>
kubectl -n <namespace> describe vspherecluster <cluster_name>
kubectl -n <namespace> describe kubeadmcontrolplane <cluster_name>-kcp
kubectl -n <namespace> describe machinedeployment <cluster_name>-md-0
kubectl -n <namespace> get cluster,vspherecluster,kubeadmcontrolplane,machinedeployment,machine,vspheremachine,vspherevm
kubectl -n cpaas-system logs deploy/capi-controller-manager

优先检查以下内容:

  • 如果 CPI 资源未被下发,请验证 ClusterResourceSet=trueClusterResourceSetClusterResourceSetBinding
  • 如果 ClusterResourceSet 存在但没有创建 ClusterResourceSetBinding,请检查 controller 是否对所引用的 ConfigMapSecret 资源具有所需的 delete 权限。
  • 如果网络插件未安装,请验证所需的集群注解是否存在,以及平台控制器是否已处理它们。
  • 如果 cpaas.io/registry-address 注解缺失或不正确,请验证其是否已设置为永久 <registry_address>,且不包含 scheme 或 /v2/。同时还要验证 public-registry-credential 是否存在,以及平台控制器是否已处理该注解。
  • 如果某个 machine 卡在 Provisioning,请检查 VSphereMachine 条件中的 MachineConfigPoolReady——它会显示插槽分配是否因 pool 绑定或 datacenter 不匹配而失败。
  • 如果某个 VM 正在等待 IP 分配,请验证 VMware Tools、静态 IP 设置以及 VSphereVM.status.addresses
  • 如果 workload Node 对象一直没有 spec.providerID,请先验证 CPI 下发资源,然后检查是否存在重复的 vCenter guest 主机名。当同一 datacenter 中的旧 VM 仍然报告与新节点相同的 guest 主机名时,cloud-provider-vsphere 可能会回退到按节点名查找、缓存旧 VM,并因 VM IP 与 kubelet node IP 不匹配而拒绝新节点。请检查 leader vsphere-cloud-controller-manager 日志、节点 SystemUUID、真实 VM UUID,以及 vCenter guest 主机名/IP 值。在修复或移除重复主机名或旧 VM 冲突后,请重启 workload 集群的 vsphere-cloud-controller-manager Pod,以清除错误的内存缓存:
    kubectl --kubeconfig=/tmp/<cluster_name>.kubeconfig -n kube-system delete pod \
      -l k8s-app=vsphere-cloud-controller-manager
  • 如果 datastore 空间耗尽,请先列出所有当前和正在终止的 VSphereMachineConfigPool 对象,并将它们的名称与每个 VSphereMachineTemplate.spec.template.spec.machineConfigPoolRef.name 进行比较。不同的 pool 名称拥有彼此独立的插槽和 VMDK;因此重复部署可能会累积多个磁盘集。然后检查 status.configStatuses[],包括 statelastReleasedTimereclaimStatus.statevolumePathlastErrorretryAfter,以及相关 Events 和 vCenter attachment 状态。insufficient disk available 可以确认 datastore 容量失败,但它本身并不能证明回收失败。在重用或 provider 管理的回收仍在等待期间,不要手动删除 VMDK,也不要移除 pool finalizer。
  • 如果模板系统盘大小与 manifest 值不一致,先检查实际克隆模式。当 VM 以 linkedClone 创建时,系统盘保持模板大小,diskGiB 被忽略。只有 fullClone 才会使用 diskGiB,并且在这种情况下 diskGiB 不能小于模板磁盘大小。
  • 如果控制平面端点未启动,请验证 TCP 6443 passthrough、所有控制平面 backend、HTTPS /healthz 结果、DNS 和证书 SAN,以及来自 global 集群和控制平面节点的可达性。
  • 如果到 vCenter 的 TLS 连接失败,请验证 thumbprint、vCenter 地址,以及是否有 proxy 设置干扰连接。

查看 controller 日志时,请遵循以下规则:

  • deploy/capi-controller-manager 运行在 global 集群的 cpaas-system 命名空间中。
  • 不要使用 workload 集群的 kubeconfig 来查看 capi-controller-manager 日志。
  • 如果平台控制器处理了集群网络注解,还要检查平台 network-controller 日志和平台 cluster-lifecycle-controller 日志。

创建时拓扑变体

基线 manifest 故意从一个 datacenter 和一个 NIC 开始。在创建集群之前,如果初始拓扑需要额外的 NIC、多个 datacenter、failure domain 或额外的数据盘,请使用以下变体。一次只应用一个变体,并在组合之前验证完整的 manifest 集合。

添加第二个 NIC

当节点需要额外的管理、存储或服务网络时,请扩展以下资源中的 manifest:

  • 16-vspheremachineconfigpool-control-plane.yaml
  • 17-vspheremachineconfigpool-worker.yaml
  • 20-control-plane.yaml
  • 30-workers-md-0.yaml
  • 如果已启用 failure domain,则还包括 18-failure-domains.yaml

每个节点插槽都在 network.primarynetwork.additional 下声明其 NIC 布局。主 NIC 用于推导 kubelet node-ip,并保持为节点的主身份;附加 NIC 会按列表顺序在其后合并。

将第二个 NIC 添加到 machine config pools 中的每个控制平面节点插槽:

network:
  primary:
    networkName: "<nic1_network_name>"
    deviceName: "<nic1_device_name>"
    ip: "<master_01_nic1_ip>/<nic1_prefix>"
    gateway: "<nic1_gateway>"
    dns:
    - "<nic1_dns_1>"
  additional:
  - networkName: "<nic2_network_name>"
    deviceName: "<nic2_device_name>"
    ip: "<master_01_nic2_ip>/<nic2_prefix>"
    gateway: "<nic2_gateway>"
    dns:
    - "<nic2_dns_1>"

将相同模式应用到 worker 节点插槽:

network:
  primary:
    networkName: "<nic1_network_name>"
    deviceName: "<nic1_device_name>"
    ip: "<worker_01_nic1_ip>/<nic1_prefix>"
    gateway: "<nic1_gateway>"
    dns:
    - "<nic1_dns_1>"
  additional:
  - networkName: "<nic2_network_name>"
    deviceName: "<nic2_device_name>"
    ip: "<worker_01_nic2_ip>/<nic2_prefix>"
    gateway: "<nic2_gateway>"
    dns:
    - "<nic2_dns_1>"

将第二个 NIC 添加到 machine templates:

network:
  devices:
  - networkName: "<nic1_network_name>"
  - networkName: "<nic2_network_name>"

如果添加第二个 NIC 后节点使用的 DNS server 发生变化,请更新 20-control-plane.yaml30-workers-md-0.yaml 中的 /etc/resolv.conf 文件条目。machine config pool 网络块中的 dns 值不会替换 bootstrap 文件中显式的 /etc/resolv.conf 条目。

如果已启用 failure domain,请更新 VSphereFailureDomain.spec.topology.networks 中的网络列表:

topology:
  networks:
  - <nic1_network_name>
  - <nic2_network_name>

定义第二个 NIC 的值时,请在基础设施检查清单和 manifest 中准备以下占位符:

  • <master_01_nic2_ip>
  • <master_02_nic2_ip>
  • <master_03_nic2_ip>
  • <worker_01_nic2_ip>
  • 如果您同时扩展 worker pool,还需要 <worker_02_nic2_ip>

对于正在运行的集群,请在不可变替换期间同时更改这三项网络定义。请参见 运行时拓扑变更

多个 datacenter 和 failure domain

当需要在不同的 vCenter datacenter 或计算集群之间进行节点放置时,请使用多个 datacenter 和 failure domain。

适用以下原则:

  • 一个集群可以定义多个 VSphereFailureDomain 对象。
  • 每个 VSphereDeploymentZone 引用一个 VSphereFailureDomain
  • 控制平面使用 VSphereCluster.spec.failureDomainSelector
  • 当 worker MachineDeployment 必须面向特定 deployment zone 时,使用 spec.template.spec.failureDomain

为第一个 datacenter 准备以下占位符:

  • <compute_cluster_1>
  • <default_datastore_1>
  • <resource_pool_path_1>
  • <fd_name_1>
  • <dz_name_1>

为第二个 datacenter 准备以下占位符:

  • <dc_name_2>
  • <fd_name_2>
  • <dz_name_2>
  • <compute_cluster_2>
  • <default_datastore_2>
  • <resource_pool_path_2>

如果您添加第三个 datacenter,请继续使用相同的占位符模式:

  • <dc_name_3>
  • <fd_name_3>
  • <dz_name_3>
  • <compute_cluster_3>
  • <default_datastore_3>
  • <resource_pool_path_3>

18-failure-domains.yaml 中创建 failure domain 对象。启用 failure domain 时,第一个 datacenter 也需要 VSphereFailureDomainVSphereDeploymentZone

apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: VSphereFailureDomain
metadata:
  name: <fd_name_1>
spec:
  region:
    name: region-a
    type: Datacenter
    tagCategory: k8s-region
    autoConfigure: true
  zone:
    name: zone-1
    type: ComputeCluster
    tagCategory: k8s-zone
    autoConfigure: true
  topology:
    datacenter: <default_datacenter>
    computeCluster: <compute_cluster_1>
    datastore: <default_datastore_1>
    networks:
    - <nic1_network_name>
---
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: VSphereDeploymentZone
metadata:
  name: <dz_name_1>
spec:
  server: <vsphere_server>
  failureDomain: <fd_name_1>
  controlPlane: true
  placementConstraint:
    resourcePool: <resource_pool_path_1>
---
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: VSphereFailureDomain
metadata:
  name: <fd_name_2>
spec:
  region:
    name: region-a
    type: Datacenter
    tagCategory: k8s-region
    autoConfigure: true
  zone:
    name: zone-2
    type: ComputeCluster
    tagCategory: k8s-zone
    autoConfigure: true
  topology:
    datacenter: <dc_name_2>
    computeCluster: <compute_cluster_2>
    datastore: <default_datastore_2>
    networks:
    - <nic1_network_name>
---
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: VSphereDeploymentZone
metadata:
  name: <dz_name_2>
spec:
  server: <vsphere_server>
  failureDomain: <fd_name_2>
  controlPlane: true
  placementConstraint:
    resourcePool: <resource_pool_path_2>

通过在 10-cluster.yamlVSphereCluster spec 中添加 failureDomainSelector,启用在可用 failure domain 之间选择控制平面:

apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: VSphereCluster
metadata:
  name: <cluster_name>
  namespace: <namespace>
spec:
  controlPlaneEndpoint:
    host: "<vip>"
    port: <api_server_port>
  identityRef:
    kind: Secret
    name: <credentials_secret_name>
  server: "<vsphere_server>"
  thumbprint: "<thumbprint>"
  failureDomainSelector: {}

空选择器 {} 会匹配所有 controlPlane: trueVSphereDeploymentZone。使用 match labels 将控制平面限制在某个 zone 子集内。

同时还要在 CPI ConfigMap 中添加 [Labels] 块,以便 vSphere CPI 在 workload 节点上发布匹配的 zone 和 region 标签。键必须与 VSphereFailureDomain.spec.zone.tagCategoryVSphereFailureDomain.spec.region.tagCategory 中使用的 tagCategory 值一致。更新 15-vsphere-cpi-clusterresourceset.yaml 中的 vsphere.conf 数据:

      vsphere.conf: |
        [Global]
        secret-name = "vsphere-cloud-secret"
        secret-namespace = "kube-system"
        service-account = "cloud-controller-manager"
        port = "443"
        insecure-flag = "1"
        datacenters = "<cpi_datacenters>"

        [Labels]
        zone = "k8s-zone"
        region = "k8s-region"

        [VirtualCenter "<vsphere_server>"]

failureDomainSelector 和 CPI [Labels] 块必须一起启用。单独添加其中任意一个都会使集群处于不一致状态:节点会获得无法解析的 zone 或 region 标签,或者控制平面无法选择 deployment target。

当 worker MachineDeployment 必须固定到某个 deployment target 时,请设置 worker deployment zone。在 30-workers-md-0.yamlspec.template.spec 中添加 failureDomain

apiVersion: cluster.x-k8s.io/v1beta1
kind: MachineDeployment
metadata:
  name: <cluster_name>-md-0
  namespace: <namespace>
spec:
  clusterName: <cluster_name>
  replicas: <worker_replicas>
  template:
    spec:
      clusterName: <cluster_name>
      failureDomain: <worker_failure_domain>
      version: "<k8s_version>"
      # ... rest of spec

<worker_failure_domain> 应使用 VSphereDeploymentZone 名称,而不是 VSphereFailureDomain 名称。

在启用多个 datacenter 之前,请确认以下所有先决条件:

  1. 模板已同步到每个目标 datacenter。
  2. 网络名称在每个目标 datacenter 中都可解析。
  3. datastore 名称在每个目标 datacenter 中都可解析。
  4. vSphere CPI datacenter 列表覆盖每个目标 datacenter。

添加数据盘

基线部署包含以下必需数据盘:

  • 控制平面节点var-cpaasvar-lib-containerdvar-lib-etcd(每个节点 3 块盘)。不要移除这些磁盘中的任何一个。var-lib-etcd 磁盘必须设置 wipeFilesystem: true,以便在滚动更新期间允许 kubeadm join
  • worker 节点var-cpaasvar-lib-containerd(每个节点 2 块盘)。不要移除这些磁盘中的任何一个。

VSphereMachineTemplate.spec.template.spec.diskGiB 是系统盘大小,而不是 VM 的总磁盘容量。请将额外的持久磁盘或数据盘保留在 VSphereMachineConfigPool.spec.configs[].persistentDisks[] 中。不要将持久磁盘大小加到 diskGiB 中,除非您确实希望系统盘变得更大。

如果节点需要除必需集合之外的更多数据盘,请在相应 VSphereMachineConfigPool 节点插槽中的同一个 persistentDisks 列表里追加更多条目。以下可选字段在这里尤其相关:

  • mountPath:如果设置,磁盘将被格式化并挂载到指定路径。如果未设置,磁盘将作为原始设备附加,并在 /dev/disk/by-capv/<name> 处创建符号链接,允许外部进程在运行时管理它。
  • wipeFilesystem:当为 true 时,新 VM 的首次启动会擦除磁盘内容。正常重启和手动服务重启不会受影响。默认为 false

如需附加原始磁盘而不格式化或挂载,请省略 mountPathfsFormat

persistentDisks:
# ...required disks...
- name: app-data
  sizeGiB: 50

该磁盘在 guest operating system 内可通过 /dev/disk/by-capv/app-data 访问。在滚动更新期间,同一个 VMDK 会重新附加到新 VM,并重新创建符号链接。磁盘不会被自动格式化或挂载;应用需要在运行时自行管理它。

验证创建时变体

在应用拓扑变体后,请使用以下命令验证集群状态:

kubectl -n <namespace> get cluster,vspherecluster,kubeadmcontrolplane,machinedeployment,machine,vspheremachine,vspherevm
kubectl --kubeconfig=/tmp/<cluster_name>.kubeconfig get nodes -o wide

确认以下结果:

  • 放置、NIC 或磁盘定义已反映到目标资源中。
  • 新节点进入 Ready 状态。
  • 现有节点在变更后仍保持健康。

下一步

有关 worker 扩容以及后续模板或运行时拓扑变更,请参见 在 VMware vSphere 上管理节点。请一次只应用一项变更,并在将多项变更组合到同一集群之前验证结果。