在 VMware vSphere 上创建集群

本文介绍如何通过向 ACP global 管理集群应用 Cluster API 清单来创建 VMware vSphere 业务集群。该操作步骤不会创建 global 集群本身。本文涵盖最低支持的拓扑:一个数据中心、每个节点一个 NIC,以及通过 VSphereMachineConfigPool 分配静态 IP。本页介绍的 provider 行为和字段已针对 VMware vSphere Provider v1.0.17 完成验证。

Provider 要求

从管理集群上的 provider AppRelease 中读取已安装的 provider 版本;不要根据 ACP 版本推断 provider 版本。请参阅识别已安装的 Provider 版本。

当 controller 引用的 VSphereMachineTemplate 设置 machineConfigPoolRef(即本页介绍的固定 IP 拓扑)时,适用以下规则。provider 的准入 webhook 会在 CREATE 和 UPDATE 上强制执行这些规则;最后一条规则在 DELETE 上强制执行。

  • KubeadmControlPlane.spec.rolloutStrategy.rollingUpdate.maxSurge 是整数 0,而 spec.replicas 是 3 或更大值。未设置 maxSurge 表示使用 Cluster API 默认值 1,该值不被接受。
  • MachineDeployment.spec.strategy.rollingUpdate.maxSurge 是 0,而 maxUnavailable 是 1 或更大值。strategy.type: OnDelete 不受这两条规则限制。
  • VSphereMachineConfigPool.spec.configs 至少声明一个插槽,并且每个插槽都声明 network。绑定到同一 Cluster 的所有池中的主机名和主 IP 地址必须唯一,并且一个池仅服务一个 controller。
  • 已处于 InUse 或 Released 状态的插槽只能接受对任一磁盘列表的新增,不能进行其他修改。请参阅已分配的插槽不可变。
  • 仍拥有 InUse 插槽的池无法删除。

使用验证安装中的 webhook 检查,确认 webhook 已在环境中注册。如果 webhook 未注册,仍然必须满足相同的值要求:固定 IP 插槽模型没有可用于临时扩容机器的空闲插槽,因此不符合要求的 rollout 会停滞,而不是在开始时直接被拒绝。

使用场景

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

  • 你要在环境中创建第一个基线 VMware vSphere 业务集群。
  • 初始验证时使用一个数据中心,并且每个节点使用一个 NIC。
  • 你想先简化首次部署,然后再启用高级放置或网络功能。

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

  • CAPV 直接连接到 vCenter。
  • 控制平面和工作节点均使用 VSphereMachineConfigPool 分配静态 IP 和数据磁盘。
  • ClusterResourceSet 自动交付 vSphere CPI 组件。
  • 初始验证使用一个数据中心,并且每个节点使用一个 NIC。

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

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

本文档面向当前平台环境编写。kube-ovn 交付路径依赖于能够处理 Cluster 资源注解的平台控制器,因此本工作流并非适用于平台环境之外的通用独立 CAPV 部署指南。

如何使用本页

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

基线是验证参考。如果目标集群需要多个可选拓扑功能,请先逐一引入并验证清单变更,再将它们组合起来。

先决条件

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

  1. 已完成 VMware vSphere 基础设施准备。
  2. global 集群能够连接到 vCenter。
  3. 目标模板、网络、数据存储和 vCenter 资源池均可用。
  4. 已选择控制平面端点模式。对于外部 LoadBalancer,其满足端点约定。对于 Self-built VIP,已满足 Self-built VIP 先决条件。集群创建后无法更改模式,因此请现在做出决定:请参阅控制平面端点模式。
  5. 所有必需的静态 IP 地址均已分配、未被使用,并且未由绑定到同一 Cluster 的其他 VSphereMachineConfigPool 中的插槽声明。
  6. 已启用 ClusterResourceSet=true。
  7. 已确定此集群使用的镜像仓库。默认情况下,集群会继承 global 集群的镜像仓库;如果该镜像仓库需要身份验证,则必须提供有效的 public-registry-credential Secret。若要改用专用镜像仓库拉取平台镜像,请在创建集群前按照为业务集群选择镜像仓库操作;创建后无法更改绑定。
  8. 所选镜像仓库可从业务节点访问,并且包含所需的 Kube-OVN、CoreDNS 和 vSphere CPI 标签。
  9. 平台能够处理安装网络插件所需的集群注解。
  10. VSphereCluster.spec.identityRef 引用的 vCenter 账户满足 VMware vSphere Provider 安装中关于 Custom Fields 特权的要求。
  11. global 集群是活动管理集群,而非灾难恢复备用集群。请参阅备用管理集群。
  12. 已规划任何专用节点池的节点标签、注解和污点。如在节点上配置注解、标签和污点所述,这些配置在节点池的资源上声明;只有在 capi-controller-manager 上启用标签同步后,Machine 标签才会传递到其 Node。

控制平面端点模式

每个集群都通过稳定的端点访问其 API server。请在编写 10-cluster.yaml 之前选择模式,因为之后无法更正。该选择会记录在 VSphereCluster.spec.controlPlaneLoadBalancer 中,而省略此字段本身也是一种选择:字段缺失表示使用外部 LoadBalancer。

Self-built VIP 仅可在创建时选择

Self-built VIP 从 provider v1.0.17 开始提供,需要 ACP v4.4 或更高版本,并且仅适用于使用该模式创建的集群。现有集群无法采用该模式,从早期版本将 provider 升级到 v1.0.17 的集群也无法采用:VIP 会在第一个控制平面节点引导时注入,然后由 Alive 接管,因此不存在就地迁移路径。这些集群会继续使用外部 LoadBalancer;要将其迁移到 Self-built VIP,必须创建新集群。请参阅升级 Provider。

spec.controlPlaneLoadBalancer端点的所有者
缺失(本页中的基线)由你负责。预配外部 Layer 4 TCP LoadBalancer,并自行填写 spec.controlPlaneEndpoint。
type: external由你负责。provider 会根据 spec.controlPlaneEndpoint 检查该值,并且不会部署 VIP 组件。
type: internal由 provider 负责。provider 会在第一个控制平面节点上注入引导 VIP,然后将 Alive(keepalived 和 IPVS)安装到业务集群中,并在控制平面节点上持有该 VIP。

集群创建后,三种模式均无法更改。该字段只能写入一次;请参阅controlPlaneLoadBalancer 字段。

建议:对于外部 LoadBalancer,请省略该字段,而不是填写 type: external。字段缺失已经表示使用外部 LoadBalancer,填写该字段仅用于记录这一意图,却会将端点固定到同一地址的冻结副本。

对于 type: internal,先决条件、字段参考、清单块和验证命令请参阅使用 Self-built VIP。

Key Objects

VMware vSphere Cluster API 资源依赖关系

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

