|
|
|
@@ -1,10 +1,25 @@
|
|
|
|
|
# Cold-rebuild runbook — homelab cluster from scratch
|
|
|
|
|
|
|
|
|
|
Rebuild the entire cluster from **this repo + backups only**, assuming nothing
|
|
|
|
|
in the cluster survives (all VMs gone, Ceph gone, in-cluster Gitea gone).
|
|
|
|
|
in the cluster survives (all three nodes wiped, Ceph gone, in-cluster Gitea
|
|
|
|
|
gone).
|
|
|
|
|
|
|
|
|
|
Order: **tofu → talos → flux → openbao → apps.**
|
|
|
|
|
|
|
|
|
|
All three nodes are bare-metal MS-01 machines. Nothing provisions them: there
|
|
|
|
|
are no VMs to recreate, and OpenTofu owns only DNS, object storage and UniFi.
|
|
|
|
|
Step 1 is therefore two unrelated halves — a `tofu apply` for the platform
|
|
|
|
|
resources, and physically booting three machines from install media.
|
|
|
|
|
|
|
|
|
|
**Someone has to be at the machines to boot them from USB** — MS-01 has no IPMI
|
|
|
|
|
and nothing here does PXE. Everything after that is remote.
|
|
|
|
|
|
|
|
|
|
A node in maintenance mode configures no VLANs and DHCPs untagged, so it comes
|
|
|
|
|
up on whatever untagged network its ports carry — not on management, which is
|
|
|
|
|
a tagged VLAN on the X710 trunk. It is reachable there: that is the address
|
|
|
|
|
`apply-config --insecure` talks to in Step 2, and it is how all three
|
|
|
|
|
bare-metal conversions were driven. Find the leases on the UDM.
|
|
|
|
|
|
|
|
|
|
## The circular-bootstrap problem this runbook exists to break
|
|
|
|
|
|
|
|
|
|
In steady state Flux's `GitRepository flux-system` pulls manifests from the
|
|
|
|
@@ -61,12 +76,21 @@ These must exist outside the cluster or the rebuild cannot complete:
|
|
|
|
|
`openbao-daily` offsite copy. In a total-loss rebuild the offsite Velero
|
|
|
|
|
copy is the one that survives.
|
|
|
|
|
- [ ] **Tooling:** `talosctl`, `kubectl`, `flux`, `tofu`, `bao`, plus the
|
|
|
|
|
OpenTofu backend + Proxmox/UniFi credentials the `opentofu/` stack needs.
|
|
|
|
|
OpenTofu backend + UniFi credentials the `opentofu/` stack needs. No
|
|
|
|
|
Proxmox credentials — there is no Proxmox any more.
|
|
|
|
|
- [ ] **Install media** written and to hand: the Image Factory ISO named in
|
|
|
|
|
Step 1, on a USB stick, for all three machines.
|
|
|
|
|
- [ ] **Someone at the machines to boot them from USB.** No IPMI, no PXE. A
|
|
|
|
|
monitor is useful for reading boot errors but is not required to find the
|
|
|
|
|
node afterwards — it DHCPs into management on its own (see the note at
|
|
|
|
|
the top). Everything past the boot is remote.
|
|
|
|
|
|
|
|
|
|
## Step 1 — tofu: provision the VMs + supporting infra
|
|
|
|
|
## Step 1a — tofu: DNS, object storage, UniFi
|
|
|
|
|
|
|
|
|
|
Recreate the Talos VMs and the platform resources OpenTofu owns (Proxmox VMs,
|
|
|
|
|
UniFi DNS records incl. `k8s.renner.uno` / node A records, etc.).
|
|
|
|
|
OpenTofu no longer provisions any node. The `proxmox_vm` module still exists in
|
|
|
|
|
the tree but nothing instantiates it, so `tofu apply` here creates only the
|
|
|
|
|
platform resources: UniFi DNS records including `k8s.renner.uno` and the node A
|
|
|
|
|
records, the Hetzner object storage, and the NAT hairpin rules.
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
cd opentofu
|
|
|
|
@@ -74,44 +98,119 @@ tofu init -upgrade -backend-config="access_key=..." -backend-config="secret_key=
|
|
|
|
|
tofu apply
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
In CI this is `.woodpecker/deploy-infrastructure.yml` (manual, `TOFU_APPLY=true`),
|
|
|
|
|
which also pre-seeds the Talos image onto every Proxmox node. Wait until the
|
|
|
|
|
node FQDNs (`k8s-{1,2,3}.srv.renner.uno`) resolve and the VMs are up before
|
|
|
|
|
Step 2 — `bootstrap.sh` polls DNS but will not create the records.
|
|
|
|
|
Run this first regardless, because Step 2 needs `k8s.renner.uno` and the node
|
|
|
|
|
FQDNs to resolve — the control-plane endpoint is baked into every generated
|
|
|
|
|
config, and the nodes are addressed by name.
|
|
|
|
|
|
|
|
|
|
**k8s-1 is not a VM and OpenTofu does not create it.** jupiter runs Talos on
|
|
|
|
|
bare metal, so this step only provisions k8s-2 (saturn) and k8s-3 (neptune).
|
|
|
|
|
Before Step 2, boot jupiter from the bare-metal image below so it comes up in
|
|
|
|
|
maintenance mode and answers on `k8s-1.srv.renner.uno` — `bootstrap.sh` treats
|
|
|
|
|
all three nodes identically from there, since `apply-config --insecure` does
|
|
|
|
|
not care whether the target is a VM or bare metal.
|
|
|
|
|
## Step 1b — boot all three machines from install media
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
https://factory.talos.dev/image/0f6fb8bd8bb53013e9743b7c7b099d96fb9c32fd2464ced32693c2dc0ea5feb9/v1.12.11/metal-amd64.iso
|
|
|
|
|
https://factory.talos.dev/image/0f6fb8bd8bb53013e9743b7c7b099d96fb9c32fd2464ced32693c2dc0ea5feb9/v1.13.9/metal-amd64.iso
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
That schematic is deliberately not the one `.woodpecker/deploy-infrastructure.yml`
|
|
|
|
|
seeds onto the Proxmox nodes. It keeps the same `net.ifnames=1` kernel argument
|
|
|
|
|
— without it the predictable interface names that `talos/patches/k8s-1.yaml`
|
|
|
|
|
matches its VLAN parents against do not exist — and differs in three ways:
|
|
|
|
|
Write it to a USB stick and boot each MS-01 from it. Each comes up in
|
|
|
|
|
maintenance mode with a DHCP address on an untagged network — not on
|
|
|
|
|
management, which is tagged. Read the leases off the UDM; those addresses are
|
|
|
|
|
what Step 2 applies the first config to.
|
|
|
|
|
|
|
|
|
|
- drops `siderolabs/qemu-guest-agent`, which has nothing to talk to on bare metal
|
|
|
|
|
- adds `siderolabs/intel-ucode`, since no hypervisor is supplying microcode now
|
|
|
|
|
- adds `siderolabs/thunderbolt`, unused so far and inert until
|
|
|
|
|
`machine.kernel.modules` requests `thunderbolt`/`thunderbolt_net`. It is
|
|
|
|
|
present only so enabling the MS-01 Thunderbolt mesh later does not require a
|
|
|
|
|
new image and a reinstall. Note its udev rule auto-authorises any Thunderbolt
|
|
|
|
|
device attached to the machine once those modules are loaded.
|
|
|
|
|
The schematic `0f6fb8bd…` carries `net.ifnames=1` — without it the predictable
|
|
|
|
|
interface names that `talos/patches/k8s-{1,2,3}.yaml` match their VLAN parents
|
|
|
|
|
against do not exist, and the node ends up with no address on any interface.
|
|
|
|
|
Beyond that it carries:
|
|
|
|
|
|
|
|
|
|
- `siderolabs/intel-ucode` — no hypervisor supplies microcode here
|
|
|
|
|
- `siderolabs/i915` — the Alder Lake-P Iris Xe. Nothing requests `/dev/dri`
|
|
|
|
|
today; present so wanting hardware transcoding later is a config change
|
|
|
|
|
rather than another rolling reinstall
|
|
|
|
|
- `siderolabs/thunderbolt` — inert until `machine.kernel.modules` requests
|
|
|
|
|
`thunderbolt`/`thunderbolt_net`, present for the same reason. Note its udev
|
|
|
|
|
rule auto-authorises any Thunderbolt device attached once those modules load
|
|
|
|
|
- no `qemu-guest-agent`, which has nothing to talk to on bare metal
|
|
|
|
|
|
|
|
|
|
Verify the disks before going further. Talos enumerates NVMe in an order that
|
|
|
|
|
is not stable across boots, and the per-node configs select by serial for
|
|
|
|
|
exactly that reason — the failure mode is the installer taking the Ceph OSD
|
|
|
|
|
drive. `talosctl -n <maintenance-ip> --insecure get disks` should show three
|
|
|
|
|
NVMe devices whose serials match the `diskSelector` and `VolumeConfig` entries
|
|
|
|
|
in `talos/patches/k8s-N.yaml`.
|
|
|
|
|
|
|
|
|
|
## Step 2 — talos: form the cluster + get kubeconfig
|
|
|
|
|
|
|
|
|
|
**Do not use `kubernetes/bootstrap.sh talos`.** That phase cannot generate a
|
|
|
|
|
config: it passes `talos/patches/cilium-cni.yaml` whole, and that file's
|
|
|
|
|
`$patch: delete` document fails against a base with no `inlineManifests`
|
|
|
|
|
(#581). Its other phases — `flux`, `wait-apps` — are still used, in Steps 3, 5
|
|
|
|
|
and 6; only config generation is broken.
|
|
|
|
|
|
|
|
|
|
The procedure below is what actually works, and is the same one
|
|
|
|
|
`RUNBOOK-baremetal-conversion.md` uses.
|
|
|
|
|
|
|
|
|
|
### Generate the configs
|
|
|
|
|
|
|
|
|
|
This is a genuinely new cluster — nothing survives to copy secrets from, so
|
|
|
|
|
generate them fresh. (In a *partial* rebuild, where one node still runs, pull
|
|
|
|
|
secrets off it instead with `talosctl get mc v1alpha1` + `gen secrets
|
|
|
|
|
--from-controlplane-config`, or the new node forms its own cluster rather than
|
|
|
|
|
joining.)
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
kubernetes/bootstrap.sh talos
|
|
|
|
|
talosctl gen secrets -o secrets.yaml
|
|
|
|
|
|
|
|
|
|
for N in 1 2 3; do
|
|
|
|
|
talosctl gen config homelab https://k8s.renner.uno:6443 \
|
|
|
|
|
--with-secrets secrets.yaml --output-types controlplane \
|
|
|
|
|
--config-patch @talos/patches/cluster.yaml \
|
|
|
|
|
--config-patch @<cilium additive half> \
|
|
|
|
|
--config-patch @talos/patches/registry-mirror.yaml \
|
|
|
|
|
--config-patch @talos/patches/k8s-$N.yaml -o install-k8s-$N.yaml
|
|
|
|
|
done
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Generates + applies Talos configs to `k8s-{1,2,3}`, bootstraps etcd/Kubernetes,
|
|
|
|
|
writes `~/.talos/config` + `~/.kube/config`, and stages both into
|
|
|
|
|
`kubernetes/access-staging/` for later phases. Ends when all 3 nodes are `Ready`.
|
|
|
|
|
Two things the generator gets wrong for this repo, both of which will bite
|
|
|
|
|
silently:
|
|
|
|
|
|
|
|
|
|
- **`cilium-cni.yaml` cannot be passed whole** — extract only its additive
|
|
|
|
|
second document. See issue #581.
|
|
|
|
|
- **Strip the generated `HostnameConfig`** — it collides with
|
|
|
|
|
`machine.network.hostname` from the per-node patch.
|
|
|
|
|
|
|
|
|
|
Validate each before applying:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
talosctl validate --config install-k8s-$N.yaml --mode metal
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Apply in two stages
|
|
|
|
|
|
|
|
|
|
Do **not** apply the full per-node config in maintenance mode. A document that
|
|
|
|
|
references an interface which does not exist yet hangs the install: the node
|
|
|
|
|
sits at `STAGE: Installing` indefinitely with `KUBELET n/a` and `APISERVER n/a`,
|
|
|
|
|
and nothing times out. Concretely, the `ens19` VLAN document must be removed
|
|
|
|
|
from the first-stage config — its parent exists, but the install stalls anyway.
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
# Stage 1: minimal config, from the maintenance-mode address off the console
|
|
|
|
|
talosctl apply-config --insecure -n <maintenance-ip> --file install-k8s-$N-stage1.yaml
|
|
|
|
|
# Stage 2: once the node is up on management, apply the complete config
|
|
|
|
|
talosctl -n k8s-$N.srv.renner.uno apply-config --mode=try --timeout=5m --file install-k8s-$N.yaml
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`--mode=try` rolls back on its own if the node becomes unreachable, which is the
|
|
|
|
|
only safety net there is for a network change that can strand a machine with no
|
|
|
|
|
IPMI. Use it for every network-affecting apply, then make it permanent once
|
|
|
|
|
proven.
|
|
|
|
|
|
|
|
|
|
### Bootstrap etcd, then collect access
|
|
|
|
|
|
|
|
|
|
Bootstrap **exactly one** node:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
talosctl -n k8s-1.srv.renner.uno bootstrap
|
|
|
|
|
talosctl -n k8s-1.srv.renner.uno kubeconfig .
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Ends when all three nodes are `Ready`. `scripts/import-cluster-access.sh` is the
|
|
|
|
|
supported way to get the kubeconfig into place afterwards.
|
|
|
|
|
|
|
|
|
|
## Step 3 — flux: bootstrap GitOps **from the GitHub mirror** (break-glass)
|
|
|
|
|
|
|
|
|
@@ -231,9 +330,28 @@ it: repo → **Settings → Repository → Mirror Settings** in Gitea, or
|
|
|
|
|
in-cluster commits made **after** Flux/Gitea broke are not on the mirror
|
|
|
|
|
(Gitea couldn't push them if Gitea itself was down). This is the intended
|
|
|
|
|
recovery point, but be aware of it when reconciling drift.
|
|
|
|
|
- `bootstrap.sh` has no `openbao` phase — Step 4 is manual because the unseal
|
|
|
|
|
key must not live in CI. See issue #187 for the open question of whether to
|
|
|
|
|
wire a semi-automated OpenBao restore.
|
|
|
|
|
- **Booting is physical; the rest is not.** MS-01 has no IPMI and nothing here
|
|
|
|
|
does PXE, so someone has to put a USB stick in three machines. From there it
|
|
|
|
|
is remote — a maintenance-mode node DHCPs untagged and is reachable at that
|
|
|
|
|
address, which is how all three bare-metal conversions were driven.
|
|
|
|
|
#576 claims a freshly booted node "comes up unreachable and must be installed
|
|
|
|
|
from a console". That is not what happens and has never been observed here.
|
|
|
|
|
- **`bootstrap.sh talos` is dead** — it cannot generate a config (#581), and
|
|
|
|
|
Step 2 no longer uses it. Its `flux` and `wait-apps` phases are still used in
|
|
|
|
|
Steps 3, 5 and 6 and work; do not read this as the whole script being gone.
|
|
|
|
|
It has also never had an `openbao` phase — Step 4 is manual because the
|
|
|
|
|
unseal key must not live in CI; see issue #187 on whether to wire a
|
|
|
|
|
semi-automated OpenBao restore.
|
|
|
|
|
- **OpenBao seals on every restart and each replica must be unsealed
|
|
|
|
|
individually.** Three raft replicas mean the service survives losing one, but
|
|
|
|
|
a sealed server is neither a voter nor a standby — unsealing only one leaves
|
|
|
|
|
the cluster short of quorum. Step 4 covers this; it is repeated here because
|
|
|
|
|
it is the step most likely to be half-done under pressure.
|
|
|
|
|
- Ceph's cluster network is a Multus attachment on VLAN 49 with whereabouts
|
|
|
|
|
IPAM, not host networking. Nothing in the rebuild needs special handling for
|
|
|
|
|
it — the nodes hold no address on that segment — but a rebuilt cluster will
|
|
|
|
|
not have the whereabouts allocations from before, which is harmless since
|
|
|
|
|
they are assigned fresh.
|
|
|
|
|
- warn-and-continue waits in the `flux` phase can mask a genuinely broken
|
|
|
|
|
Ceph/infra bring-up; always read the phase output rather than trusting the
|
|
|
|
|
exit code.
|
|
|
|
|