Skip to content

Boot & Bootstrap Process

This page follows a single node from the moment you press the power button to the moment it shows up in kubectl get nodes. Understanding this sequence is what turns a stalled PXE boot from a mystery into a question with an obvious next step: which arrow didn't happen?

1. Preparation (on the deployment host)

Before any node is powered on, the operator runs make config to generate the per-host Ignition and PXE configs, then make serve to start the TFTP and HTTP servers. See the Quickstart for the exact sequence.

2. Network Boot

sequenceDiagram
    participant Node
    participant DHCP
    participant Server as Boot Server

    Node->>DHCP: 1. PXE request
    DHCP-->>Node: 2. IP, next-server, filename
    Node->>Server: 3. TFTP bootloader
    Server-->>Node: 4. syslinux + menu
    Node->>Server: 5. HTTP kernel, initrd
    Server-->>Node: 6. Flatcar kernel, initrd

The DHCP server is external to this project. It must hand out the boot server's IP as next-server and a syslinux filename — see the Quickstart prerequisites.

Step 2 is where most first attempts die, and it dies silently: the node asks, nothing useful answers, and the firmware moves on to the next boot device without a word of complaint.

3. Install & Bootstrap

PXE does not boot the node; it boots the installer — and only when the node has been armed with make reinstall, since the generated config otherwise says DEFAULT localboot. What it loads is a RAM environment whose only job is to write Flatcar to install_disk and reboot into it.

There are two templates, because there are two machines. The RAM environment and the node it installs share a disk and nothing else, and pretending otherwise is what made every failure in this page's troubleshooting possible.

renders to runs in
butane_installer_config.yaml.j2 ignition-<host>-install.json the PXE environment
butane_node_config.yaml.j2 ignition-<host>.json the installed system, on first boot

The PXE menu points at the first. The installer fetches the second as a plain file and hands it to flatcar-install -i, which embeds it in the OEM partition of the system it writes — so the node's own config is delivered by the installer without ever being executed in it.

The installer config is deliberately small: an SSH key, wipe_table: true, the one file to hand onward, and the two flatcar-install units. Everything it does not contain is downloaded into a RAM disk and discarded ninety seconds later — which used to include 144 MiB of sysext images the installer had no use for.

Keep the installer minimal

Anything added to butane_installer_config.yaml.j2 is paid for on every install of every node, in RAM and in download time, and thrown away. If it configures the node rather than the installation, it belongs in butane_node_config.yaml.j2.

sequenceDiagram
    participant Node
    participant Server as Boot Server

    Node->>Server: 7. HTTP Ignition config
    Note over Node,Server: Only when armed with make reinstall
    Server-->>Node: 8. ignition-<host>-install.json
    Node->>Server: 9. HTTP Flatcar image + signature
    Note over Server: Server disarms the menu back to localboot
    Node->>Node: 10. Wipe disk, flatcar-install, reboot
    Note over Node: Now booting from disk, not the network
    Node->>Node: 11. Ignition partitions and writes /etc
    Node->>Server: 12. HTTP sysext images
    Server-->>Node: 13. kubernetes, containerd
    Node->>Node: 14. systemd unit runs kubeadm
    Note over Node: Node is NotReady - no CNI yet

Step 10 is the point of no return, and the wipe is deliberately in three parts rather than one:

Ignition wipe_table Destroys the GPT, in the initramfs, before the installer unit runs. Only in ignition-<host>-install.json — see Wiping the disk
blkdiscard Returns the whole device to unwritten. Best-effort — SATA without TRIM declines it
format: none on rook-osd On the installed system's first boot, in butane_node_config.yaml.j2

Boot order

The install leaves the firmware's boot order alone. flatcar-install can write a UEFI boot entry for the disk with -u, which is efibootmgr -c and puts that entry at the front of BootOrder; this project does not pass it.

That is deliberate, because the boot order is the one thing the PXE menu cannot override. These nodes are set to network boot first and reach their disk through LOCALBOOT, so the generated menu decides what happens on every boot of every node. An install that quietly promoted the disk ahead of PXE would take a node out of that arrangement: make reinstall would rewrite a menu the firmware had stopped reading, and the node would ignore it with no error anywhere.

The cost is that a node has to be able to reach its disk without that entry — network boot first with a working LOCALBOOT, or the disk ahead of PXE in the firmware. A machine with neither installs correctly and then has nothing to boot.

Wiping the disk

wipe_table is the one setting the two environments need opposite answers for, and it is why there are two files rather than one.

In the installer it must be true. The disk is about to be overwritten wholesale, so starting from a blank table is what makes an install reproducible: partition numbers, offsets and sizes come out the same whether the disk was empty or held the last cluster. With it false, a rebuild inherits the previous layout — Ignition pins rook-osd to its old offset while growing ROOT over it, and ignition-disks.service fails in the initramfs:

sgdisk --delete=9 --delete=10 --new=9:12722176:+102400000 --new=10:65150976:+0
Could not create partition 9 from 12722176 to 115122175