阶段输入应用预期结果优先诊断项
端点和 RegistryLoadBalancer、永久 Registry、版本集验证外部依赖项。从所需网络可以访问端点和 Registry。LoadBalancer 健康状况、DNS、Registry API 和 ACP 端口规则。
集群根资源网络 CIDR、vCenter 身份、端点应用 Cluster 和 VSphereCluster。两个资源均存在,且报告正在推进的状态。Cluster 和 VSphereCluster 状态。
CPI 部署vCenter CPI 配置和凭证应用 ClusterResourceSet 资源。workload 集群的 API 可访问后,集群会收到 vSphere CPI。ClusterResourceSetBinding 和 CPI Pods。
节点槽位主机名、静态 IP、数据存储、持久磁盘应用两个 VSphereMachineConfigPool 对象。每个池都处于 Ready 状态,并公开预期的槽位。池状态和 status.configStatuses。
控制平面VM 模板、kubeadm 配置、Kubernetes 版本应用 VSphereMachineTemplate 和 KubeadmControlPlane。三个控制平面 Machine 均变为 Ready。KCP、Machine、VSphereMachine、VSphereVM 和端点健康状况。
WorkerWorker 模板和引导配置应用 KubeadmConfigTemplate 和 MachineDeployment。Worker Machine 和节点均变为 Ready。MachineDeployment 发布状态、VM 状态、CPI 和 CNI。

ClusterResourceSet

ClusterResourceSet 是 global 集群中的 Cluster API 资源。workload API server 可访问后,它会将引用的 ConfigMap 和 Secret 资源应用到 workload 集群。

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

vSphere CPI 组件

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

清单中由 Provider 管理的字段

应用的对象中有些字段在创建后由 provider 管理。了解这些字段可以避免升级时发生仓库冲突。

  • 应用 KubeadmControlPlane 后,provider 会对其进行修补。CoreDNS 和 kube-proxy 跳过注解应设为 "true",而 spec.kubeadmConfigSpec.clusterConfiguration.dns.imageRepository 应由控制器设置。
  • 自行设置 dns.imageTag,并将 dns.imageRepository 留空。 提供目标 ACP 版本发布的 CoreDNS 标签:ACP v4.4 及更高版本使用暂存于 acp/coredns 下的标签;低于 v4.4 的版本使用 tkestack/coredns 下的标签。provider 会读取标签中的 -v<acp> 后缀来选择相应仓库,因此不带该后缀的标签会解析到 tkestack/coredns ——在 ACP v4.4 或更高版本中,该仓库不包含此标签,CoreDNS Pod 会停留在 ImagePullBackOff。
  • kube-proxy 仓库根据 Kubernetes 版本确定:acp/k8s 适用于 v1.35 及更高版本,低于该版本则使用 tkestack。Kube-OVN chart 在 chart v4.4 及更高版本中为 acp/chart-kube-ovn,低于该版本则为 acp/chart-cpaas-kube-ovn。
  • 这三者的 Registry 前缀均来自 Cluster 注解 cpaas.io/registry-address。
  • workload 集群的 kube-system/kube-proxy DaemonSet 和 kube-system/coredns Deployment 镜像仓库也会协调更新,但仅在控制平面发布完成后。KubeadmControlPlane 达到目标版本与这些工作负载更改仓库之间出现延迟是正常现象。
  • 这些受管组件的镜像拉取凭证来自 workload 集群的 cpaas-system/sentry ServiceAccount。
WARNING

请勿删除 CoreDNS 和 kube-proxy 跳过注解,也不要重新应用缺少这些注解的已存储 KubeadmControlPlane 清单。在 provider 同时管理 CoreDNS 或 kube-proxy 时,将其交还给 kubeadm 管理会导致仓库冲突,使镜像来回切换。

machine config pool

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

  • 控制平面节点使用一个 machine config pool。
  • Worker 节点使用一个 machine config pool。

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

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

  • networkName 是 vCenter 网络或端口组名称。
  • deviceName 是来宾操作系统内部的 NIC 名称。

如果设置了 deviceName,CAPV 会将该值写入生成的来宾网络元数据。如果省略该字段,当前实现通常会按 NIC 顺序使用 eth0、eth1 和 eth2 等 NIC 名称。

还需区分以下值的格式:

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

spec.configs 必须至少声明一个槽位,并且每个槽位都必须声明 network,且其 network.primary.networkName 不得为空。

每个槽位都可以声明两个磁盘列表。persistentDisks[] 会在 VM 替换后保留,并重新挂载到替换旧 VM 的 Machine 上;ephemeralDisks[] 会随 VM 一起删除,并在重建时为空。标准集合将 /var/cpaas 声明为持久磁盘,并将 /var/lib/kubelet、/var/lib/containerd 和 /var/lib/etcd 声明为临时磁盘。在同一槽位的两个列表中,磁盘名称和挂载路径共用一个命名空间。ephemeralDisks[] 在 provider v1.0.17 中新增,因此只有在 v1.0.17 或更高版本上创建的集群才能使用此划分方式。请参阅选择持久磁盘或临时磁盘。

一个池恰好服务于一个控制器。 Machine 分配槽位时,provider 会在 status.consumerRef 中记录所属的 KubeadmControlPlane 或 MachineDeployment。模板引用同一池的第二个控制器不会被接受。控制平面使用一个池,每个 MachineDeployment 使用一个池。

VM 模板要求

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

  1. 使用目标平台环境所需的操作系统。
  2. 包含 cloud-init。
  3. 包含 VMware Tools 或 open-vm-tools。
  4. 包含 containerd。
  5. 包含 kubeadm 引导所需的基线组件。
  6. 在 /root/images/ 下包含预先导出的容器镜像 tar 文件。这些文件会在运行 kubeadm 之前由 capv-load-local-images.sh 导入 containerd,因此节点引导不依赖从远程 Registry 拉取镜像。
  7. 这些 tar 文件应包含随操作系统模板提供的 kubeadm 控制平面镜像,包括 etcd 和 kube-apiserver;其镜像引用必须与根据 clusterConfiguration.imageRepository、Kubernetes 版本和组件镜像标签生成的引用完全一致。虽然 etcd 和 kube-apiserver 以静态 Pod 运行,但仍需要容器镜像。如果本地引用缺失或不匹配,运行时会尝试使用已配置的 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 镜像,既违背本地预加载的目的,也会导致在隔离环境中失败。

静态 IP 配置、主机名注入和其他初始化设置依赖 cloud-init。节点 IP 报告依赖来宾工具。

本地文件布局

工作负载集群命名

工作负载 cluster_name 不得为 global。该名称保留给 global 集群,重复使用会导致工作负载集群的资源与 cpaas-system 中 global 集群的资源发生冲突。global- 前缀保留给 global 集群 DR 工作流拥有的资源;请参阅通用前提条件。不要将 global- 用于工作负载集群资源,因为故障转移操作可能会将这些资源选作属于 global 集群的资源。

按照惯例,CAPI Cluster 和 provider 集群资源(VSphereCluster)应始终命名为 <cluster_name>,非根 CAPI 和 provider 资源(KubeadmControlPlane、KubeadmConfigTemplate、MachineDeployment、VSphereMachineTemplate、VSphereMachineConfigPool 等)应以 <cluster_name>- 为前缀——例如,示例清单使用了 <cluster_name>-kcp 和 <cluster_name>-md-0。这是建议,而非控制器强制执行的规则,但它可以避免多个工作负载集群位于 cpaas-system 时发生同一命名空间内的资源冲突,并使操作期间的资源归属一目了然。

创建本地工作目录,并按以下布局存储清单:

capv-cluster/
├── 00-namespace.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"}'
kubectl -n kube-public get configmap etcd-sync -o name 2>/dev/null || echo "not standby"

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

