User guide: MachineSet
A Deployment/ReplicaSet-shaped abstraction over Machine: kairon-controller
creates/deletes plain Machine objects to match spec.replicas, each built
from spec.template -- so a fleet of identical Machines doesn't mean
hand-creating each one and hand-tracking template drift yourself.
Example
apiVersion: kairon.zyvor.dev/v1alpha1
kind: MachineSet
metadata:
name: web
namespace: prod
spec:
replicas: 3
strategy: RollingUpdate
maxUnavailable: 1
template:
labels:
app: web
spec:
image: {path: /var/lib/fluxvm/images/web.qcow2}
resources: {cpu: "2", memory: "4Gi"}
runtime: {backend: qemu}
powerState: Running
Or, for the flag-friendly fields above, kaironctl create:
$ kaironctl create machineset web --image /var/lib/fluxvm/images/web.qcow2 \
--cpu 2 --memory 4Gi --replicas 3 --max-unavailable 1 --label app=web
machineset/web created
$ kaironctl scale machineset web --replicas 5
machineset/web scaled to 5 replicas
$ kaironctl edit machineset web --strategy Recreate --max-unavailable 2
machineset/web updated
create machineset reuses the exact same Machine-spec flags kaironctl create (a single Machine) already has for spec.template.spec -- anything
those flags don't cover (spec.placement, device claims, security,
per-volume claims) still needs kubectl apply/YAML, the same limit a plain
create machine already has for those fields. scale (replicas) and edit
(--strategy/--max-unavailable) are the only mutations these verbs
support beyond create -- editing the template itself still means kubectl edit/apply.
scale also supports scaling every MachineSet a label selector matches to
the same replica count in one call (kaironctl scale machineset --selector env=staging --replicas 0) instead of a single NAME -- see
kaironctl-scale-selector.md.
spec.template.spec is exactly a Machine's own spec -- anything a
hand-created Machine accepts (image, volumes, deviceClaims,
network, cloudInit, ...) works here unchanged. spec.template.labels
are applied to every replica in addition to (never replacing) the two
labels kairon-controller itself always adds:
kairon.zyvor.dev/machineset=<name> and
kairon.zyvor.dev/machineset-template-hash=<hash of the current template>.
Every replica is a completely ordinary Machine object once created --
kubectl get machines, kaironctl evacuate, MachineDisruptionBudget
selectors (match on spec.template.labels, e.g. {app: web}, to throttle
disruption across a MachineSet's own replicas the same way they'd throttle
any other Machines), and everything else that already operates on Machines
keeps working exactly as it does today. kaironctl get machinesets (or
kubectl get machinesets) lists the MachineSets themselves --
STRATEGY/REPLICAS/READY/UPDATED at a glance, without having to
separately count matching Machines by hand -- kairon-ui's own "Machine
sets" dashboard page (read-only, GET /api/v1/machinesets) shows the same
thing.
Deleting a MachineSet deletes its replicas too
kubectl delete machineset/kaironctl delete machineset deletes every
Machine it owns along with it -- Kairon has no Kubernetes
ownerReference/garbage-collection mechanism to do this for free (no
client-go dependency anywhere in this project to drive one), so
kairon-controller does it explicitly: a kairon.zyvor.dev/machineset-cleanup
finalizer holds the MachineSet object in Kubernetes until every Machine
still carrying its kairon.zyvor.dev/machineset label is actually gone
(not merely deletion-requested -- each Machine's own runtime-cleanup
finalizer keeps it around for a few more ticks while kairon-node tears
down its FluxVM VM). A single stuck Machine delete leaves the MachineSet
in Terminating and is retried every tick from a fresh Machine listing,
the same fail-closed shape NetworkSecurityGroup/MachineNetworkPolicy
deletion already uses.
How replicas are reconciled
Every reconcile tick, for each MachineSet:
- Find its owned Machines (
kairon.zyvor.dev/machinesetlabel match, excluding any already being deleted). - Split them into current (template hash matches
spec.templateright now) and outdated (an earlier template hash -- meaningspec.templatechanged since they were created). - If the total (current + outdated) is below
spec.replicas, create enough current-template replicas to fill the gap -- this always takes priority over any rollout concern, whether the shortfall is a fresh scale-up or a replica that died on its own. - Otherwise, advance the rollout by exactly one bounded step (see below), never the whole thing in one tick -- a MachineSet rollout is always paced by, and visible across, real reconcile intervals.
status.replicas/.readyReplicas/.updatedReplicas are patched every
tick from this same pass -- readyReplicas counts current-template
replicas whose status.phase is Running; updatedReplicas counts every
current-template replica regardless of phase.
Metrics and alerts
kairon-controller's /metrics also exposes
kairon_machineset_status{namespace, machineset, field}, one gauge per
field (replicas/ready_replicas/updated_replicas) -- mirroring
kube-state-metrics' own kube_replicaset_status_replicas/
kube_replicaset_status_ready_replicas/kube_deployment_status_replicas_updated
gauges, collapsed into a single vector the same way kairon_quota_resource/
kairon_disruption_budget_status already collapse their own dimensions.
Recorded once per Reconcile tick (internal/metrics.Recorder.ObserveMachineSets,
called from reconcileMachineSets above) from the exact same tally that
tick's status patch uses -- never a second, independently-computed number
that could drift from what kubectl get machineset shows for the same
tick. kairon-controller-only, for the same reason kairon_quota_resource
is: kairon-node never lists MachineSets cluster-wide (it only
reconciles individual Machines already assigned to it) and kairon-ui
has no reconcile loop at all, so neither has a meaningful per-tick tally to
report here. See docs/guides/observability.md.
charts/kairon/alerts.yaml's kairon-machinesets group adds
KaironMachineSetRolloutStuck: ready_replicas below replicas for over
30 minutes on some MachineSet, the same "sustained gap, not a transient
blip" 30-minute threshold KaironMigrationStuckInFlight already uses for
an in-flight migration. Previously the only signal a rollout had stalled
was polling kaironctl get machinesets/kubectl get machinesets and
noticing READY never catches up to REPLICAS -- this fires proactively
instead. severity: warn, pointed at this page rather than a dedicated
runbook (check kubectl get machines -l kairon.zyvor.dev/machineset=<name>
for the specific stuck replica, per the "Real limits today" section below).
Rollout strategy
RollingUpdate(the default): replaces outdated replicasmaxUnavailable-at-a-time, computed each tick as(ready current + outdated) - (replicas - maxUnavailable)-- i.e. never letting actual available capacity drop further belowreplicasthanmaxUnavailableallows. A current-template replica only counts toward that available total once itsstatus.phaseisRunning; one that merely exists (just created, not yet scheduled or booted) doesn't -- so a rollout won't tear down another old, healthy replica to make room for a replacement that hasn't actually come up yet. Replacement creation happens automatically on a later tick, once a deletion has actually dropped the total belowreplicasagain (step 3 above picks it up).maxUnavailableis an integer or a percentage string (e.g."25%"), same parsingMachineDisruptionBudgetalready uses; empty defaults to1.Recreate: terminates every outdated replica first, and creates no replacements until none remain -- matches a real KubernetesDeployment's ownRecreatestrategy (full stop, then restart) exactly. Simpler thanRollingUpdate, at the cost of a window with fewer thanreplicasMachines running during every template change.
Changing spec.template never touches an already-running replica directly
-- it only changes what new replicas look like; existing ones are
replaced (not edited in place) by the rollout logic above.
Real limits today (first cut)
- Gated on
status.phase == Running, not on deeper application health.RollingUpdatenow withholds further replacement until each already- created current-template replica reachesRunning(see above) -- but that's Kairon's own coarse VM-power-state signal, not a guest-level readiness probe (nothing here waits for a guest agent heartbeat, an HTTP health check, or any other application-defined signal). A template with a boot failure severe enough to never reachRunningcorrectly stalls the rollout in place rather than cycling every replica through it; one that boots fine but is application-broken is not caught here. - No
maxSurge. A rollout never temporarily exceedsspec.replicas-- onlymaxUnavailable(a dip below, never a surge above) is offered, since Kairon's scheduler (internal/scheduler) has no capacity/ overcommit model to safely reason about a temporary surge against yet. - No autoscaling.
spec.replicasis only ever changed by you (or your own tooling) editing the object -- there's noMachineHorizontalAutoscaleror similar reacting to load. - No pause/rollback primitive. There's no
kubectl rollout pause/undoequivalent -- to stop a rollout partway, editspec.templateback to the previous shape yourself; the same incremental reconciliation resumes toward whateverspec.templatesays right now. - Replica names are randomly suffixed (
<machineset-name>-<8 hex chars>), not sequential/predictable -- don't script against a specific replica's name surviving a rollout.