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>
12 KiB
Execution log: lab1 workers on both PVE nodes (template copy per node)
Pairs with docs/plans/2026-09-29-2315-pv1-2-workers.md.
Status
- Step 0 — Record the plan
- Step 1 —
vmbr0onpv1-2VLAN-aware (owner) - Step 2 — Template 9101 on
pv1-2(owner) - Step 3 — Second worker MachineDeployment
lab1-workers-pv1-2 - Step 4 — Scale test across both nodes
- Step 5 — Docs and history
Step 0 — Record the plan (2026-09-29 23:15 CEST)
Plan and this log committed on their own, together with the session note that motivated them.
Starting state (read-only PVE API):
- cluster
homelab:pv1-1192.168.0.8,pv1-2192.168.0.10 vmbr0onpv1-2has nobridge_vlan_aware- template 101 is only on
pv1-1, with disksscsi0+efidisk0, both onlocal-lvm nextidis 103
Steps 1–2 — bridge and template on pv1-2 (owner, verified 2026-09-30 08:38 CEST)
The owner made vmbr0 VLAN-aware on pv1-2 (/etc/network/interfaces, then ifreload -a):
bridge-vlan-aware yes
bridge-vids 2-10
This deviates from the plan: bridge-vids is 2-10, not 2-4094. That's enough for VLAN 4, and
any VLAN above 10 on pv1-2 would need the range widened first.
Template copy, run on pv1-1. A cross-node clone is refused from local-lvm, so the copy is a
clone on the same node followed by an offline migration:
qm clone 101 9101 --full 1 --name ubuntu-2404-kube-v1.35.9 --storage local-lvm
qm migrate 9101 pv1-2 --with-local-disks
ssh root@192.168.0.10 qm template 9101
The PVE API shows 9101 on pv1-2 with template: 1, and scsi0/efidisk0 as
local-lvm:base-9101-disk-*.
Smoke test (image-builder-proxmox.md) on
pv1-2, with TPL=9101 ID=9901: the VM got a VLAN 4 DHCP address through the guest agent, and was
destroyed afterwards. Before running it:
/root/jan.pubhad to be copied topv1-2first:scp ~/.ssh/id_ed25519.pub root@192.168.0.10:/root/jan.pub.- The doc's
ID=9001now collides withdebian-13-template, and the snippet ends withqm destroy $ID. The snippet now uses 9901 and checks that the ID is free (a9f5879).
Worth noting:
- VMIDs are cluster-wide (pmxcfs), which is why the copy needs its own ID. Names don't have to be unique.
- The copied
efidisk0reportssize=528K(the source says 4M). This doesn't matter: the template boots SeaBIOS.
Step 3 — second worker MachineDeployment (prepared 2026-09-30, apply pending)
Changes in configs/capi/lab1.yaml:
- new
MachineDeployment/lab1-workers-pv1-2andProxmoxMachineTemplate/lab1-worker-2g-pv1-2(sourceNode: pv1-2,templateID: 9101,allowedNodes: [pv1-2]) ProxmoxCluster.allowedNodesstays[pv1-1]for the control plane- header hand edits #10 and #11
Deviation from the plan: the IP pool is enlarged and the max sizes are raised (owner's request, "20 nodes"):
ipv4Config.addresses192.168.4.31-192.168.4.39→192.168.4.31-192.168.4.50lab1-workersmax 5 → 6,lab1-workers-pv1-2max 12- 6 + 12 + 1 CP = 19 of 20 addresses. The spare covers the KCP rollout surge, and CAPMOX pools
hold a freed address for 60 s (
AddressReuseGracePeriodSeconds,pkg/kubernetes/ipam/ipam.go:318). - An ARP probe from
lab-mgmtfound.32–.60silent;.30(VIP) and.31(CP) share the CP VM's MAC. - The owner confirmed
.31–.50is outside the VLAN 4 DHCP range. - Changing the pool is safe: the ProxmoxCluster webhook only validates the addresses, and the
controller copies them into the InClusterIPPool on every reconcile (
ipam.go:323-324).
Sizing:
pv1-1: 6 × 2 GiB within ~17.7 GiB of reservable headroom (160 %).pv1-2: 12 × 2 GiB of 65.4 GiB reservable (40.9 GiB physical); 48 vCPU on 12 cores, which is fine for the pause-containerinflatetests but CPU-bound under real load.
yq parse check: 9 documents; both worker PMTs point at the right node, template and allowedNodes.
Applied by the owner (kubectl diff / kubectl apply -f configs/capi/lab1.yaml). The CA
discovered lab1-workers (max 6) and lab1-workers-pv1-2 (max 12).
Step 4 — scale test, first run (2026-09-30 ~06:45 UTC)
The inflate Deployment had 16 pods (1500m / 512Mi each, 2 per worker).
- Result: 8 workers Ready by 06:47Z.
lab1-workers0→6: VMIDs 103–108 onpv1-1, IPs .34–.39.lab1-workers-pv1-20→2: VMIDs 109–110 onpv1-2, IPs .32–.33.- No
VMProvisionFailed.status.proxmoxNodeand providerIDs are consistent. The pool had 9 of 20 addresses in use.
- Checked with: the
cluster-inspectoragent (read-only). Root causes came from the source (upstream-researcher, CA tagcluster-autoscaler-1.35.2, CAPI v1.12.11, CAPMOX v0.9.1).
Two problems.
1. Not balanced. The log shows Best option to resize: lab1-workers → No similar node groups found → 0->6 (max: 6). Only in the next loop did lab1-workers-pv1-2 get 0->2.
- The inspector guessed the
cluster.x-k8s.io/deployment-namelabel. The source rules that out: CA never reads it. - What the source does show in CA 1.35.2
cloudprovider/clusterapi:Labels()readsspec.template.spec.metadata.labels, which doesn't exist in v1beta2 (clusterapi_unstructured.go:207). MD template labels therefore never reach the from-zero template node.- CAPMOX
ProxmoxMachineTemplatehas nostatus, sokubernetes.io/osandarchbecome""on the template (clusterapi_nodegroup.go:393-405). A pod withnodeSelector: kubernetes.io/os: linuxwould not trigger a scale-up from zero. - The capacity key is
maxPods; ourmaximum-podswas ignored, and 110 only applied because it is the default. MixedTemplateNodeInfoProvidercaches a template built from a real node for--node-info-cache-expire-time(default 87600h). A group that has had nodes keeps that template: ~1.78Gi allocatable, os/arch set,node-rolelabel. A from-zero group gets the synthetic template: 2Gi, empty os/arch. The generic comparator rejects that pair.- That this cache caused this particular decision is inferred. The reason for a mismatch is only logged at V(5).
- Fix (
lab1.yamlhand edit #12):- both MDs carry the template label
node.cluster.x-k8s.io/pool: workers, which CAPI syncs to the Node; - the annotation
capacity.cluster-autoscaler.kubernetes.io/labels: node.cluster.x-k8s.io/pool=workers,kubernetes.io/os=linux,kubernetes.io/arch=amd64; maximum-pods→maxPods;- CA
balancing-label: node.cluster.x-k8s.io/pool, which compares only that label and turns off the resource checks.
- both MDs carry the template label
2. Loop 8–26 s, throttled.
- The log is full of
Waited before sending request … client-side throttlingon/api,/apisand/apis/infrastructure.cluster.x-k8s.io/v1alpha2against the lab-mgmt API. - Cause:
- For every MD at 0 replicas,
CanScaleFromZero→readInfrastructureReferenceResource→getKindPreferredVersionruns through the provider's uncached discovery client. The cached one is built atclusterapi_provider.go:195but not passed to the controller. - That is 3 GETs per MD per
NodeGroups()call, andNodeGroups()runs about 15× per loop. - The client uses QPS 5 / burst 10 (
--kube-client-qps/--kube-client-burstdefaults).
- For every MD at 0 replicas,
- There is no upstream fix up to 1.36.1 or master (2026-09-29).
- The owner rejected raising QPS (50/100) and wants fewer requests instead. Fix in
configs/cluster-autoscaler/values.yaml:scan-interval: 1m(was 10s).loop/trigger.go:74-100: unschedulable pods and finished scale-ups/downs still start a loop immediately. Only idle loops are spaced out, so scale-down takes up to about +1 min.v: 2(chart default 4). The throttling and "discovered node group" lines are V(3)/V(4).
- Considered and not taken:
min-size: 1: no MD at 0 means no discovery, but two idle VMs.CAPI_VERSION=v1beta1: v1beta1 refs carryapiVersion, so no discovery. But v1beta1 is deprecated and the path is untested with CAPMOX.- An upstream patch to pass the cached discovery client (a future task).
Rename of the pv1-1 group (owner's request, 2026-09-30)
This deviates from the plan: the plan kept lab1-workers. The owner wants symmetric per-node
names now that node names show the MD name (lab1-workers-zldvn-* next to
lab1-workers-pv1-2-nlv6s-*).
Changes in configs/capi/lab1.yaml (hand edit #13):
MachineDeployment/lab1-workers→lab1-workers-pv1-1.ProxmoxMachineTemplate/lab1-worker-2g→lab1-worker-2g-pv1-1, now with explicitallowedNodes: [pv1-1].
A rename creates new objects. kubectl apply doesn't prune, so the owner deletes the old MD and PMT
once the old MD has scaled to 0. The timing is right because the CA was already scaling every worker
down (inflate at 0). The new MD comes up with 0 replicas: replicas is unset, and the CAPI webhook
defaults it to the autoscaler min-size annotation.
Step 4 — re-test after the fixes (2026-09-30 07:23 UTC)
The owner applied the changes in this order:
kubectl apply -f configs/capi/lab1.yaml: newlab1-workers-pv1-1+lab1-worker-2g-pv1-1, the pool label,maxPods, os/arch.- Deleted the old
MachineDeployment/lab1-workersandProxmoxMachineTemplate/lab1-worker-2g. helm upgrade --install ca … --version 9.59.0 -f configs/cluster-autoscaler/values.yaml:scan-interval 1m,v 2,balancing-label.- Scaled
inflate0 → 16.
The CA log (--since=15m):
orchestrator.go:752] Splitting scale-up between 2 similar node groups: {…/lab1-workers-pv1-2, …/lab1-workers-pv1-1}
orchestrator.go:263] Final scale-up plan: [{…/lab1-workers-pv1-2 0->4 (max: 12)} {…/lab1-workers-pv1-1 0->4 (max: 6)}]
- Placement: ProxmoxMachines
lab1-workers-pv1-1-*onpv1-1(VMIDs 103, 104, 107, 110) andlab1-workers-pv1-2-*onpv1-2(105, 106, 108, 109). - Nodes: all 8 Ready about 1–1.5 min after creation, with labels
node.cluster.x-k8s.io/pool=workersandkubernetes.io/os=linux. - Loop timing: idle loops now run on the
1m0s timer. - One transient warning:
W clusterstate.go:510 Failed to find acceptable ranges for MachineDeployment/default/lab1-workers-pv1-1, once, in the loop during which the new MD was created. Acceptable ranges are snapshotted at the start of the loop, and the group appeared in a laterNodeGroups()call. Harmless; it did not repeat.
Worth noting:
ProxmoxMachineTemplate/lab1-worker(4 GiB, decision #15) is still in the cluster, and no MD references it. Deleting it is left to the owner.
Step 5 — docs and history (2026-09-30 09:28 CEST)
docs/decisions.md: #7 →superseded by #18. #18 (template copy + MD per PVE node, pool.31–.50, symmetric names) and #19 (CAbalancing-label,scan-interval 1m,v 2, QPS left at the default) are bothactive, since the lab confirmed them in Step 4.docs/phase-1-proxmox-capi.md:- intro and topology (PVE cluster, templates, both MDs, pool);
- Step 0: bridge on every node, per-node template;
- Step 5: annotations incl.
maxPods,labels, the pool label; - Step 6: the settings for two groups, and what a balanced scale-up logs;
- troubleshooting: 7 rows (maxPods, empty os/arch, no balancing, throttling, the acceptable-ranges warning, cross-node clone, VLAN on a new node).
docs/image-builder-proxmox.md: section "Template on other PVE nodes"; the result line names 101/9101.configs/capi/clusterctl.yaml.example: comments onTEMPLATE_VMID/ALLOWED_NODES.docs/future-tasks.md: report the three CA clusterapi bugs upstream. The namespace task now uses the new MD names.README.md(lab inventory, key decision 7),CLAUDE.md/AGENTS.md(lab facts): PVE cluster,pv1-2, template 9101..claude/settings.json: two inspector allowlist rulesmd lab1-workers→md lab1-workers-pv1-1.CHANGELOG.md: entry added after the owner confirmed the setup works (2026-09-30 11:05 CEST). The owner's pending #16/#17 work went in first as its own commit (5e5b6c5).