export CLUSTER_REGISTRY_ADDRESS="<registry_address>"
export IMAGE_REGISTRY_ADDRESS="<image_registry>"
# Repository names depend on the target version. The values below are the
# ACP v4.4 and later ones; for an earlier target, change them to:
#   Kube-OVN chart -> acp/chart-cpaas-kube-ovn
#   CoreDNS        -> tkestack/coredns
export KUBE_OVN_CHART_REPO="acp/chart-kube-ovn"
export COREDNS_REPO="acp/coredns"

curl -sk "https://${CLUSTER_REGISTRY_ADDRESS}/v2/${KUBE_OVN_CHART_REPO}/tags/list"
curl -sk "https://${IMAGE_REGISTRY_ADDRESS}/v2/${COREDNS_REPO}/tags/list"
curl -sk "https://${IMAGE_REGISTRY_ADDRESS}/v2/ait/cloud-provider-vsphere/tags/list"

NAME_UNKNOWN 响应表示仓库名称与目标版本不匹配;请根据上面的映射更正 KUBE_OVN_CHART_REPO 或 COREDNS_REPO 后重试,不要将其理解为缺少标签。

上面的仓库边界与 provider 在运行时应用的边界相同;有关选择规则,请参阅清单中由 provider 管理的字段;权威的 chart 名称表请参阅在控制平面之前升级 Kube-OVN。

/v2/ 片段仅属于 Registry HTTP API URL。在工作负载 Cluster 清单中,将 Registry 注解设置为 <registry_address>,不要包含 scheme 或路径。

确认以下结果:

  • global 集群可访问。
  • Alauda Container Platform Kubeadm Provider 和 Alauda Container Platform VMware vSphere Infrastructure Provider 正在运行。
  • 控制器参数包含 ClusterResourceSet=true。
  • public-registry-credential Secret 存在;此操作步骤不会打印其内容。
  • global 集群不是灾难恢复备用集群。kube-public/etcd-sync ConfigMap 不得存在。请参阅备用管理集群。
  • 永久平台 Registry 包含所需的 Kube-OVN、CoreDNS 和 vSphere CPI 标签。
  • VM 模板已通过可执行的本地镜像验证;其中包括完全匹配的 etcd、kube-apiserver 和 sandbox 镜像引用。它们通过静态 Pod 部署并不会取消镜像要求。

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

  • vCenter 服务器地址可访问。
  • vCenter 用户名和密码有效。
  • thumbprint 正确。
  • 模板名称正确。
  • 可在目标数据中心解析模板。
  • 如果 VM 克隆为 fullClone,模板系统磁盘不得大于后续清单中使用的 diskGiB 值。如果 CAPV 完成 linkedClone,系统磁盘大小将保持模板的大小,并忽略 diskGiB。
  • 模板中已安装 VMware Tools 或 open-vm-tools。
  • 外部 LoadBalancer 遵循规划控制平面端点中规定的第 4 层侦听器、后端、运行状况检查、可达性和所有权要求。
  • 如果选择了 type: internal,则应改为满足自建 VIP 前提条件:VIP 未被占用;dvPortGroup 安全策略或 NSX-T 分布式防火墙规则未阻止 VRRP 和免费 ARP;模板能够加载 ip_vs;并且 alive 插件可部署到 global 集群。

创建命名空间和 vCenter 凭证 Secret

创建用于存储工作负载集群对象的命名空间。

此工作流会将工作负载集群对象存储在 cpaas-system 命名空间中。在下面的清单和命令中,将每个 <namespace> 占位符替换为 cpaas-system。

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

应用命名空间:

kubectl apply -f 00-namespace.yaml

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

请使用编辑器编写凭证文件,而不是在 shell 提示符中输入值。shell 会将包含密码在内的整个命令记录到历史记录中;而使用编辑器写入的文件不会留下此类副本。将其保存在 capv-cluster/ 之外,以免被误认为是清单。

vsphere-credentials.env
username=<vsphere_username>
password=<vsphere_password>

每一行的格式为 key=value。不要为值加引号——kubectl 会将引号字符按字面值处理。值可以包含 =;每行中只有第一个 = 用于分隔键和值。

限制文件权限,从中创建 Secret,然后删除该文件:

chmod 600 ../vsphere-credentials.env

VSPHERE_SECRET_NAME="<credentials_secret_name>"
VSPHERE_NAMESPACE="<namespace>"

kubectl create secret generic "${VSPHERE_SECRET_NAME}" \
  --namespace "${VSPHERE_NAMESPACE}" \
  --from-env-file=../vsphere-credentials.env

rm -f ../vsphere-credentials.env
不要使用 kubectl apply 创建凭证 Secret

客户端 apply 会将其应用的清单(包括 vCenter 密码)存储在 Secret 自身的 kubectl.kubernetes.io/last-applied-configuration 注解中。kubectl create 不会写入此类注解。无论值是以 stringData 形式还是以 base64 data 形式写入,这一点都适用;base64 是编码,而不是保护措施。本页稍后创建 CPI 凭证时同样适用。

创建 Cluster 和 VSphereCluster 对象

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

下面的清单使用外部 LoadBalancer,这是本页的基线配置。对于自建 VIP,请将在 10-cluster.yaml 中启用自建 VIP中的 spec.controlPlaneLoadBalancer 块添加到 VSphereCluster 对象中。

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"
    # Optional. Bind this cluster to a dedicated image registry instead of the
    # registry of the `global` cluster. Read only at cluster creation.
    # cpaas.io/registry-reference: <registry_credential_name>
  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>"

应用清单:

kubectl apply -f 10-cluster.yaml

创建 vSphere CPI 交付资源

创建一个 ClusterResourceSet,使工作负载集群 API 服务器可访问后,工作负载集群能够自动接收 vSphere CPI 配置和清单。

INFO

在基线工作流中,VSphereCluster.spec.failureDomainSelector 有意不设置,CPI vsphere.conf 也不包含 [Labels] 块。只有启用故障域后才需要这两项;请按照多个数据中心和故障域中的说明一起配置。如果在 vsphere.conf 中添加 [Labels],却没有匹配的 VSphereFailureDomain 对象,CPI 将查找不存在的 zone 和 region 标签。

INFO

vSphere CPI 的 TLS 绕过选项是 insecure-flag。请将 insecure-flag = "1" 保留在 <cluster_name>-vsphere-cpi-config 的 [Global] 部分中,以便 vCenter 证书为自签名证书或不受工作负载集群节点信任时,CPI 仍可连接。对于未设置自身 insecure-flag 的 vCenter 条目,CPI 会应用全局值。

WARNING

CPI 的 ConfigMap、Secret 和 ClusterResourceSet 资源必须创建在与 Cluster 资源相同的命名空间中。本指南中的命名空间为 cpaas-system。ClusterResourceSet 只能匹配其自身命名空间中的集群;将其部署在其他命名空间中会静默阻止资源交付。

INFO

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

TIP

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

该清单声明两个 ConfigMap 和 ClusterResourceSet。它不会声明 ClusterResourceSet 同样引用的 CPI 凭证 Secret——该 Secret 携带 vCenter 密码,并在下一步单独创建。

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: 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

创建 CPI 凭证 Secret。它封装 vSphere CPI 在工作负载集群上读取的 vsphere-cloud-secret,因此其负载包含 vCenter 密码。请出于本页前文所述原因,使用编辑器编写负载,并将其保存在 capv-cluster/ 之外,以免被误认为是清单:

vsphere-cpi-secret-payload.txt
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>

限制文件权限,从中创建 Secret,然后删除该文件:

chmod 600 ../vsphere-cpi-secret-payload.txt

