从零到一:在 Proxmox VE 上构建企业级 K8s 高可用集群
1. 架构设计与资源规划
在 PVE 环境下,为了模拟生产环境的健壮性,采用 3 Master + 3 Worker + 2 LB 的 8 节点架构。
节点规格表
| 角色 | 数量 | vCPU | RAM | Disk | 说明 |
|---|---|---|---|---|---|
| Load Balancer | 2 | 1核 | 1 GiB | 20G | 运行 Keepalived + HAProxy,集群入口 |
| Master 节点 | 3 | 2核 | 4 GiB | 40G | 形成 etcd 仲裁,保证控制平面高可用 |
| Worker 节点 | 3 | 4核 | 10 GiB | 100G | 运行业务 Pod,预留较大空间 |
| 合计 | 8 | 20核 | 44G | 400G | 建议保留 20% 宿主机资源冗余 |
核心工具链
- Infrastructure as Code (IaC): Terraform + Cloud-Init 快速分发虚拟机
- 负载均衡: Keepalived + HAProxy
- 容器运行时: Containerd
- K8s 部署工具: Sealos (最终使用方案)
2. 虚拟机模板
虽然 PVE 的克隆功能非常强大,即便提前准备好了软件包,克隆出的虚拟机仍然需要进行大量的基础配置和环境调整。Cloud-Init 解决身份认证,克隆功能解决复杂预配制,Terraform 解决自动化部署虚拟机。
在 PVE 中制作一个包含所有基础依赖的 Ubuntu 24.04 模板是高效的前提。
PVE 模板制作
-
下载官方 Ubuntu 24.04 Cloud Image。
Terminal window wget https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img -o noble-server-cloudimg-amd64.img -
使用 qm 创建虚拟机
注意:这里直接使用 SCSI 作为系统盘接口,避免后续转换成模板时出现 IDE 转 SCSI 的麻烦。这主要也是因为 Ubuntu 24.04 默认支持 virtio-scsi-pci。现代环境更常用 virtio/virtio-scsi,IDE 已较少使用。
STORAGE 是 PVE 中的存储池名称,通常是 local-lvm,我的这台机器安装时就组建了 zfs,所以此处名为 local-zfs。import subprocess# 配置参数VM_ID = "201"VM_NAME = "ubuntu-2404-scsi-template"IMG_PATH = "noble-server-cloudimg-amd64.img"STORAGE = "local-zfs"def run_cmd(cmd):print(f"执行: {cmd}")subprocess.run(cmd, shell=True, check=True)try:# 1. 创建 VM,预设 SCSI 控制器为性能最好的 virtio-scsi-pcirun_cmd(f"qm create {VM_ID} --name {VM_NAME} --memory 2048 --cores 2 --net0 virtio,bridge=vmbr0")run_cmd(f"qm set {VM_ID} --scsihw virtio-scsi-pci")# 2. 导入并挂载系统盘 (scsi0)run_cmd(f"qm importdisk {VM_ID} {IMG_PATH} {STORAGE}")run_cmd(f"qm set {VM_ID} --scsi0 {STORAGE}:vm-{VM_ID}-disk-0")# 3. 将 Cloud-init 挂载到 scsi1 (替代之前的 ide2)run_cmd(f"qm set {VM_ID} --scsi1 {STORAGE}:cloudinit")# 4. 设置启动顺序、串口和显示run_cmd(f"qm set {VM_ID} --boot c --bootdisk scsi0 --serial0 socket --vga serial0")print(f"\n✅ 纯 SCSI 模板虚拟机 {VM_ID} 已就绪。")print("注意:在 PVE UI 的 Cloud-Init 界面配置好后再转模板。")except Exception as e:print(f"❌ 脚本执行失败: {e}")
基础初始化
# 更换内网镜像源/大陆镜像源,更加推荐内网搭建nexus OSS,这不仅能做为镜像仓库,还能做为 apt 镜像源,极大提升部署效率。sudo sed -i 's/archive.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.listsudo sed -i 's/security.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.listsudo sed -i 's|http://|https://|g' /etc/apt/sources.listsudo apt-get update -y && sudo apt-get upgrade -y
# 必备工具apt install -y qemu-guest-agent socat btop tcpdump curl jq nfs-common必须注意:cloud-init 将在每次服务器启动的时候执行一次,所以如果你要手动修改系统配置,最好在修改完成后执行 cloud-init clean --logs 来清理 cloud-init 的状态,否则下次启动时它可能会重复执行之前的配置步骤,导致一些意外的行为。我采用的是直接修改配置文件的方式,关闭一些模块,避免重启后意外开启 ipv6 或者其他不必要的服务。
K8s 环境预设 (模板关键点)
在模板中预加载内核模块,避免部署时报 modprobe 错误:
# 开启 IP 转发与网桥过滤cat <<EOF | sudo tee /etc/modules-load.d/k8s.confoverlaybr_netfilterEOF
cat <<EOF | sudo tee /etc/sysctl.d/k8s.confnet.bridge.bridge-nf-call-iptables = 1net.bridge.bridge-nf-call-ip6tables = 1net.ipv4.ip_forward = 1EOFsudo sysctl --system模板特征清理
如果不清理,克隆出的机器会有相同的 machine-id,导致 DHCP 分配相同 IP。
sudo truncate -s 0 /etc/machine-idsudo rm /var/lib/dbus/machine-idsudo ln -s /etc/machine-id /var/lib/dbus/machine-idsudo cloud-init clean --logshistory -c && sudo shutdown -h nowTerraform 模板
部署过程中注意:
对于虚拟机的硬件配置修改,reboot/terraform apply 可能并不会生效,还是建议手动停止虚拟机,关机后再运行 terraform apply 来更新配置。
terraform { required_providers { proxmox = { source = "telmate/proxmox" version = "3.0.2-rc07" } }}
# Proxmox API 认证配置provider "proxmox" { pm_api_url = "https://192.168.4.145:8006/api2/json" pm_api_token_id = "root@pam!terraform" pm_api_token_secret = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeee" pm_tls_insecure = true}
# 虚拟机规格定义locals { vm_list = { "lb-1" = { vmid = 301, cpu = 1, mem = 1024, disk = "20G", ip = "192.168.4.110" } "lb-2" = { vmid = 302, cpu = 1, mem = 1024, disk = "20G", ip = "192.168.4.111" } "master-1" = { vmid = 311, cpu = 2, mem = 4096, disk = "40G", ip = "192.168.4.112" } "master-2" = { vmid = 312, cpu = 2, mem = 4096, disk = "40G", ip = "192.168.4.113" } "master-3" = { vmid = 313, cpu = 2, mem = 4096, disk = "40G", ip = "192.168.4.114" } "worker-1" = { vmid = 321, cpu = 4, mem = 10240, disk = "100G", ip = "192.168.4.115" } "worker-2" = { vmid = 322, cpu = 4, mem = 10240, disk = "100G", ip = "192.168.4.116" } "worker-3" = { vmid = 323, cpu = 4, mem = 10240, disk = "100G", ip = "192.168.4.117" } }}
# 使用 Cloud-Init 克隆模板并创建虚拟机resource "proxmox_vm_qemu" "k8s_node" { for_each = local.vm_list name = each.key vmid = each.value.vmid target_node = "pve-bigc" clone = "Ubuntu-template-modify" full_clone = true os_type = "cloud-init" agent = 1
cpu { cores = each.value.cpu type = "host" } network { id = 0 model = "virtio" bridge = "vmbr0" # 确保这是你 PVE 上的网桥名称 } serial { id = 0 type = "socket" } vga { type = "serial0" } memory = each.value.mem scsihw = "virtio-scsi-pci"
disks { scsi { scsi0 { disk { size = each.value.disk storage = "local-zfs" } } scsi1 { cloudinit { storage = "local-zfs" } } } }
# Cloud-init 配置:自动注入 IP 和 SSH Key ciuser = "iris" ipconfig0 = "ip=${each.value.ip}/24,gw=192.168.4.1" sshkeys = <<EOFssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG21JF/tUKdRTShlvIGb+aHiABgSpWpf0I1sk7mWyKmB ed25519 256-20260302EOF}3. 集群入口:Keepalived + HAProxy 高可用
在 Master 节点部署前,必须先建立 VIP (192.168.4.100),这将作为 K8s API Server 的统一入口。Keepalived 负责 VIP 的高可用,HAProxy 负责流量分发。
VIP
安装 Keepalived 和 HAProxy,详细步骤不再赘述,核心配置如下:
sudo apt update && sudo apt install -y keepalived haproxyvrrp_instance VI_1 { state MASTER interface eth0 # 确认这是正确的网卡名,用 ip addr 查一下 virtual_router_id 51 priority 100 # 优先级,MASTER 高于 BACKUP advert_int 1 authentication { auth_type PASS auth_pass 123456 } virtual_ipaddress { 192.168.4.100 # VIP }}vrrp_instance VI_1 { state BACKUP # 改为 BACKUP interface eth0 virtual_router_id 51 priority 90 # 优先级比 100 小即可 advert_int 1 authentication { auth_type PASS auth_pass 123456 } virtual_ipaddress { 192.168.4.100 }}错误路径:网卡名对不上
现象:Keepalived 启动失败,日志报错 interface enp0s18 doesn't exist。
解决:PVE 的网卡名在不同内核下可能从 eth0 变成 ens18。务必先 ip addr 确认,并在 keepalived.conf 中修正,altname enp0s18 并非总是能正确兼容,还是用 ip addr 查到的名字最可靠。
HAProxy 配置
frontend k8s-api bind *:6443 mode tcp option tcplog default_backend k8s-masters
backend k8s-masters mode tcp option tcplog option tcp-check balance roundrobin server master-1 192.168.4.112:6443 check server master-2 192.168.4.113:6443 check server master-3 192.168.4.114:6443 check预期配置
- Keepalived: 负责监控 LB 存活,确保 VIP 始终在线。
- HAProxy: 负责将 6443 端口的请求轮询分发给 3 台 Master,这个端口是 K8s API Server 的默认端口。
4. K8s 部署:从 KubeKey 切换到 Sealos
手动部署 K8s 既不方便,也不标准,后期若是出了问题,还不知道该怎么查。 所幸社区有很多自动化工具,这将提供极大的便利。
对于国内用户来说,部署工具的选择往往受限于镜像拉取问题。尝试了多个方案,最终选择了 Sealos。KK 没有被选择的原因主要是它在某些硬编码逻辑中仍然依赖于外部镜像路径,加上内网并没有合适的镜像仓库,导致部署失败,而 Sealos 则通过在控制服务器上通过 http 代理下载了所需镜像,再通过内网 SCP 分发二进制包,完全绕过了镜像拉取的瓶颈。
路径 A:KubeKey (KK) —— 因镜像问题折戟
遭遇挑战:
- Sandbox 镜像拉取失败:
registry.k8s.io/pause:3.8无法访问,这并非简单的网络问题,而是因为 KK 的某些组件在部署过程中硬编码了这个路径,导致整个部署流程卡死在拉取这个镜像的阶段,官方提供的解决方案是,使用 manifasest 功能制作离线的部署包。 - 镜像拉平失败:尝试通过
namespaceOverride将路径拉平至私有仓库,但 KK 在部分硬编码逻辑中仍寻找原路径,在折腾了许久之后,认定按照 KK 的设计思路,国内用户很难绕过这个问题,除非自己搭建一个完全兼容的镜像仓库,这点的复杂度和维护成本都过高。
路径 B:Sealos
核心优势:Sealos 将 K8s 视为一个“操作系统镜像”,通过内网 SCP 分发二进制包,极大减少了对外部镜像仓库的依赖。
export https_proxy=http://proxy.example.com:8080export http_proxy=http://proxy.example.com:8080export no_proxy=192.168.4.0/24,localhost,127.0.0.1sudo -E sealos run \ labring/kubernetes:v1.29.0 \ labring/helm:v3.14.0 \ labring/calico:v3.27.4 \ --masters 192.168.4.112,192.168.4.113,192.168.4.114 \ --nodes 192.168.4.115,192.168.4.116,192.168.4.117 \ --user iris \ --pk /home/iris/k8s-infra/k8s \ --env controlPlaneEndpoint=192.168.4.100:6443 \ --force可能遇到的问题:
Containerd 冲突 错误:Sealos 报错
containerd is installed... please uninstall it,但apt却说没装。 原因:系统$PATH中残留了手动下载的二进制文件。 解决:sudo rm -f /usr/bin/containerd*,物理抹除后再运行。Sudo 环境变量丢失 错误:设置了
https_proxy后sudo sealos run依然报错 500/404。 原因:普通sudo会重置环境变量,导致代理失效。 解决:使用sudo -E sealos run ...,通过-E强制保留当前用户的代理设置。
5. 收官之战:解决 TLS 证书“死锁”
当看到下面的 ASCI 图标时,恭喜,集群已经 Ready 了!但还差一点: VIP 服务还没有用得上呢,当前的 master 节点的 API Server 仍然绑定在它们各自的本地 IP 上,而不是 VIP 上。这就导致了后续访问 VIP 时的证书错误。
___ ___ ___ ___ ___ ___ /\ \ /\ \ /\ \ /\__\ /\ \ /\ \ /::\ \ /::\ \ /::\ \ /:/ / /::\ \ /::\ \ /:/\ \ \ /:/\:\ \ /:/\:\ \ /:/ / /:/\:\ \ /:/\ \ \ _\:\~\ \ \ /::\~\:\ \ /::\~\:\ \ /:/ / /:/ \:\ \ _\:\~\ \ \ /\ \:\ \ \__\ /:/\:\ \:\__\ /:/\:\ \:\__\ /:/__/ /:/__/ \:\__\ /\ \:\ \ \__\ \:\ \:\ \/__/ \:\~\:\ \/__/ \/__\:\/:/ / \:\ \ \:\ \ /:/ / \:\ \:\ \/__/ \:\ \:\__\ \:\ \:\__\ \::/ / \:\ \ \:\ /:/ / \:\ \:\__\ \:\/:/ / \:\ \/__/ /:/ / \:\ \ \:\/:/ / \:\/:/ / \::/ / \:\__\ /:/ / \:\__\ \::/ / \::/ / \/__/ \/__/ \/__/ \/__/ \/__/ \/__/
Website: https://sealos.io/ Address: github.com/labring/sealos Version: 5.1.1-1e312ad2c现象
在任意 master-node 上运行 kubectl get nodes 报错:certificate is valid for 192.168.4.112... not 192.168.4.100。
解决方案
- 修正解析:检查
/etc/hosts,确保apiserver.cluster.local指向 VIP 而非节点本地 IP。当你始终使用apiserver.cluster.local访问 API Server 时,证书中的 SANs 就不必重新签名了,因为它默认已经包含了这个域名。
grep apiserver /etc/hosts- 导出配置:
kubectl --insecure-skip-tls-verify \ -n kube-system get configmap kubeadm-config \ -o jsonpath='{.data.ClusterConfiguration}' > kubeadm-config.yaml- 注入 SANs:在
apiServer.certSANs下手动添加192.168.4.100。 - 重签证书:删除
/etc/kubernetes/pki/apiserver.crt,运行kubeadm init phase certs apiserver --config kubeadm-config.yaml。 - 重启服务:杀死
kube-apiserver进程让 Kubelet 自动拉起。
此时 VIP 上的 API Server 已经启用,访问 https://VIP:6443/healthz 应该能看到正常的响应了。
6. 总结
通过在 Proxmox VE 上构建企业级 K8s 集群,经历了从基础架构设计、虚拟机模板制作、负载均衡配置,到最终的 K8s 部署和证书调整的完整流程。 下面是这个项目的 GitHub 仓库地址,欢迎 star 和 fork:KagurazakaIris/k8s-infra