The node drops to an emergency shell before flatcar-install.service ever runs, so the disk is never touched. A node that has never been installed is unaffected, which is why this only appears on a rebuild.

In the installed system it must be false, and Ignition will not do it anyway. That config runs on the first boot from the disk it would be wiping, and Ignition refuses by name — before reading the partition table, before running sgdisk, about a millisecond into the stage:

Ignition failed: create partitions failed: refusing to wipe active disk "/run/ignition/dev_aliases/dev/nvme0n1"

Which makes it the safer of the two mistakes: loud, and before anything is written. The installer side has no such guard. A missing wipe there does not announce itself — it surfaces as the sgdisk error above, about offsets you have to work backwards from, and only on hardware that has been installed before.

The tell that the two values have been collapsed into one is quieter still: resize: true on ROOT stops mattering. With the table always wiped there is never an existing partition to match, so the flag can never fire.

storage.filesystems follows the same split for the same reason. The installer has just wiped the table, so rook-osd does not exist there, and asking Ignition to prepare a filesystem on it blocks until it gives up:

Ignition failed: failed to create filesystems: failed to wait on filesystems devs:
device unit dev-disk-by\x2dpartlabel-rook\x2dosd.device timeout

Erasing the previous cluster's BlueStore signature belongs on the installed system regardless — that is where the partition is.

The third wipe is about Ceph specifically. BlueStore metadata lives at the start of the raw rook-osd partition, and ceph-volume reads that signature rather than the partition table — so a repartition alone can resurrect an OSD on a cluster that has never heard of it. The partition is new; the bytes under it are not, which is why the erase belongs where the partition is created.

Check install_disk before you check anything else.

Between steps 9 and 10 the boot server rewrites that node's menu back to DEFAULT localboot. Nothing else would: make reinstall arms the menu and the generated files never disarm it, so on firmware that network boots first the reboot in step 10 would read the same armed menu and start the install over. See Boot Server → Switching back to local boot.

Step 11 runs from disk, not from the network. Ignition is embedded in the OEM partition by flatcar-install -i, so the node no longer depends on the boot server for its config — only for the sysext images in step 12, and only on this first boot.

Step 14 leaving the node NotReady is correct and expected — there is no CNI yet, so the kubelet has nothing to plug pods into. It stays that way until make install-core lands Cilium.

Every boot after the first

The node boots from its own disk. The boot server can be, and should be, switched off.

/ ext4 on partition 9, capped at 50 GB and grown into it by grow-root.service
Ignition Runs once, on the first boot after the install
/etc/kubernetes, /var/lib/etcd, /var/lib/rook On disk; survive a reboot
rook-osd Partition 10, raw and unmounted — Ceph owns it

A reboot is therefore just a reboot. bootstrap-k8s.service does not fire, because its ConditionPathExists=!/etc/kubernetes/kubelet.conf is no longer satisfied — the file is still there from last time. etcd comes back with its data, Rook finds its OSD, and the kubelet rejoins a cluster it never left.

Why grow-root.service exists

Flatcar grows its root filesystem on first boot, and taking partition 9 over in Ignition is precisely what stops that happening — the stock systemd-growfs-root.service is static and is pulled in by an x-systemd.growfs mount option, which a root mounted from root=LABEL=ROOT on the kernel command line does not carry. Without the unit, a node comes up with a 50 GB ROOT partition holding the image's original ~1.6 GB filesystem — about 1.2 GB free for everything the node writes. It runs the same binary the stock unit does, and is a no-op once the filesystem already fills the partition.

Two boot paths, and the menu picks the safe one

Nothing is chosen at the console. PROMPT 0 boots whatever DEFAULT names and shows no menu, and the template always emits DEFAULT localboot — so a node that network-boots for any reason ends up on its own disk, with no keyboard involved. Installing means arming it with make reinstall, which rewrites that one line in the generated file; see Repartitioning the nodes. Holding Shift or Alt at boot still forces the prompt, which is the escape hatch for a node that is armed and should not be.

4. Post-Installation Bootstrap

Once Kubeadm has initialized the control plane, the remaining components are installed from the deployment host. This is the last time anything is applied by hand; after step 6 the repository is in charge.

sequenceDiagram
    participant Admin as Operator
    participant Deploy as Deployment Host
    participant Cluster

    Admin->>Deploy: 1. make install-core
    Deploy->>Cluster: 2. Helm install Cilium, cert-manager
    Note over Cluster: Nodes become Ready
    Admin->>Deploy: 3. make install-argo
    Deploy->>Cluster: 4. Helm install ArgoCD
    Admin->>Deploy: 5. make bootstrap-apps
    Deploy->>Cluster: 6. Apply root.yaml (App-of-Apps)

Note

make untaint is not part of this flow. It removes the control-plane NoSchedule taint and applies only to a single-node cluster. The layout in Architecture Overview has dedicated workers, so the taint should stay in place — an untainted control plane is a control plane that will one day be evicted by a Helm chart with ambitious resource requests. On a single node it is not optional and it goes before step 1: cert-manager has no tolerations, so make install-core hangs waiting on pods that cannot be scheduled. See Single-node clusters.