CLUSTER_NAME="<cluster_name>"

kubectl create secret generic "${CLUSTER_NAME}-vsphere-cpi-secret" \
  --namespace "${VSPHERE_NAMESPACE}" \
  --type=addons.cluster.x-k8s.io/resource-set \
  --from-file=data=../vsphere-cpi-secret-payload.txt

rm -f ../vsphere-cpi-secret-payload.txt

在所有其他位置使用相同的 <cluster_name>。创建 vCenter 凭证时已设置 VSPHERE_NAMESPACE;如果使用新的 shell,请再次设置。Secret 的唯一键必须是 data,该键由 --from-file=data= 生成。

然后应用清单:

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 服务证书 DNS SAN 以及 kubelet node-ip。hostname 必须是有效的 DNS-1123 子域名。

INFO

deviceName 是可选项。如果不需要强制指定来宾 NIC 名称,请从每个节点槽位中删除 deviceName 行。provider 会按照 NIC 顺序分配诸如 eth0、eth1 的 NIC 名称。

WARNING

dns 中的 VSphereMachineConfigPool 条目会保留在静态网络配置中,但在受影响的 VMware 部署中,它们可能无法可靠地更新来宾操作系统的 /etc/resolv.conf。因此,下面的控制平面和 worker 引导清单还会通过 kubeadm files 显式写入 /etc/resolv.conf。

槽位验证规则

provider 会验证以下内容:

  • spec.configs 至少声明一个槽位,并且每个槽位都声明带有非空 network.primary.networkName 的 network。
  • 每个磁盘都声明一个不小于 1 的 sizeGiB。
  • 持久磁盘的 unitNumber(如果设置)必须介于 0 和 15 之间,并且绝不能为 7;该值保留给 SCSI 控制器。
  • 在一个槽位内,persistentDisks 和 ephemeralDisks 中的磁盘名称和挂载路径必须唯一。
  • 在池内,以及命名空间中绑定到同一 Cluster 的其他每个池之间,主机名和主 IP 地址必须唯一。
已分配的槽位不可变更

一旦槽位的 status.configStatuses[].state 为 InUse 或 Released,就不能删除该槽位、修改其 network.primary.ip 或 .ipv6、从 persistentDisks[] 或 ephemeralDisks[] 中删除磁盘,也不能更改磁盘的 sizeGiB、mountPath 或已分配的 unitNumber。可以向任一列表添加磁盘,但只有在下次重新创建 VM 时才会生效。

在第一个 Machine 分配槽位之前规划主机名、地址和磁盘大小。有关扩容和运行时变更,请参阅在 VMware vSphere 上管理节点。

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
    ephemeralDisks:
    - name: var-lib-kubelet
      sizeGiB: <cp_var_lib_kubelet_size_gib>
      mountPath: /var/lib/kubelet
      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
  - 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
    ephemeralDisks:
    - name: var-lib-kubelet
      sizeGiB: <cp_var_lib_kubelet_size_gib>
      mountPath: /var/lib/kubelet
      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
  - 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
    ephemeralDisks:
    - name: var-lib-kubelet
      sizeGiB: <cp_var_lib_kubelet_size_gib>
      mountPath: /var/lib/kubelet
      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

创建 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
    ephemeralDisks:
    - name: var-lib-kubelet
      sizeGiB: <worker_var_lib_kubelet_size_gib>
      mountPath: /var/lib/kubelet
      fsFormat: ext4
    - name: var-lib-containerd
      sizeGiB: <worker_var_lib_containerd_size_gib>
      mountPath: /var/lib/containerd
      fsFormat: ext4

应用两个清单:

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

创建 Machines 前,验证池绑定和槽位状态:

kubectl -n <namespace> get vspheremachineconfigpool
kubectl -n <namespace> get vspheremachineconfigpool <cluster_name>-cp-pool \
  -o jsonpath='{range .status.conditions[*]}{.type}={.status} ({.reason}){"\n"}{end}'
kubectl -n <namespace> get vspheremachineconfigpool <cluster_name>-cp-pool -o yaml
kubectl -n <namespace> get vspheremachineconfigpool <cluster_name>-worker-pool -o yaml

列表输出包含 TOTAL、AVAILABLE 和 ALLOCATED 列,这些列取自 status.total、status.available 和 status.allocated。创建 Machines 前,应看到 ClusterRefReady=True、VCenterAvailable=True、MembersValid=True、MembersUnique=True 和 Ready=True。

SlotAvailable 有意不包含在 Ready 摘要中。槽位全部分配的池仍会报告带有 SlotAvailable=False 的 Ready=True;在 maxSurge: 0 rollout 期间,这种组合是正常的,并不矛盾。

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

不要将创建新的池名称作为 Machine 替换的常规重试方式。不同的 VSphereMachineConfigPool.metadata.name 会创建独立的槽位和 VMDK,因此反复拆除和重新部署可能会暂时占用多套完整磁盘。在容量受限的数据存储上进行另一次部署前,请等待早期池及其 VMDK 完成 provider 管理的删除。有关拆除检查,请参阅持久磁盘生命周期。实时信号为 status.persistentDiskStatuses[].phase,其值包括 Creating、Attached、Available、Reclaiming、Reclaimed 和 Error。status.configStatuses[].reclaimStatus 子树以及 spec.configs[].persistentDisks[].volumePath 和 .diskUUID 字段已弃用、冻结并计划移除。

应用可选的故障域对象

对于基线的单数据中心拓扑,请跳过此步骤。

如果启用了故障域,请完成多个数据中心和故障域,然后在创建控制平面前应用故障域对象:

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

验证集群引用的每个 VSphereFailureDomain 和 VSphereDeploymentZone 都存在。部署 zone 可用之前不要继续。将以下内容视为一个配置集:

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

未启用故障域时,不要将 failureDomainSelector 或 CPI 的 [Labels] 块添加到基线清单中。

创建控制平面对象

创建 VSphereMachineTemplate 和 KubeadmControlPlane 对象。使用检查清单文档中收集的值替换以下完整模板中的占位符。

Kubernetes 1.35 kubelet 设置

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

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

"imagePullCredentialsVerificationPolicy": "NeverVerify",

cloneMode 和 diskGiB 都保留在模板中,因为 CAPV 接受这两个字段。实际上,只有在实际克隆操作为 fullClone 时,diskGiB 才会影响系统磁盘。如果 cloneMode 为 linkedClone 且模板具有可用快照,CAPV 会完成链接克隆,系统磁盘大小将保持与源模板相同。如果不存在可用快照,CAPV 会回退到 fullClone,并再次应用 diskGiB。

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

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

数据磁盘分别在 VSphereMachineConfigPool.spec.configs[].persistentDisks[] 和 .ephemeralDisks[] 下声明。不要将数据磁盘大小加到 diskGiB 中;否则 VM 会获得一个更大的系统磁盘以及单独的数据磁盘,导致容量翻倍。

对于 fullClone,diskGiB 必须大于或等于 OS 镜像模板中的系统磁盘大小。对于 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

对于 KubeadmControlPlane,如果其基础设施模板设置了 machineConfigPoolRef,则 rolloutStrategy.rollingUpdate.maxSurge: 0 和值至少为 3 的 replicas 并非风格选择,而是必需项。请参阅Provider Requirements。

应用清单:

kubectl apply -f 20-control-plane.yaml

创建 worker 对象

