在 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 部署指南。
如何使用本页
- 完成基础设施和参数检查清单。
- 按照下方操作步骤准备基线清单文件。
- 应用清单前,仅添加所需的创建时拓扑变体。
- 应用完整的清单集并完成验证检查。
- 集群运行后,使用在 VMware vSphere 上管理节点进行扩容、不可变模板替换和运行时拓扑变更。
基线是验证参考。如果目标集群需要多个可选拓扑功能,请先逐一引入并验证清单变更,再将它们组合起来。
先决条件
开始之前,请确保满足以下条件:
- 已完成 VMware vSphere 基础设施准备。
global 集群能够连接到 vCenter。
- 目标模板、网络、数据存储和 vCenter 资源池均可用。
- 已选择控制平面端点模式。对于外部 LoadBalancer,其满足端点约定。对于 Self-built VIP,已满足 Self-built VIP 先决条件。集群创建后无法更改模式,因此请现在做出决定:请参阅控制平面端点模式。
- 所有必需的静态 IP 地址均已分配、未被使用,并且未由绑定到同一
Cluster 的其他 VSphereMachineConfigPool 中的插槽声明。
- 已启用
ClusterResourceSet=true。
- 已确定此集群使用的镜像仓库。默认情况下,集群会继承
global 集群的镜像仓库;如果该镜像仓库需要身份验证,则必须提供有效的 public-registry-credential Secret。若要改用专用镜像仓库拉取平台镜像,请在创建集群前按照为业务集群选择镜像仓库操作;创建后无法更改绑定。
- 所选镜像仓库可从业务节点访问,并且包含所需的 Kube-OVN、CoreDNS 和 vSphere CPI 标签。
- 平台能够处理安装网络插件所需的集群注解。
VSphereCluster.spec.identityRef 引用的 vCenter 账户满足 VMware vSphere Provider 安装中关于 Custom Fields 特权的要求。
global 集群是活动管理集群,而非灾难恢复备用集群。请参阅备用管理集群。
- 已规划任何专用节点池的节点标签、注解和污点。如在节点上配置注解、标签和污点所述,这些配置在节点池的资源上声明;只有在
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。
集群创建后,三种模式均无法更改。该字段只能写入一次;请参阅controlPlaneLoadBalancer 字段。
建议:对于外部 LoadBalancer,请省略该字段,而不是填写 type: external。字段缺失已经表示使用外部 LoadBalancer,填写该字段仅用于记录这一意图,却会将端点固定到同一地址的冻结副本。
对于 type: internal,先决条件、字段参考、清单块和验证命令请参阅使用 Self-built VIP。
Key Objects

顶层 Cluster 引用 VSphereCluster 以及控制平面或 worker 控制器。这些控制器引用不可变的 VSphereMachineTemplate 对象。每个模板都引用一个 VSphereMachineConfigPool,其槽位会为运行时的 Machine、VSphereMachine 和 VSphereVM 对象提供主机名、静态网络配置和持久磁盘。
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 模板至少应满足以下要求:
- 使用目标平台环境所需的操作系统。
- 包含
cloud-init。
- 包含 VMware Tools 或
open-vm-tools。
- 包含
containerd。
- 包含 kubeadm 引导所需的基线组件。
- 在
/root/images/ 下包含预先导出的容器镜像 tar 文件。这些文件会在运行 kubeadm 之前由 capv-load-local-images.sh 导入 containerd,因此节点引导不依赖从远程 Registry 拉取镜像。
- 这些 tar 文件应包含随操作系统模板提供的 kubeadm 控制平面镜像,包括 etcd 和 kube-apiserver;其镜像引用必须与根据
clusterConfiguration.imageRepository、Kubernetes 版本和组件镜像标签生成的引用完全一致。虽然 etcd 和 kube-apiserver 以静态 Pod 运行,但仍需要容器镜像。如果本地引用缺失或不匹配,运行时会尝试使用已配置的 Registry。
/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 阶段,然后再继续验证。
验证
使用以下命令验证集群创建工作流。
- 检查
global 集群中的 CPI 交付资源:
kubectl -n <namespace> get clusterresourceset
kubectl -n <namespace> get clusterresourcesetbinding
- 导出业务集群 kubeconfig:
kubectl -n <namespace> get secret <cluster_name>-kubeconfig -o jsonpath='{.data.value}' | base64 -d > /tmp/<cluster_name>.kubeconfig
- 检查 vSphere CPI daemonset 是否已在业务集群中创建:
kubectl --kubeconfig=/tmp/<cluster_name>.kubeconfig -n kube-system get daemonset
- 检查
global 集群对象:
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
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 可能在任何对象创建之前被拒绝。将拒绝消息中的片段与下表进行匹配。
创建时拓扑变体
基线清单有意从一个数据中心和一个 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 字段
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 名称。
启用多个数据中心之前,请确认以下所有先决条件:
- 模板已同步到每个目标数据中心。
- 每个目标数据中心都能解析网络名称。
- 每个目标数据中心都能解析数据存储名称。
- 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[]。
对于不应位于系统磁盘上的可重建节点本地数据,请使用 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 上管理节点。每次仅应用一项更改,并在同一集群中合并多项更改之前验证结果。