PVE is now the 2-node cluster homelab (pv1-1 + pv1-2), no shared storage: - template 101 copied to pv1-2 as 9101; MDs lab1-workers-pv1-1 (max 6) and lab1-workers-pv1-2 (max 12), PMTs pinned with allowedNodes; pool .31-.50; lab1-workers renamed for symmetry (decision #18, #7 superseded) - CA: balancing-label node.cluster.x-k8s.io/pool (+ pool label, capacity labels with os/arch, maxPods on the MDs), scan-interval 1m, v 2, default QPS (#19). Scale test: 16 pods -> 4 + 4 across both nodes. - runbook, image-builder copy procedure, fixes tables, future task to report three CA clusterapi bugs upstream, lab facts, changelog, execution log Plan: docs/plans/2026-09-29-2315-pv1-2-workers.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6.8 KiB
Node VM template with image-builder (Proxmox)
Result: Proxmox template ubuntu-2404-kube-v1.35.9 on node pv1-1 — Ubuntu 24.04.4, containerd,
kubeadm/kubelet/kubectl v1.35.9, pre-pulled control-plane images, cloud-init, qemu-guest-agent.
Its VMID (101) is TEMPLATE_VMID for CAPMOX. A byte-identical copy, 9101, sits on pv1-2
(Template on other PVE nodes).
Build host prerequisites (Ubuntu 24.04)
sudo apt install -y make git curl unzip ca-certificates openssl openssh-client \
python3 python3-venv python3-pip
python3 -m venv ~/ib-venv && source ~/ib-venv/bin/activate
pip install ansible-core==2.18.18 # avoids PEP 668 / --break-system-packages
git clone https://github.com/kubernetes-sigs/image-builder.git ~/image-builder
cd ~/image-builder/images/capi && git checkout v0.1.55
make deps-proxmox # python/ansible/packer 1.9.5 checks + packer init
Build
Overrides live in a var file (configs/image-builder/proxmox-lab.json.example),
passed via PACKER_VAR_FILES (last on the packer command line → highest precedence).
export PROXMOX_URL="https://192.168.0.8:8006/api2/json"
export PROXMOX_USERNAME='capmox@pve!capi'
export PROXMOX_TOKEN="<secret>"
export PROXMOX_NODE="pv1-1" # PVE node *name* (pvesh get /nodes), not IP; template lands here
export PROXMOX_ISO_POOL="local" # storage with content 'iso' (pvesm status --content iso)
export PROXMOX_STORAGE_POOL="local-lvm" # storage with content 'images' (pvesm status --content images)
export PROXMOX_BRIDGE="vmbr0"
export PROXMOX_VLAN="4"
export PACKER_VAR_FILES=~/proxmox-lab.json
unset PACKER_FLAGS ISO_FILE
export SSH_PASSWORD=labpass # optional: ssh builder@<vm> while provisioning runs
ON_ERROR_ASK=1 PACKER_LOG=1 PACKER_LOG_PATH=packer.log make build-proxmox-ubuntu-2404
The build VM (VLAN 4) needs DHCP and must reach the Packer HTTP server on the build host
(ports 8000–9000) to fetch the autoinstall user-data.
What the build does
set-ssh-passwordrenders the autoinstalluser-datawith a hashed password for userbuilder.- Packer creates VM
packer-<uuid>(2 vCPU, 2 GB, 20 GB), attaches the ISO, serveshttp/24.04/. - After
boot_waitit types the GRUB boot command over VNC:autoinstall ds=nocloud-net;s=http://<host>:<port>/24.04/. - Subiquity installs Ubuntu and reboots; Packer waits for SSH (up to 2h), IP via guest agent.
- Provisioners: wait for cloud-init → ansible
firstboot.yml→ reboot → ansiblenode.yml(containerd, k8s binaries, images, sysctl, cloud-init config) → goss tests → lockbuilder, shutdown. - VM converted to template
ubuntu-2404-kube-v1.35.9.
Observe it in the PVE noVNC console; DEBUG=1 steps through each Packer stage; make -n prints the
exact packer build command. Typical duration 20–40 min.
Fixes log
| # | Symptom | Root cause | Fix |
|---|---|---|---|
| 1 | POST .../storage/local/upload: write: broken pipe |
pveproxy dropped the 3.17 GiB upload mid-stream — missing Datastore.AllocateTemplate (token had only PVEVMAdmin) or no spool space in /var/tmp (exact cause not pinned down) |
PVEDatastoreAdmin on /storage, check df -h /var/tmp; then avoid re-uploads with iso_file |
| 2 | net0.bridge: invalid bridge ID 'vmbr0.4@vmbr0' |
PROXMOX_BRIDGE set to the ip link name of a VLAN sub-interface; PVE wants a bridge name + separate tag |
PROXMOX_BRIDGE=vmbr0, PROXMOX_VLAN=4 (→ bridge=vmbr0,tag=4) |
| 3 | 403 Permission check failed (/sdn/zones/localnetwork/vmbr0.4, SDN.Use) |
since PVE 8 every Linux bridge is an object in SDN zone localnetwork; PVEVMAdmin lacks SDN.Use |
PVESDNUser on /sdn/zones/localnetwork |
| 4 | unsupported format 'qcow2' at .../LvmThinPlugin.pm line 102 |
image-builder defaults disk_format to qcow2; LVM-thin is raw-only. --var disk_format=raw in PACKER_FLAGS was lost when PACKER_FLAGS was re-exported |
"disk_format": "raw" in the PACKER_VAR_FILES JSON |
| 5 | one of iso_file, iso_url, or ... cd_content must be specified for boot_iso |
packer-plugin-proxmox v1.2.4 requires exactly one ISO source; ubuntu-2404.json sets iso_file from $ISO_FILE and a hardcoded iso_url → count 2. Misleading error; README tip is broken (worth an upstream issue) |
set iso_file and "iso_url": "" in the var file, unset ISO_FILE |
Notes carried into CAPMOX (phase 1): same ACL set, vlan: 4 on the network device, no
format: qcow2 in ProxmoxMachineTemplates.
Smoke test of the template
The template has no usable login (builder is locked) — access only via cloud-init (NoCloud,
which the PVE Cloud-Init drive provides):
Run it on the node that holds the template. VMIDs are cluster-wide, so pick a free ID: 9001 is
now debian-13-template, and the last line destroys $ID. The key file must exist on that node
(scp ~/.ssh/id_ed25519.pub root@<node-ip>:/root/jan.pub).
TPL=<template vmid>; ID=9901
qm status $ID 2>/dev/null && echo "VM $ID exists, pick another ID" # must print nothing
qm clone $TPL $ID --name smoke-k8s --full 1 --storage local-lvm
qm set $ID --ide2 local-lvm:cloudinit
qm set $ID --ciuser jan --sshkeys /root/jan.pub --ipconfig0 ip=dhcp
qm set $ID --net0 virtio,bridge=vmbr0,tag=4
qm start $ID && qm guest cmd $ID network-get-interfaces
# inside: cloud-init status --long; kubeadm version -o short; crictl images
qm stop $ID && qm destroy $ID --purge
kubelet crash-looping before kubeadm init/join is expected.
Template on other PVE nodes
Without shared storage, a template on local-lvm can only be cloned on its own node. That holds
for PVE and for CAPMOX: can't clone VM to node '<target>' (VM uses local storage). Each additional
PVE node therefore gets its own copy under its own VMID, since VMIDs are cluster-wide.
Copy rather than rebuild: the copy is byte-identical, whereas a second Packer run would pick up
newer apt packages. The source must have no disk or CD-ROM on non-shared storage other than
local-lvm; check with qm config 101.
# on the node holding the template (pv1-1); 9101 = copy for pv1-2
qm clone 101 9101 --full 1 --name ubuntu-2404-kube-v1.35.9 --storage local-lvm # clone stays on pv1-1
qm migrate 9101 pv1-2 --with-local-disks # offline: copies the LVs
ssh root@192.168.0.10 qm template 9101
Then run the smoke test on the target node with TPL=9101. It also proves that VLAN 4 works on
that node's vmbr0. The copy is referenced by templateID: 9101 + sourceNode: pv1-2 in that
node's ProxmoxMachineTemplate
(phase 1, Step 0).
A new template build (a new Kubernetes version) means building on pv1-1 and then repeating the
copy for every other node, with a new VMID pair per version.