创建 worker machine 模板、bootstrap 模板和 MachineDeployment。

基线 worker kubelet 补丁省略了仅适用于 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

对于基础设施模板设置了 machineConfigPoolRef 的 MachineDeployment,strategy.rollingUpdate.maxSurge: 0 和 maxUnavailable: 1 同样是必需的。Cluster API 默认的 maxUnavailable 为 0,这会与 maxSurge: 0 形成死锁。

应用清单:

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

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

  • 主 worker 清单默认不设置 failureDomain,因为基线工作流假设只有一个数据中心。如果需要将 worker MachineDeployment 放置在特定的 VSphereDeploymentZone 中,请按照Multiple datacenters and failure domains中的说明添加 failureDomain。
  • 某些环境会在 KubeadmConfigTemplate 中添加额外的运行时镜像替换命令或服务重启命令。基线示例中有意不包含这些命令。仅当环境的平台要求明确需要时才添加。

等待集群就绪

应用所有清单后,集群创建过程将异步进行。使用以下命令监控进度:

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

请等待 KubeadmControlPlane 报告预期数量的就绪副本,并且所有 Machine 对象均进入 Running 阶段,然后再继续验证。

验证

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

  1. 检查 global 集群中的 CPI 交付资源:
    kubectl -n <namespace> get clusterresourceset
    kubectl -n <namespace> get clusterresourcesetbinding
  2. 导出业务集群 kubeconfig:
    kubectl -n <namespace> get secret <cluster_name>-kubeconfig -o jsonpath='{.data.value}' | base64 -d > /tmp/<cluster_name>.kubeconfig
  3. 检查 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. 检查业务节点:
    kubectl --kubeconfig=/tmp/<cluster_name>.kubeconfig get nodes -o wide
  6. 检查池槽位计数器以及两个列表中每个磁盘的状态:
    kubectl -n <namespace> get vspheremachineconfigpool
    kubectl -n <namespace> get vspheremachineconfigpool <cluster_name>-cp-pool \
      -o jsonpath='{range .status.persistentDiskStatuses[*]}{.hostname}{" "}{.name}{" "}{.phase}{" unit="}{.unitNumber}{"\n"}{end}'
    kubectl -n <namespace> get vspheremachineconfigpool <cluster_name>-cp-pool \
      -o jsonpath='{range .status.ephemeralDiskStatuses[*]}{.hostname}{" "}{.name}{" unit="}{.unitNumber}{"\n"}{end}'

确认以下结果:

  • vsphere-cloud-controller-manager 出现在业务集群中。
  • 控制平面节点和 worker 节点均已创建。
  • 节点最终变为 Ready。
  • 每个池均报告 Ready=True,其 ALLOCATED 计数等于从该池中分配的运行中 Machine 数量。
  • 已分配槽位上的每个持久磁盘均报告 phase: Attached。
  • 已分配槽位上的每个临时磁盘均出现在 status.ephemeralDiskStatuses[] 中,并分配了单元号。临时磁盘没有阶段;它们会随 VM 一起创建。

故障排除

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

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=true、ClusterResourceSet 和 ClusterResourceSetBinding。
  • 如果存在 ClusterResourceSet,但未创建 ClusterResourceSetBinding,请检查 controller 是否对所引用的 ConfigMap 和 Secret 资源拥有所需的删除权限。
  • 如果未安装网络插件,请验证所需的集群注解是否存在,以及平台 controller 是否已处理这些注解。
  • 如果缺少或错误设置了 cpaas.io/registry-address 注解,请验证其是否设置为永久的 <registry_address>,且不包含 scheme 或 /v2/。然后验证为其提供支持的凭证:当集群继承 global 集群的 registry 时,使用 public-registry-credential;当集群使用专用 registry 时,使用 cpaas.io/registry-reference 指定的 Secret。
  • 如果 Machine 停留在 Provisioning,请检查 VSphereMachine 的 MachineConfigPoolReady 条件——该条件会显示槽位分配是否因池绑定或数据中心不匹配而失败。然后读取池自身的条件:MembersValid、MembersUnique、PersistentDisksReady、ClusterRefReady 和 VCenterAvailable。
  • 如果 Machine 始终无法完成 bootstrap,请检查 VSphereVM 和 VSphereMachine 上的 BootstrapReady 条件。原因 BootstrapSecretGetFailed 表示 bootstrap Secret 缺失或无法读取;BootstrapSecretContentInvalid 表示它存在但无法使用。在检查 vCenter 之前,请先检查该 Machine 的 KubeadmConfig 以及其 bootstrap data Secret。
  • 如果节点在 vCenter 中被关闭电源后一直处于关闭状态,这是预期行为。VSphereVM 在首次成功开机后会锁存 InitialPowerOnCompleted=True,且永远不会清除;从此以后,已关闭电源的 VM 会被视为有意执行的带外操作,不会再次自动开机。PoweredOn=False 会传播到 VSphereVM.Ready,然后传播到 VSphereMachine.Ready,因此报告未就绪是正确的。请在 vCenter 中为 VM 开机即可恢复;无需执行 rollout。rollout 创建的替代 VM 未设置该锁存值,因此其首次启动不受影响。
  • 如果 VSphereMachine 在没有明显原因的情况下清除 Ready,请注意:当后端 VSphereVM、其 providerID 或其网络信息不可用时,Ready 会被清除,并且系统现在会观测带外电源变更。在将其视为 controller 故障之前,请读取 VSphereVM 条件。
  • 如果完全没有任何 reconcile 且条件也没有变化,请检查管理集群是否为灾难恢复备用集群。请参阅 Standby Management Clusters。
  • 如果 VM 正在等待 IP 分配,请验证 VMware Tools、静态 IP 设置和 VSphereVM.status.addresses。
  • 如果业务 Node 对象一直没有 spec.providerID,请先验证 CPI 交付资源,然后检查 vCenter guest hostname 是否重复。当同一数据中心中的旧 VM 仍报告与新节点相同的 guest hostname 时,cloud-provider-vsphere 可能会回退到按节点名称查找,缓存旧 VM,并因 VM IP 与 kubelet 节点 IP 不匹配而拒绝新节点。请检查 leader vsphere-cloud-controller-manager 日志、节点的 SystemUUID、实际 VM UUID 以及 vCenter guest hostname/IP 值。修复或删除重复 hostname 或旧 VM 冲突后,重启业务集群的 vsphere-cloud-controller-manager Pods,以清除错误的内存缓存:
    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 进行比较。不同的池名称拥有相互独立的槽位和 VMDK,因此重复部署可能会累积多组磁盘。然后检查 status.configStatuses[] 中的 state 和 lastReleasedTime,以及 status.persistentDiskStatuses[] 中的 phase、lastError、retryAfter 和 taskRef,同时检查相关 Events 和 vCenter 附加状态。insufficient disk available 可确认 datastore 容量故障,但其本身不能证明回收失败。在复用或 provider 管理的回收仍处于待处理状态时,不要手动删除 VMDK 或移除池 finalizer。
  • 不要从 status.persistentDiskStatuses[] 中删除 phase: Reclaimed 条目。这是一个有意保留的墓碑记录:后端 VMDK 已经不存在,而该记录可阻止 controller 根据冻结的 spec volumePath 重新为磁盘植入初始数据。删除它会产生植入、删除、重新植入的循环,使池 finalizer 永远无法清除。
  • 如果模板系统磁盘大小与清单值不匹配,请先检查实际的克隆模式。当 VM 以 linkedClone 创建时,系统磁盘会保持模板的大小,并忽略 diskGiB。只有 fullClone 会使用 diskGiB,且在此情况下,diskGiB 不得小于模板磁盘大小。
  • 如果控制平面端点未启动,请验证 TCP 6443 透传、所有控制平面后端、HTTPS /healthz 结果、DNS 和证书 SAN,以及从 global 集群和控制平面节点进行访问的连通性。
  • 如果与 vCenter 的 TLS 连接失败,请验证 thumbprint、vCenter 地址,以及 proxy 设置是否干扰连接。

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

  • deploy/capi-controller-manager 在 global 集群的 cpaas-system namespace 中运行。
  • 不要使用业务集群 kubeconfig 检查 capi-controller-manager 日志。
  • 如果平台 controller 处理集群网络注解,还应检查平台 network-controller 日志和平台 cluster-lifecycle-controller 日志。

