Compare commits

...
Author SHA1 Message Date
claude-botandClaude Opus 5 f51eb8bf35 docs(runbook): drop the claim that a cold rebuild needs console access
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
The rewrite earlier today stated, at the top and again in known gaps, that the
rebuild cannot be driven remotely: management is a tagged VLAN, a maintenance
mode node cannot configure VLANs, so it comes up unreachable and every machine
needs a keyboard.

That is wrong. A maintenance-mode node DHCPs untagged and is reachable at that
address — it is not on management, but it answers, and that is precisely the
address `apply-config --insecure` targets in Step 2. All three bare-metal
conversions were driven that way.

The claim came from #576 and was carried into this document without being
checked against what had actually happened days earlier in this repo. It is the
worst place to put an unverified constraint: a disaster-recovery runbook is read
when there is no capacity to discover that a stated prerequisite is fiction, and
"you need physical access to three machines" is exactly the kind of statement
that changes whether someone attempts a recovery at all.

What survives is narrower and true: MS-01 has no IPMI and nothing here does
PXE, so booting from USB is physical. Everything after the boot is remote.

Also removes the follow-on error from the first correction, which claimed the
switch port change on 2026-08-27 had fixed a reachability problem. There was no
reachability problem to fix. That change is real and useful — a maintenance mode
node now lands in management rather than the fallback network — but it did not
turn an impossible rebuild into a possible one.

