# Ansible Configuration

Ansible never configures a node here: it generates files on the deployment host, and Ignition applies them once, on the first boot after a node is installed.

It renders two Ignition configs per host from two templates — one the PXE environment [runs to install](<https://homelab.wlkr.ch/architecture/boot-process/#3-install-bootstrap>), one `flatcar-install` embeds into the system it writes. Changing anything under `ansible/` therefore takes a rebuild of the affected node, not a reboot. The directory layout is in [Repository layout](<https://homelab.wlkr.ch/architecture/#repository-layout>).

## Inventory

`ansible/inventory.yaml` is the single source of truth for the machines; every generated artifact comes from it, and the values are baked into the output rather than read at boot. The variables are described in [Quickstart step 2](<https://homelab.wlkr.ch/quickstart/#setup>). Every playbook targets the `k8s_nodes` group, the parent of `control_plane` and `workers`, which carries `ansible_user` and the SSH options.

> [!WARNING]
>
> **Every edit needs a regeneration, and some need two**
>
> `make config` re-renders the Ignition configs, PXE menus and router config from the inventory; `make download` fetches what `flatcar_channel`, `flatcar_version`, `syslinux_version` and `kubernetes_version` name. The Flatcar, Kubernetes and kube-vip versions are rendered into the configs too, so a version bump needs `make artifacts`, which runs both. Which target reads which variable is in the [Quickstart table](<https://homelab.wlkr.ch/quickstart/#setup>). An edit followed by neither changes nothing at all, silently.

`bootstrap_cluster` in the `control_plane` group decides whether the first control-plane host renders `kubeadm init` (`true`, the first install) or, like every other control plane, `kubeadm join --control-plane` (`false`, every rebuild). A rebuilt node that ran `init` would start a second cluster on the same VIP — see [Rebuilding a node](<https://homelab.wlkr.ch/operations/nodes/#rebuilding-or-repartitioning-a-node>).

`kube_vip_version` only sets the bootstrap static pod that gives `kubeadm init` a VIP; once ArgoCD syncs, `payload/platform/kube-vip/` takes over and its image is what runs — see [Control Plane VIP](<https://homelab.wlkr.ch/operations/control-plane-vip/index.md>).

## Playbooks

| Playbook | Does |
| --- | --- |
| `config.yaml` | Generates the bootstrap token, certificate key and etcd encryption key into `output/credentials/` (mode `0700`); renders the Ignition configs via Butane into `output/http` (mode `0600`), the sysupdate config for running nodes beside them, the per-MAC PXE menus into `output/tftp/pxelinux.cfg/`, the router's FRR config into `output/router/frr-bgp.conf`, and a readable copy of the kubeadm config into `output/tmp/` (the copy that runs is inlined into Ignition) |
| `download.yaml` | Fetches the Flatcar kernel, initrd, OS image and its detached signature, checked against the `.DIGESTS.asc` that `gpgv` verifies; the Kubernetes sysext image; and the syslinux bootloader files, checked the same way against `sha256sums.asc`. The signing keys are fetched and trusted through the primary fingerprints pinned in the playbook |
| `reinstall.yaml` | Rewrites `DEFAULT` in the generated PXE menus — `install` to arm, `localboot` to disarm. `make reinstall` and `make reinstall-cancel` are the two directions, both taking `LIMIT=<host>`. Arming refuses while `bootstrap_cluster` is `true` and the API VIP answers, or when an [endpoint name](<https://homelab.wlkr.ch/operations/control-plane-vip/#endpoint-name>) is not the one the cluster publishes. It touches only `output/tftp/pxelinux.cfg/`, so the next `make config` regenerates the safe default. See [Switching back to local boot](<https://homelab.wlkr.ch/architecture/boot-process/#switching-back-to-local-boot>) |
| `kubeconfig.yaml` | Copies the admin kubeconfig over SSH to `output/kubeconfig`, mode `0600`, with the VIP as its `server` |

`config.yaml` is safe to re-run: the `password` lookup reads an existing credential file back instead of generating a new value, so the encryption key every control-plane node shares stays the one etcd's Secrets were encrypted with.

`config.yaml` renews the join credentials on the first control-plane host that answers with a running cluster, so the node being [replaced](<https://homelab.wlkr.ch/operations/nodes/#replacing-a-failed-node>) may be any of them; it fails when none answers while `output/kubeconfig` exists, since a node booted then could not join, and skips the renewal silently only before the first bootstrap.

`kubeconfig.yaml` targets `control_plane[0]` — the first control-plane host in the inventory, with no fallback. If that node is down, copy `/etc/kubernetes/admin.conf` off any other control-plane node by hand. Keep a copy of the result off the cluster: it is the credential you want when the web UIs are unreachable, and `output/` is exactly as durable as the laptop it is on.