Admission 拒绝

apply 或 edit 可能在任何对象创建之前被拒绝。将拒绝消息中的片段与下表进行匹配。

拒绝消息包含原因和修复
maxSurge must be 0 when the infrastructure template references a machineConfigPoolRef在 KubeadmControlPlane 或 MachineDeployment 上将 maxSurge 设置为整数 0。不设置该字段意味着使用 Cluster API 默认值 1。
must have at least 3 replicas将 KubeadmControlPlane.spec.replicas 设置为 3 或更高值。固定 IP rollout 使用 maxSurge: 0,而 Cluster API 仅允许控制平面具有三个或更多副本时使用该值。
maxUnavailable must be at least 1 when the infrastructure template references a machineConfigPoolRef将 MachineDeployment.spec.strategy.rollingUpdate.maxUnavailable 设置为 1 或更高值。Cluster API 默认值 0 会与 maxSurge: 0 形成死锁。
unit number 7 is reserved for the SCSI controller在 0 和 15 之间选择其他 unitNumber。
also used by poolhostname 或主 IP 与此 namespace 中绑定到同一 Cluster 的另一个池中的槽位冲突。
allocated slot槽位为 InUse 或 Released。消息会指出被冻结的内容——主 IP 或 IPv6,或磁盘的 sizeGiB、mountPath 或 unitNumber——或者报告该槽位或其任一列表中的磁盘无法移除。请参阅 Allocated slots are immutable。
cannot delete pool while池仍拥有 InUse 个槽位。请先删除占用这些槽位的 Machine。
is already referenced by 或 is bound to两个 controller 引用了同一个池。为每个 KubeadmControlPlane 和 MachineDeployment 分配各自的池。
cannot change clusterRef while consumerRef is set该池已绑定到 controller。请创建新池,而不是重新指定此池的目标。
VSphereMachineTemplate spec.template.spec field is immutable在新名称下创建新模板。请参阅 Upgrading Clusters on VMware vSphere。
is immutable once controlPlaneLoadBalancer is setspec.controlPlaneLoadBalancer 只能写入一次。请参阅 controlPlaneLoadBalancer 字段。
cannot be set after the cluster is created集群创建时未设置 spec.controlPlaneLoadBalancer,而这已经表示使用外部 LoadBalancer。端点在创建时即已固定,之后无法再填充。

创建时拓扑变体

基线清单有意从一个数据中心和一个 NIC 开始。如果初始拓扑需要自建控制平面 VIP、额外 NIC、多个数据中心、故障域或额外数据磁盘,请在创建集群前使用以下变体。每次应用一个变体,并在组合变体之前验证完整的清单集。

使用自建 VIP

如果创建集群时使用 VSphereCluster.spec.controlPlaneLoadBalancer.type: internal 而不是外部 LoadBalancer,请使用此变体。先在控制平面端点模式中确定模式;集群创建后无法更改。

自建 VIP 的先决条件

选择 type: internal 之前,请完成以下所有事项。

  • 平台版本为 ACP v4.4 或更高版本。较早的平台版本不支持自建 VIP,与 provider 版本无关。
  • VIP 必须是 IPv4 地址、当前未被占用,并且与控制平面节点 IP 位于同一二层广播域。不接受 IPv6 和双栈端点。
  • VIP 不能是此集群 VSphereMachineConfigPool 对象中声明的槽位地址(network.primary.ip 或 network.additional[].ip),且必须位于所有外部 IPAM 池范围之外。provider 会检查槽位地址,但无法读取外部 IPAM 池,也无法检测此类冲突。
  • vrid 在该二层域中必须唯一。
  • 网络路径必须允许控制平面节点之间的 VRRP(IP 协议 112、组播 224.0.0.18)和免费 ARP。请确认 dvPortGroup 安全策略和任何 NSX-T 分布式防火墙规则均未阻止这些流量。
  • VM 模板必须提供 Linux IPVS 子系统(ip_vs 模块系列可加载),并允许 Alive 设置 net.ipv4.conf.all.arp_accept=1 和 net.ipv4.vs.conntrack=1。
  • alive 集群插件必须存在,并且可部署到 global 集群。

provider 本身不携带 Alive。它会在 global 集群上创建 ModuleInfo,平台再据此渲染工作负载 cpaas-system/alive AppRelease。请在 global 集群上确认插件:

kubectl get clustermodule global -o jsonpath='{.spec.version}{"\n"}'
kubectl get moduleplugin alive -o jsonpath='{.status.targetClusterVersions}{"\n"}'
kubectl get moduleplugin alive -o jsonpath='{.status.latestVersion}{"\n"}'
kubectl get moduleconfig alive-<alive_version> -o jsonpath='{.status.readyForDeploy}{"\n"}'

provider 按以下顺序解析 <alive_version>:如果 provider 部署上设置了 --plugin-alive-version 覆盖值,则优先使用;其次使用 ModulePlugin/alive 条目 status.targetClusterVersions[<ClusterModule spec.version>].version;最后才使用 status.latestVersion。先读取与第一条命令打印的版本匹配的 targetClusterVersions 条目;仅当映射中没有该版本对应的条目时,才适用 latestVersion。

工作负载集群自身的 ClusterModule 只有在集群创建后才会存在,因此规划时请读取 global 集群的版本作为规划依据。这是当前最接近的参考值,但并非保证:集群创建后,请使用 kubectl get clustermodule <cluster_name> 读取其自身版本,并针对该版本解析出的 alive 版本重新执行 readyForDeploy 检查。请在控制平面完成引导之前执行,因为之后无法更改 type: internal。如果要创建的正是 global 集群,请改为根据安装包确认组件版本和 alive 插件是否存在。

最后一条命令必须针对解析选出的版本打印 true。如果缺少 ModulePlugin/alive,或者缺少匹配的 ModuleConfig,或其报告为 false,则无法在此环境中安装自建 VIP。请改用外部 LoadBalancer 创建集群,并保持不设置 spec.controlPlaneLoadBalancer。请联系 Alauda 技术支持,获取与平台版本匹配的插件包。

controlPlaneLoadBalancer 字段