#576 still carries the original claim and is flagged in known gaps as wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 11:25:50 +02:00
k.renner d1e072146f Merge pull request 'docs(runbook): rewrite the cold rebuild for bare metal' (#661) from runbook-cold-rebuild-baremetal into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#661
2026-08-27 08:11:24 +00:00
claude-botandClaude Opus 5 4678f73e9f docs(runbook): rewrite the cold rebuild for bare metal
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Step 1 was "tofu: provision the VMs" and there are no VMs. The OpenTofu stack
has no node definitions left — only the uninstantiated proxmox_vm module beside
DNS, object storage and UniFi — so `tofu apply` provisions nothing and Step 2
then has no cluster to form. The document was half-updated: it knew k8s-1 was
bare metal but still treated k8s-2 and k8s-3 as Proxmox VMs, which stopped
being true at the neptune conversion.

That matters more than an ordinary stale doc, because it is the document
someone reaches for when everything is gone — exactly when there is no capacity
to discover that step one is fiction.

Step 1 splits into two unrelated halves: a tofu apply for platform resources
that do still exist, and physically booting three machines from install media.

Step 2 stops calling `bootstrap.sh talos`, which cannot generate a config
(#581), and carries the procedure that actually works — the one
RUNBOOK-baremetal-conversion.md uses. Both traps it hides are now stated: the
whole cilium-cni.yaml cannot be passed as a patch, and the generated
HostnameConfig collides with the per-node hostname. So is the two-stage apply:
a config referencing an interface that does not exist yet hangs the install at
STAGE: Installing with nothing timing out.

Scoped that claim rather than repeating "bootstrap.sh is dead" wholesale — the
flux and wait-apps phases are still used in Steps 3, 5 and 6 and work. Only
config generation is broken, and saying otherwise would have left the runbook
contradicting itself three sections later.

Adds the constraint that decides whether this is even attemptable remotely, at
the top and again in known gaps: management is a tagged VLAN, a maintenance
mode node cannot configure VLANs, and MS-01 has no IPMI. Every node needs a
keyboard and a monitor until #576 puts VLAN 40 native on the I226-V port. That
was true before today and undocumented.

Also records what changed under the runbook recently: the schematic now carries
i915, Ceph's cluster network is a Multus attachment rather than host
networking, and OpenBao runs three replicas that each need unsealing
individually — a sealed server is neither a voter nor a standby, so doing one
leaves the cluster short of quorum.

Disk verification before install is now explicit. Talos enumerates NVMe in an
order that is not stable across boots, the per-node configs select by serial for
that reason, and the failure mode is the installer taking the Ceph OSD drive.

Closes #660.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 10:09:50 +02:00
k.renner 5f8c0115b5 Merge pull request 'fix(monitoring): raise VeleroBackupStale from 30h to 60h' (#658) from velero-stale-60h into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#658
2026-08-27 07:36:12 +00:00
claude-botandClaude Opus 5 e2f3d67f79 fix(monitoring): raise VeleroBackupStale from 30h to 60h
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
At 30h a single missed cycle pages critical, and a single missed cycle is the
normal cost of touching the nodes. Velero moves data through pods, so draining
a node kills whatever upload is in flight and the schedule is lost for that day
— it is not retried. Backup windows run 03:15 to 08:45 UTC, so any rolling node
work overlapping them produced a guaranteed critical alert per affected
schedule.

That is noise about planned work, not signal about backups. It fired for eleven
schedules during the bare-metal Talos upgrade and again today.

60h still catches what the rule exists for. Two consecutive failures is no
longer explicable by one maintenance window. Nothing is hidden in the meantime:
VeleroBackupFailed fires on the first failure regardless, so the first cycle is
still visible — only the escalation to critical waits for a second cycle to
confirm it.

Calibration against the current state: the four stale schedules sit at 46.8 to
48.5 hours since their last success, so the new threshold leaves roughly twelve
hours — exactly until today's runs. If those fail too, the alert fires, which
is the intended behaviour rather than a gap.

Verified: the rule passes a server-side dry run, the expression returns nothing
at 60h, and the same query at a lower bound returns the four affected schedules
with their ages — so the selector is known to match rather than merely silent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 09:35:09 +02:00
k.renner 7b0b664a46 Merge pull request 'chore(talos): move to v1.13.9' (#656) from talos-1-13-9 into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#656
2026-08-27 06:57:58 +00:00
claude-botandClaude Opus 5 3abdeb2203 chore(talos): move to v1.13.9
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Nodes run v1.12.11, which is the last 1.12 patch — v1.12.12 does not exist.
v1.13.9 is current.

The upgrade path is the documented one rather than a shortcut. Migration is
only tested between adjacent minor releases, and the recommendation is to go
to the latest patch of each intermediate minor. 1.12.11 -> 1.13.9 satisfies
both: one minor step, taken from the last patch of the version being left. No
intermediate hop is needed.

Kubernetes is not forced along. Talos 1.13's support matrix lists 1.36, 1.35,
1.34, 1.33, 1.32 and 1.31, and the cluster runs 1.35.2 — so it stays there
until `talosctl upgrade-k8s` moves it as a separate decision. This was the one
open question before the jump and it is answered from the matrix rather than
assumed.

Deprecations in 1.13 were checked against this cluster, not just read:

  .machine.env -> EnvironmentConfig          not used
  .machine.network.kubespan -> KubeSpanConfig not used
  EtcdConfigs/KubeletConfigs protobuf change  no consumer in this repo
  legacy upgrade API -> LifecycleService      still functional; `talosctl
                                              upgrade` keeps working, this
                                              only matters once it is removed

Both the five machine-config files here and the live config on k8s-1 were
checked for the first two, because the running config is generated from a base
this repo only patches and could carry fields the patches never mention.
Neither appears in either.

The removed interactive installer and the dropped `talosctl install` do not
apply — installation here is `talosctl upgrade --image` and Image Factory
media.

Schematic is unchanged; the factory image was verified to exist for the new
tag before committing. The cold-rebuild ISO URL moves with it.

Not applied to any node. Rolling it out is drain -> upgrade -> wait for Ceph
to return to 217 PGs active+clean, one node at a time, as in the 1.12.11 run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 08:56:20 +02:00
k.renner cdebaba2d3 Merge pull request 'chore(rook-ceph): return mon_data_avail_warn to the Ceph default' (#655) from ceph-mon-disk-warn-default into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#655
2026-08-27 06:40:38 +00:00
3 changed files with 183 additions and 42 deletions
+152 -34
View File
@@ -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.
@@ -40,17 +40,31 @@ spec:
description: "A Velero backup run for {{ $labels.schedule }} completed with Failed or PartiallyFailed status in the last hour. Inspect with `kubectl -n velero get backup.velero.io -l velero.io/schedule-name={{ $labels.schedule }}` and the velero pod logs."
- alert: VeleroBackupStale
# Daily schedule with no successful backup in over 30h: covers
# the case where multiple consecutive runs failed (Garage flapping
# over the backup window) without the per-event alerts being
# acted on.
# Daily schedule with no successful backup in over 60h: covers the
# case where consecutive runs failed without the per-event alerts
# being acted on.
#
# 60h, not 30h. At 30h a single missed cycle pages, and a single
# missed cycle is the normal cost of touching the nodes: Velero moves
# data through pods, so draining a node kills whatever upload is in
# flight and the schedule is simply lost for that day — it is not
# retried. Rolling node work between 03:15 and 08:45 UTC therefore
# produced a guaranteed critical alert for every schedule it
# overlapped, which is noise about planned work rather than a signal
# about backups.
#
# 60h still catches what this is for. Two consecutive failures is no
# longer explicable by one maintenance window, and the per-event
# VeleroBackupFailed alert fires on the first failure regardless — so
# nothing is hidden, only the escalation to critical is deferred
# until a second cycle confirms it.
expr: |
time() - velero_backup_last_successful_timestamp{schedule!=""} > 30 * 3600
time() - velero_backup_last_successful_timestamp{schedule!=""} > 60 * 3600
and
velero_backup_last_successful_timestamp{schedule!=""} > 0
for: 10m
labels:
severity: critical
annotations:
summary: "Velero {{ $labels.schedule }} has no successful backup in 30h+"
description: "Last successful backup for {{ $labels.schedule }} was over 30 hours ago. With a daily schedule that means at least one full cycle missed. RPO is degrading."
summary: "Velero {{ $labels.schedule }} has no successful backup in 60h+"
description: "Last successful backup for {{ $labels.schedule }} was over 60 hours ago. With a daily schedule that is at least two consecutive cycles missed, which one maintenance window does not explain. RPO is degrading."
+10 -1
View File
@@ -42,8 +42,17 @@ machine:
# Only read at install and upgrade time. Changing it here does nothing to a
# running node until that node is upgraded. Bump the tag with the Talos
# version — it must match the running version, or the upgrade is a downgrade.
#
# Upgrades follow the documented path: migration is only tested between
# adjacent minor releases, so go to the latest patch of each intermediate
# minor rather than skipping. 1.12.11 -> 1.13.9 satisfies that — one minor
# step from the last 1.12 patch, no intermediate needed.
#
# Kubernetes is a separate decision and is not forced by this. Talos 1.13
# supports Kubernetes 1.36, 1.35, 1.34, 1.33, 1.32 and 1.31, so the cluster
# stays on 1.35.2 until `talosctl upgrade-k8s` moves it deliberately.
install:
image: factory.talos.dev/metal-installer/0f6fb8bd8bb53013e9743b7c7b099d96fb9c32fd2464ced32693c2dc0ea5feb9:v1.12.11
image: factory.talos.dev/metal-installer/0f6fb8bd8bb53013e9743b7c7b099d96fb9c32fd2464ced32693c2dc0ea5feb9:v1.13.9
features:
rbac: true
stableHostname: true