Iris
返回文章列表
2814 字15 分钟

从零到一:在 Proxmox VE 上构建企业级 K8s 高可用集群

文章#K8s / PVE

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 模板制作

  1. 下载官方 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
  2. 使用 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-pci
    run_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}")

基础初始化

Terminal window
# 更换内网镜像源/大陆镜像源,更加推荐内网搭建nexus OSS,这不仅能做为镜像仓库,还能做为 apt 镜像源,极大提升部署效率。
sudo sed -i 's/archive.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list
sudo sed -i 's/security.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list
sudo sed -i 's|http://|https://|g' /etc/apt/sources.list
sudo 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 错误:

Terminal window
# 开启 IP 转发与网桥过滤
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
sudo sysctl --system

模板特征清理

如果不清理,克隆出的机器会有相同的 machine-id,导致 DHCP 分配相同 IP。

Terminal window
sudo truncate -s 0 /etc/machine-id
sudo rm /var/lib/dbus/machine-id
sudo ln -s /etc/machine-id /var/lib/dbus/machine-id
sudo cloud-init clean --logs
history -c && sudo shutdown -h now

Terraform 模板

部署过程中注意: 对于虚拟机的硬件配置修改,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 = <<EOF
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG21JF/tUKdRTShlvIGb+aHiABgSpWpf0I1sk7mWyKmB ed25519 256-20260302
EOF
}

3. 集群入口:Keepalived + HAProxy 高可用

在 Master 节点部署前,必须先建立 VIP (192.168.4.100),这将作为 K8s API Server 的统一入口。Keepalived 负责 VIP 的高可用,HAProxy 负责流量分发。

VIP

安装 Keepalived 和 HAProxy,详细步骤不再赘述,核心配置如下:

Terminal window
sudo apt update && sudo apt install -y keepalived haproxy
/etc/keepalived/keepalived.conf
vrrp_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
}
}
/etc/keepalived/keepalived.conf
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 配置

/etc/haproxy/haproxy.cfg
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) —— 因镜像问题折戟

遭遇挑战

  1. Sandbox 镜像拉取失败registry.k8s.io/pause:3.8 无法访问,这并非简单的网络问题,而是因为 KK 的某些组件在部署过程中硬编码了这个路径,导致整个部署流程卡死在拉取这个镜像的阶段,官方提供的解决方案是,使用 manifasest 功能制作离线的部署包。
  2. 镜像拉平失败:尝试通过 namespaceOverride 将路径拉平至私有仓库,但 KK 在部分硬编码逻辑中仍寻找原路径,在折腾了许久之后,认定按照 KK 的设计思路,国内用户很难绕过这个问题,除非自己搭建一个完全兼容的镜像仓库,这点的复杂度和维护成本都过高。

路径 B:Sealos

核心优势:Sealos 将 K8s 视为一个“操作系统镜像”,通过内网 SCP 分发二进制包,极大减少了对外部镜像仓库的依赖。

Terminal window
export https_proxy=http://proxy.example.com:8080
export http_proxy=http://proxy.example.com:8080
export no_proxy=192.168.4.0/24,localhost,127.0.0.1
sudo -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

可能遇到的问题:

  1. Containerd 冲突 错误:Sealos 报错 containerd is installed... please uninstall it,但 apt 却说没装。 原因:系统 $PATH 中残留了手动下载的二进制文件。 解决sudo rm -f /usr/bin/containerd*,物理抹除后再运行。

  2. Sudo 环境变量丢失 错误:设置了 https_proxysudo 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

解决方案

  1. 修正解析:检查 /etc/hosts,确保 apiserver.cluster.local 指向 VIP 而非节点本地 IP。当你始终使用 apiserver.cluster.local 访问 API Server 时,证书中的 SANs 就不必重新签名了,因为它默认已经包含了这个域名。
Terminal window
grep apiserver /etc/hosts
  1. 导出配置
Terminal window
kubectl --insecure-skip-tls-verify \
-n kube-system get configmap kubeadm-config \
-o jsonpath='{.data.ClusterConfiguration}' > kubeadm-config.yaml
  1. 注入 SANs:在 apiServer.certSANs 下手动添加 192.168.4.100
  2. 重签证书:删除 /etc/kubernetes/pki/apiserver.crt,运行 kubeadm init phase certs apiserver --config kubeadm-config.yaml
  3. 重启服务:杀死 kube-apiserver 进程让 Kubelet 自动拉起。

此时 VIP 上的 API Server 已经启用,访问 https://VIP:6443/healthz 应该能看到正常的响应了。


6. 总结

通过在 Proxmox VE 上构建企业级 K8s 集群,经历了从基础架构设计、虚拟机模板制作、负载均衡配置,到最终的 K8s 部署和证书调整的完整流程。 下面是这个项目的 GitHub 仓库地址,欢迎 star 和 fork:KagurazakaIris/k8s-infra