Build a K8s-Ready Node Template¶
The Kubernetes Engine's Auto-provision node source (see Deploy a Kubernetes Cluster) clones a VM template for every control-plane/worker node it creates, then drives the whole install over SSH — join the cluster, mount storage, everything. That SSH step needs three things from the template that a bare OS install doesn't give you for free: it has to be dnf-based, it has to boot with cloud-init enabled, and it should carry the guest agent. Skip any of these and cluster creation stalls or times out with no obvious cause from the Kubernetes tab itself — the fix belongs on the template, not in a support ticket.
This is the general golden-image flow, K8s-flavored
Everything here follows Create a Golden Image Template. This page exists because K8s nodes have stricter requirements than a general-purpose template — read that page first if you haven't built a template before.
Why each requirement matters¶
| Requirement | Why K8s needs it |
|---|---|
| dnf-based (Rocky/RHEL/Alma family) | LINSTOR CSI's satellite bootstrap (core/linstor_node_setup.py) runs command -v dnf as a hard precondition and installs kmod-drbd9x via ELRepo — it has no apt/apk path. Even if you're not using LINSTOR storage, pick a dnf-based template so the option stays open. |
| cloud-init installed and enabled | The engine's per-node seed ISO (NoCloud datasource) is how the node gets its hostname, the k8s SSH user, and your master's SSH key. No cloud-init on the template = no SSH user ever gets created = every node create times out waiting for SSH or waiting for a guest IP, even though the VM itself boots fine. |
| qemu-guest-agent installed and enabled | MFCloud's guest-IP detection prefers the agent channel over ARP/DHCP-lease scanning. Without it, IP detection is slower and less reliable, especially on VLANs with no libvirt-managed DHCP. |
Step-by-step¶
1. Deploy a plain base VM¶
Inventory → Deploy VM, any Rocky/RHEL/Alma ISO or existing minimal image, on the host you want the template to live on (templates are node-local — see Create a Golden Image Template). Give it enough disk for the largest node you'll ever clone from it — clones can only grow the disk, not shrink it below the template's own size.
2. SSH in and install the three packages¶
cloud-utils-growpart isn't strictly required but every K8s node profile lets
the operator pick disk_gb at deploy time — without growpart the extra space
just sits unpartitioned.
3. Enable the boot-time services¶
All four cloud-init units matter — cloud-init-local and cloud-init handle
network/hostname/datasource detection early in boot, cloud-config and
cloud-final run the users:/runcmd: sections of the seed data (this is
where the k8s user and its SSH key actually get created). Enabling only
cloud-init.service is a common half-fix that leaves user/SSH-key injection
broken while hostname/network still appear to work.
4. Confirm nothing blocks NoCloud detection¶
Stock cloud-init packages ship with no override here, which is what you want
— ds-identify runs its default detection at boot and finds the cidata-
labeled seed ISO MFCloud attaches automatically. If a previous provisioning
tool (a cloud vendor's own image customization, for example) left a
datasource_list override pointing at something else, remove it or the
NoCloud seed will never be read.
5. Check for a hardcoded network profile¶
You want method=auto (DHCP) under [ipv4], not a static address — a static
IP baked into the template means every clone fights over the same address.
MFCloud doesn't set a static network-config in the seed data, so DHCP is what
every node profile expects.
6. Generalize and clean up¶
cloud-init clean --logs --seed
rm -f /etc/NetworkManager/system-connections/*.nmconnection
rm -f /etc/ssh/ssh_host_*
truncate -s 0 /etc/machine-id
rm -f /root/.ssh/authorized_keys /root/.bash_history
history -c
cloud-init cleanclears cached instance/datasource state so the template looks like a never-booted instance — without this, cloud-init may see a stale "already ran" marker and skip re-running on the clone.- Clearing SSH host keys and
machine-idstops every clone from sharing the same host identity (host keys regenerate automatically on next boot on Rocky/RHEL;machine-idregenerates via systemd). - Remove any SSH key you added to
root/authorized_keysfor provisioning access before converting — whatever's left in the template'sauthorized_keysgets cloned onto every future node.
7. Shut down and convert¶
Then VM action menu → Convert to Template (Golden Image) — same flow as any other template. See Create a Golden Image Template for the storage-tier placement rules and the Ceph/LINSTOR-backed-VM caveat (network disks can't be converted in place).
Verifying it worked¶
Deploy one throwaway node from the new template (either a plain VM deploy, or a single-control-plane K8s cluster with no storage/networking extras) and check:
GET /api/kubernetes/{id}— or the Kubernetes tab — reachesProvisioning→Activewithout stalling at "waiting for guest IP" or "waiting for SSH."- The node's hostname is the VM name, not
localhost— the single clearest sign cloud-init actually ran. Avirsh screenshotof the console showinglocalhost login:on a VM that's been up for several minutes means cloud-init never fired; go back to steps 2–4.
See also¶
- Create a Golden Image Template
- Deploy a Kubernetes Cluster
- Add LINSTOR / DRBD Storage — compute-node-side setup for LINSTOR CSI, separate from the K8s node template itself.
- Troubleshooting → Convert to Template Fails