参数类型说明必填
.spec.controlPlaneLoadBalancer.typestring (internal / external)internal 会安装 Alive 并管理 VIP。external 会跳过 Alive,并要求预先配置四层 TCP LoadBalancer。值必须为小写;其他 provider 的拼写方式不同。否(默认为 external)
.spec.controlPlaneLoadBalancer.hoststring控制平面端点地址。对于 internal,此值为自建 VIP,且必须是 IPv4 地址。对于 external,此值为负载均衡器前端地址。是
.spec.controlPlaneLoadBalancer.portint (1–65535)控制平面端口。通常为 6443。是
.spec.controlPlaneLoadBalancer.vridintKeepalived virtual_router_id,范围为 1–255,且在控制平面二层域中必须唯一。当 type: internal 时
.spec.controlPlaneLoadBalancer.interfacestring(最多 15 个字符)承载 VIP 的主机接口。留空时,provider 会匹配承载节点主 IP 的接口。否,但请参阅下方警告
controlPlaneLoadBalancer 只能写入一次

provider 仅在创建集群时接受 spec.controlPlaneLoadBalancer。此后,type、host、port、vrid 和 interface 均会被冻结,无法清除此字段;未在创建时设置该字段的集群也无法再添加该字段。要修正错误,必须重建集群。

这会带来两个影响。对于多 NIC 节点,请在创建时显式设置 interface——之后无法修补错误的自动检测接口。此外,host 和 port 必须描述与 spec.controlPlaneEndpoint 相同的地址;不接受不匹配的值。

vrid 范围由 provider 的准入 webhook 强制执行,而非由 CRD schema 强制执行。客户端 dry run 不会联系服务器,因此既无法捕获此规则,也无法捕获 schema 规则;请使用 kubectl apply --dry-run=server 查看拒绝原因,而不创建对象。

在 10-cluster.yaml 中启用自建 VIP

对于 type: internal,请将以下代码块添加到创建 Cluster 和 VSphereCluster 对象中的 VSphereCluster 对象。基线清单中的其他内容保持不变。

spec:
  controlPlaneLoadBalancer:
    type: internal
    host: "<vip>"           # must equal spec.controlPlaneEndpoint.host
    port: <api_server_port> # must equal spec.controlPlaneEndpoint.port
    vrid: <vrid>
    interface: "<vip_interface>" # set explicitly on multi-NIC nodes

provider 还会为你协调工作负载集群的 kube-system/kube-proxy 配置,设置 ipvs.strictARP: true,并将 <vip>/32 添加到 ipvs.excludeCIDRs,然后滚动更新 kube-proxy DaemonSet。请勿手动还原这些值;provider 会重新应用这些值,并再次滚动更新 DaemonSet。

验证自建 VIP

kubectl -n <namespace> get vspherecluster <cluster_name> \
  -o jsonpath='{range .status.conditions[*]}{.type}={.status} ({.reason}){"\n"}{end}'
kubectl --kubeconfig=/tmp/<cluster_name>.kubeconfig -n kube-system get pods -l app=alive -o wide
kubectl --kubeconfig=/tmp/<cluster_name>.kubeconfig -n cpaas-system get apprelease alive
kubectl --kubeconfig=/tmp/<cluster_name>.kubeconfig get --raw='/version'

预期结果为 SelfBuiltLoadBalancerReady=True,每个控制平面节点上有一个 Alive pod,并且通过 VIP 发出的 API 请求成功,且无需禁用 TLS 验证。

Alive pod 运行在 kube-system 中,而非 cpaas-system 中;cpaas-system 包含 alive AppRelease,由平台根据 provider 的 ModuleInfo 渲染而来。SelfBuiltLoadBalancerReady 覆盖整个链路,包括 AppRelease 和 pod,因此当其为 False 时,请检查这两个 namespace。

INFO

SelfBuiltLoadBalancerReady 仅报告 VIP 链路状态。它不会阻止 VSphereCluster.status.ready,因此基础设施集群可能已报告就绪,而 VIP 仍在收敛中。条件原因包括 SelfBuiltLoadBalancerReady、SelfBuiltLoadBalancerReconciling、SelfBuiltLoadBalancerNotReady 和 InvalidSelfBuiltLoadBalancerConfiguration。

VIP 尚未就绪时,provider 也会推迟协调工作负载集群的 CoreDNS 和 kube-proxy 仓库,详情请参阅清单中由 provider 管理的字段。

添加第二个 NIC

如果节点需要额外的管理、存储或服务网络,请扩展以下资源中的清单:

  • 16-vspheremachineconfigpool-control-plane.yaml
  • 17-vspheremachineconfigpool-worker.yaml
  • 20-control-plane.yaml
  • 30-workers-md-0.yaml
  • 如果启用了故障域,还需修改 18-failure-domains.yaml

每个节点槽位都在 network.primary 和 network.additional 下声明其 NIC 布局。主 NIC 用于推导 kubelet node-ip,并继续作为节点的主要标识;其他 NIC 则按所列顺序依次合并。

在 machine config pool 中,为每个控制平面节点槽位添加第二个 NIC:

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:

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

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

如果启用了故障域,请更新 VSphereFailureDomain.spec.topology.networks 中的网络列表:

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

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

  • <master_01_nic2_ip>
  • <master_02_nic2_ip>
  • <master_03_nic2_ip>
  • <worker_01_nic2_ip>
  • 同时扩展 worker 池时使用 <worker_02_nic2_ip>

对于运行中的集群,请在不可变替换期间同时更改全部三个网络定义。请参阅运行时拓扑更改。

多个数据中心和故障域

如果需要在不同的 vCenter 数据中心或计算集群之间放置节点,请使用多个数据中心和故障域。

以下原则适用:

  • 一个集群可以定义多个 VSphereFailureDomain 对象。
  • 每个 VSphereDeploymentZone 都引用一个 VSphereFailureDomain。
  • 控制平面使用 VSphereCluster.spec.failureDomainSelector。
  • 如果 worker MachineDeployment 必须部署到特定部署区域,则使用 spec.template.spec.failureDomain。

为第一个数据中心准备以下占位符:

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

为第二个数据中心准备以下占位符:

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

如果添加第三个数据中心,请沿用相同的占位符模式:

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

在 18-failure-domains.yaml 中创建故障域对象。启用故障域时,第一个数据中心还需要 VSphereFailureDomain 和 VSphereDeploymentZone:

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.yaml 中的 VSphereCluster spec 中添加 failureDomainSelector,以启用跨可用故障域的控制平面选择:

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: true 的 VSphereDeploymentZone。使用匹配标签将控制平面限制在部分区域内。

还要将 [Labels] 块添加到 CPI ConfigMap 中,以便 vSphere CPI 在工作负载节点上发布匹配的 zone 和 region 标签。键必须与 VSphereFailureDomain.spec.zone.tagCategory 和 VSphereFailureDomain.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 标签,或者控制平面无法选择部署目标。

如果 worker MachineDeployment 必须固定到某个部署目标,请设置 worker 部署区域。在 30-workers-md-0.yaml 中的 spec.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 名称。

启用多个数据中心之前,请确认以下所有先决条件:

  1. 模板已同步到每个目标数据中心。
  2. 每个目标数据中心都能解析网络名称。
  3. 每个目标数据中心都能解析数据存储名称。
  4. vSphere CPI 数据中心列表包含所有目标数据中心。

添加数据磁盘

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

  • 控制平面节点(每个节点 4 个磁盘):var-cpaas 位于 persistentDisks[] 中,另有 var-lib-kubelet、var-lib-containerd 和 var-lib-etcd 位于 ephemeralDisks[] 中。请勿移除这些磁盘。
  • worker 节点(每个节点 3 个磁盘):var-cpaas 位于 persistentDisks[] 中,另有 var-lib-kubelet 和 var-lib-containerd 位于 ephemeralDisks[] 中。请勿移除这些磁盘。

