Guide: VirtualMachine Pools¶
Keep a set of already-provisioned VMs standing by, so a consumer gets one immediately instead of waiting out provisioning.
A VirtualMachinePool creates nothing but ordinary VirtualMachines. The
scheduler places them, providers realise them, and no provider learns that
pools exist — which is why there is no VSpherePool or LibvirtPool and
never will be
(ADR-0046).
Scope
A pool maintains a warm set. Handing a member out to a specific
consumer — binding, expiry, and the guarantee that a member is used once
and then destroyed — is VirtualMachineClaim, which is not implemented
yet (ADR-0047). Today you can watch a pool fill, refill and roll; you
cannot yet claim from it.
Why a pool exists at all¶
For an installMode: Immediate image a VM boots from an already-installed
disk, and provisioning is fast enough that a pool is a convenience.
For installMode: Deferred it is not a convenience. A disk sealed to a
per-VM vTPM can only be produced by letting the guest install itself on
first boot, with its own TPM attached
(ADR-0040),
and Instant Clone is ruled out
(ADR-0052).
So that VM costs a full unattended install — minutes — and no provisioning
cleverness removes it, because the slowness is the security property.
The only way to hand out such a VM quickly is to have installed it already.
Quick start¶
examples/18-virtualmachinepool.yaml is a complete pool. It pairs with the
Provider, VMImage and VMClass from examples/17-virtualmachine-libvirt.yaml.
kubectl apply -f examples/17-virtualmachine-libvirt.yaml
kubectl apply -f examples/18-virtualmachinepool.yaml
kubectl get virtualmachinepool,virtualmachine -n banlieue-system -w
Within a reconcile you will see members appear with generated names:
NAME WARM AVAILABLE PROVISIONING CLAIMED AGE
sandbox-pool 2 0 2 0 5s
NAME CLASS IMAGE PROVIDER POWER READY
sandbox-pool-9dmtx small ubuntu-22.04-libvirt grill
sandbox-pool-bfttl small ubuntu-22.04-libvirt grill
and, once they provision, AVAILABLE reaches WARM.
The one field that can fail silently¶
spec.readiness selects which member condition means claimable. It is
required and has no default, which is unusual in this API and
deliberate.
| Value | Means | Use when |
|---|---|---|
InfrastructureReady |
the backend says the VM exists and is running | installMode: Immediate |
GuestReady |
the installed guest booted and announced itself | installMode: Deferred |
For a Deferred image, InfrastructureReady fires when the install
starts, not when it finishes. A pool using it would hand out VMs that
are still installing.
GuestReady is not satisfiable yet
ADR-0043 is what publishes that condition, and it is not implemented. A
pool set to GuestReady today reports:
Warm=False reason=ReadinessSignalAbsent
message: no member has published the "GuestReady" condition;
spec.readiness selects a signal nothing is setting
and never fills. That condition exists because the alternative — a pool sitting at zero with no explanation — is indistinguishable from a slow install.
The practical consequence: pools of TPM-sealed VMs are not achievable
until ADR-0043 lands. Pools of Immediate VMs work today.
This is also why the field has no default. GuestReady is the right
default for the use case pools were built for, and defaulting to it would
make every pool silently do nothing.
Sizing¶
Two numbers, and one of them is usually got wrong:
warmReplicas >= peak requests per minute x minutes to provision one member
maxReplicas >= warmReplicas + peak concurrent holds + maxSurge
A pool shallower than the first line hands out nothing during a burst — the consumer waits exactly as long as it would have with no pool at all.
Without the + maxSurge term in the second, an image rollout has to retire
warm capacity to build its replacements, and status.available dips while
it does.
maxSurge is a host-protection knob, not a throughput knob. Each
provisioning member is a full install's worth of disk and CPU on hosts that
are also running members already handed out. If refill is too slow, raise
warmReplicas or shorten the install — raising maxSurge mostly moves the
pain onto the hosts.
What the pool does on its own¶
Refills¶
Members are replaced whenever available drops below warmReplicas —
whether a member was deleted, expired, or failed. Members carry
generateName, never an index: they are deleted from the middle of the set
constantly, so nothing may depend on an ordinal.
Reaps poisoned members¶
A member still provisioning past provisioningTimeoutSeconds (default
1800s) is deleted and replaced, never repaired. A pool has no way to
diagnose a half-finished unattended install, and a warm set is worthless if
it can contain one.
The default is generous on purpose — a Deferred member is not Ready until a full install plus a reboot have finished. Lowering it reaps healthy members mid-install, which then get replaced by members that are reaped the same way.
Follows the image¶
recycleOnImageChange defaults on, unlike most booleans in this API. A
warm member is an already-installed VM: a pool that ignored image rebuilds
would keep handing out the unpatched build indefinitely, and nothing would
say so.
Rollout is surge-style — a stale member is retired only once enough fresh
members are ready to keep warmReplicas claimable, unless maxReplicas
leaves no room to build the replacement first. Give it headroom and
available never dips.
maxIdleSeconds bounds how stale a member can get between rebuilds.
Addressing¶
Omit spec.addressing for DHCP or class-level IPAM. Set it to stamp a
static address per member into the template's networkOverrides:
addressing:
interface: eth0 # matches a VMClass network interface name
rangeStart: 192.0.2.10
rangeEnd: 192.0.2.29 # 20 addresses for maxReplicas: 6
prefix: 24
gateway: 192.0.2.1
Size the range above maxReplicas, with spares. An address is held
until a deleted member's backend VM is really gone, not until its delete is
issued — so a pool churning members needs more addresses than it has
members. When the range runs out the pool reports:
This inline range is interim. CAPI IPAM
(ADR-0033)
is recorded but not implemented; when it lands this field gains a poolRef
alternative and the inline range stays as the zero-dependency option.
Reading a pool's status¶
| Field | Meaning |
|---|---|
replicas |
members of any phase, excluding ones already being deleted |
available |
Ready and unclaimed — what could be handed out right now |
provisioning |
members still coming up |
claimed |
members held by a claim (always 0 until ADR-0047) |
imageRevision |
the image build new members are being created from |
Conditions:
| Type | Reason | Meaning |
|---|---|---|
Warm |
Warm |
available >= warmReplicas — the pool is doing its job |
Warm |
Filling |
not warm yet, but making progress |
Warm |
ReadinessSignalAbsent |
stuck — nothing publishes the condition spec.readiness names |
Capacity |
MaxReplicasReached |
wanted to create members, maxReplicas forbade it |
Capacity |
AddressRangeExhausted |
wanted to create members, no free address |
Filling and ReadinessSignalAbsent are the pair worth telling apart:
the first means wait, the second means the pool will never warm.
Deleting a pool¶
Deleting a pool deletes its members, and deleting a member deletes its backend VM. That is the intended behaviour and also a sharp edge:
Members are owned by the pool through ownerReferences, so Kubernetes
garbage-collects them, and each member's own finalizer blocks until the
backend VM is really gone. The base image the members were built from is
never touched — it belongs to the VMImage.
Once VirtualMachineClaim lands (ADR-0047), a claimed member is re-parented
to its claim at bind time, so deleting a pool will not kill VMs that are in
use.
Troubleshooting¶
| Symptom | Cause |
|---|---|
Warm=False, reason ReadinessSignalAbsent |
spec.readiness names a condition nothing publishes. Almost always GuestReady before ADR-0043. Switch to InfrastructureReady if the image is Immediate. |
available stays 0, members exist and are Ready |
The members' Ready is not the condition spec.readiness selects. Check kubectl get virtualmachine <member> -o jsonpath='{.status.conditions}'. |
| No members created at all | The pool's template does not schedule. Check a member's Scheduled condition; if there are no members, check the pool's own events and that the VMClass and VMImage it names exist. |
Capacity=False, MaxReplicasReached |
maxReplicas is below what warmReplicas plus churn needs. |
Capacity=False, AddressRangeExhausted |
The address range has no spares above maxReplicas. |
| Members churn: created, then deleted, repeatedly | provisioningTimeoutSeconds is shorter than the image takes to install. |
Pool keeps creating members up to maxReplicas |
The readiness signal is never satisfied, so available never rises and the pool keeps trying. The ReadinessSignalAbsent condition above names the cause. |
Full schema reference¶
VirtualMachinePoolAPI reference- ADR-0046 —
VirtualMachinePool: warm, never-reused VMs VirtualMachineAPI reference — a member is one of these