VSphereMachineTemplate.spec.template.spec.diskGiB 是系统磁盘大小,而非 VM 的总磁盘容量。请将数据磁盘配置在 VSphereMachineConfigPool.spec.configs[].persistentDisks[] 和 .ephemeralDisks[] 下。除非你有意增大系统磁盘,否则不要将数据磁盘大小加到 diskGiB 中。

如果节点需要在必需磁盘之外增加数据磁盘,请将更多条目追加到相应 VSphereMachineConfigPool 节点槽位中的匹配列表:数据必须在 VM 替换后保留时使用 persistentDisks,可重建时使用 ephemeralDisks。以下可选字段在此处尤其相关:

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

若要附加原始磁盘而不进行格式化或挂载,请省略 mountPath 和 fsFormat:

persistentDisks:
# ...standard persistent disks...
- name: app-data
  sizeGiB: 50

磁盘可在客户机 OS 中通过 /dev/disk/by-capv/app-data 访问。滚动更新时,同一个 VMDK 会重新附加到新 VM,并重新创建符号链接。磁盘不会自动格式化或挂载;应用负责在运行时管理该磁盘。

选择持久磁盘或临时磁盘

一个槽位声明两个磁盘列表:spec.configs[].persistentDisks[] 和 spec.configs[].ephemeralDisks[]。

persistentDisks[]ephemeralDisks[]
Spec 字段name、sizeGiB、datastore、storagePolicy、unitNumber、mountPath、mountOptions、fsFormat、wipeFilesystemname、sizeGiB、datastore、storagePolicy、mountPath、mountOptions、fsFormat
VM 删除时分离并保留,然后在同一槽位重新附加到替换 VM随 VM 一起删除
VM 重建时相同的 VMDK 和相同的内容,除非设置了 wipeFilesystem: true始终为新的空磁盘,从头开始格式化
回收和槽位释放门控参与。在磁盘仍处于附加状态时阻止槽位释放和池回收不参与
观测状态status.persistentDiskStatuses[],包含路径、UUID、单元号、阶段和所属 machinestatus.ephemeralDiskStatuses[],仅包含主机名、名称和单元号
SCSI 单元号使用 unitNumber 固定,或让控制器分配并从状态中读取仅由控制器分配;没有对应的 Spec 字段
客户机寻址/dev/disk/by-capv/<name>。客户机脚本先通过 VMDK diskUUID 查找设备,然后通过 SCSI 单元号查找/dev/disk/by-capv/<name>。客户机脚本仅通过 SCSI 单元号查找设备

对于不应位于系统磁盘上的可重建节点本地数据,请使用 ephemeralDisks,包括标准的 /var/lib/kubelet、/var/lib/containerd 和 /var/lib/etcd 磁盘,以及任何临时空间。对于任何丢失即意味着数据丢失的内容,请使用 persistentDisks。

将标准磁盘保留在其声明的列表中

/var/cpaas 必须保留在 persistentDisks[] 中:它包含必须在 VM 替换后保留的平台状态。/var/lib/kubelet、/var/lib/containerd 和 /var/lib/etcd 属于 ephemeralDisks[];它们会在每个新 VM 上重建,将其声明为持久磁盘会把过时的 kubelet、运行时和 etcd 成员数据带到替换节点上。

此拆分是在创建时确定的。ephemeralDisks[] 在 provider v1.0.17 中首次引入,因此在更早版本的 provider 上创建的集群完全无法使用标准临时磁盘集:它将 /var/cpaas、/var/lib/containerd 和 /var/lib/etcd 声明为持久磁盘,并将 wipeFilesystem: true 放在 etcd 磁盘上。该集群会保留其布局:provider 升级不会迁移任何磁盘,并且已分配的槽位会拒绝移除任一列表中的磁盘,或调整任一列表中的磁盘结构。请参阅升级 Provider。

以下示例在标准 worker 磁盘集旁边添加一个临时暂存磁盘:

persistentDisks:
- name: var-cpaas
  sizeGiB: <worker_var_cpaas_size_gib>
  mountPath: /var/cpaas
  fsFormat: ext4
ephemeralDisks:
- name: var-lib-kubelet
  sizeGiB: <worker_var_lib_kubelet_size_gib>
  mountPath: /var/lib/kubelet
  fsFormat: ext4
- name: var-lib-containerd
  sizeGiB: <worker_var_lib_containerd_size_gib>
  mountPath: /var/lib/containerd
  fsFormat: ext4
- name: scratch
  sizeGiB: 50
  mountPath: /var/scratch
  fsFormat: ext4

在一个槽位内,两个列表中的磁盘名称和挂载路径必须唯一。

固定 SCSI 单元号

持久磁盘上的 unitNumber 可在 VM 重建过程中保持客户机设备顺序稳定。有效值范围为 0 到 15,但不包括为 SCSI 控制器保留的 7,且每个值在槽位内必须唯一。槽位分配后,所分配的值不可变更。

如果省略 unitNumber,控制器会在克隆时分配一个值,将其记录在 status.persistentDiskStatuses[].unitNumber 中,并在 VM 重建时重新使用该值。

临时磁盘完全没有 unitNumber 字段。控制器会在每次克隆时分配其单元号,并在 status.ephemeralDiskStatuses[].unitNumber 中报告,因此无法固定标准的 /var/lib/kubelet、/var/lib/containerd 和 /var/lib/etcd 磁盘。

迁移 pod 日志目录

当槽位中的任一磁盘挂载到 /var/lib/containerd 时,客户机磁盘协调脚本会将 /var/log/pods 指向该磁盘上的目录,使 pod 日志写入数据磁盘,而不是根文件系统。在标准布局中,该磁盘是临时磁盘,因此迁移后的 pod 日志会随其所属 VM 一起销毁;需要保留的内容请发送到平台日志存储中。

WARNING

此迁移仅通过 guestinfo 用户数据写入客户机,且仅在首次启动前执行。升级 provider 不会将其补应用到现有 machine;节点滚动更新并替换后才会应用此更改。请参阅不更改 Kubernetes 版本的 OS 镜像更新。要在运行中的节点上原地修复,必须使用带外脚本,这不属于此工作流。

验证创建时拓扑变体

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

kubectl -n <namespace> get cluster,vspherecluster,kubeadmcontrolplane,machinedeployment,machine,vspheremachine,vspherevm
kubectl --kubeconfig=/tmp/<cluster_name>.kubeconfig get nodes -o wide
kubectl -n <namespace> get vspheremachineconfigpool <pool_name> \
  -o jsonpath='{range .status.ephemeralDiskStatuses[*]}{.hostname}{" "}{.name}{" unit="}{.unitNumber}{"\n"}{end}'

确认以下结果:

  • 部署位置、NIC 或磁盘定义已反映到目标资源中。
  • 新节点进入 Ready 状态。
  • 更改后现有节点仍保持正常运行。
  • 每个声明的临时磁盘都已分配单元号,并可在客户机中的 /dev/disk/by-capv/<name> 处寻址。

后续步骤

有关业务节点扩容以及后续模板或运行时拓扑更改,请参阅 在 VMware vSphere 上管理节点。每次仅应用一项更改,并在同一集群中合并多项更改之前验证结果。