Move to a three-node cluster with a Talos VIP; add Longhorn

Every node is now both control plane and worker, giving real tolerance
for losing any single machine (etcd quorum needs 3, not 1). A Talos VIP
gives the cluster one stable address regardless of which node answers.

Also pre-installs Longhorn for replicated storage, backed by a Talos
UserVolume on each node's second disk (>=900GB, non-system) rather than
the default path — mirroring the pattern from the homescale repo.

Docs renumbered to make room for 06-longhorn.md; checklist, glossary,
network map, and CLAUDE.md's guided-bootstrap instructions updated to
match, including asking the user for node IPs, VIP, and disk info before
generating any Talos config.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Maxwell Morris
2026-08-22 18:25:38 +01:00
co-authored by Claude Sonnet 5
parent 92c3ad3191
commit 110bd97cca
26 changed files with 777 additions and 261 deletions
+68 -44
View File
@@ -5,64 +5,81 @@ This is the detailed, copy-pasteable companion to
first for the explanations — this one is here so you have every exact
command in one place once you understand what they do.
Throughout, replace `<NODE_IP>` with your machine's real IP address. If it's
a VM, give it a DHCP reservation (a fixed IP tied to its MAC address) in your
router or hypervisor before you start, so the address never changes under
the cluster. Talos bakes the IP into certificates during setup, and changing
it later means starting over.
Throughout, replace `<NODE1_IP>`, `<NODE2_IP>`, `<NODE3_IP>` with your three
machines' real IP addresses, and `<VIP_IP>` with the shared address you
picked (with Claude's help) for the cluster's VIP — see
[`docs/02-talos-bootstrap.md`](../docs/02-talos-bootstrap.md#the-vip-one-address-for-a-cluster-that-moves).
Give each machine a DHCP reservation (a fixed IP tied to its MAC address)
in your router or hypervisor before you start, so none of these addresses
change under the cluster. Talos bakes them into certificates during setup,
and changing one later means starting over for that node.
Steps 1–2 below run **once, total**. Steps 3 runs **once per node** (three
times). Steps 4–6 run **once, total**, against whichever node you like
(or, once it exists, the VIP).
All commands assume you're running them from the root of this repo.
## 1. Generate machine configuration
```sh
talosctl gen config gitops-starter https://<NODE_IP>:6443 \
talosctl gen config gitops-starter https://<VIP_IP>:6443 \
--output-dir _out \
--config-patch-control-plane @talos/patch-controlplane.yaml
--config-patch-control-plane @talos/patch-controlplane.yaml \
--config-patch-control-plane @talos/patch-longhorn.yaml
```
Note the cluster endpoint here is the **VIP**, not any one node's address
— every node needs to agree in advance on where the cluster "lives," and
that address has to be the one that keeps working no matter which node is
currently answering.
This writes three files into `_out/` (already covered by this repo's
`.gitignore` — they contain private keys and must never be committed):
- `controlplane.yaml` — the full machine configuration for a control-plane
node, with our single-node patch baked in.
- `worker.yaml` — a worker machine configuration. Unused for now (single
node), kept for when you follow docs/08-growing-up.md.
- `controlplane.yaml` — the machine configuration for a control-plane
node, with both patches baked in. Applied identically to all three
nodes — there's no separate worker config in this setup, since every
node is both.
- `worker.yaml` — a plain worker config, generated automatically but
unused here since every node in this repo's setup is both control plane
and worker. Kept around for [`docs/09-growing-up.md`](../docs/09-growing-up.md),
where a fourth-and-beyond node uses exactly this file.
- `talosconfig` — *your* client credentials for talking to the Talos API on
this cluster (equivalent in spirit to a kubeconfig, but for Talos itself
rather than Kubernetes).
`gitops-starter` is the cluster name — an arbitrary label baked into the
generated certificates and config, used to tell clusters apart if you ever
manage more than one. `https://<NODE_IP>:6443` is the cluster's Kubernetes
API endpoint: port 6443 is the standard Kubernetes API server port, and this
same address gets written into the certificates and your future kubeconfig.
manage more than one. Port 6443 is the standard Kubernetes API server port.
## 2. Point talosctl at this cluster
```sh
export TALOSCONFIG=_out/talosconfig
talosctl config endpoint <NODE_IP>
talosctl config node <NODE_IP>
talosctl config endpoint <NODE1_IP> <NODE2_IP> <NODE3_IP>
```
`endpoint` is which machine talosctl *talks to*. `node` is which machine a
command *applies to*. On a single-node cluster they're the same address, but
Talos always keeps the two concepts separate, because on a multi-node
cluster you often want to ask one node about another.
`endpoint` is which machine(s) talosctl is willing to *talk to* — giving
it all three means it can still reach the cluster if any one of them is
down. You'll pass `--nodes` per-command below for which machine a given
command actually *applies to* — Talos always keeps those two concepts
separate.
## 3. Apply the configuration
## 3. Apply the configuration — once per node
Boot the target machine from the Talos ISO (see
[docs/01-prerequisites.md](../docs/01-prerequisites.md) for where to get it).
It boots into "maintenance mode" — alive enough to accept a configuration,
with nothing installed yet. Confirm it's reachable, then apply:
Boot each machine from your custom Talos ISO (see
[docs/01-prerequisites.md](../docs/01-prerequisites.md)). Each boots into
"maintenance mode" — alive enough to accept a configuration, with nothing
installed yet. For **each** node in turn:
```sh
talosctl get disks --insecure --nodes <NODE_IP>
```
Note the disk name you want Talos installed to (e.g. `/dev/sda`). Then:
Confirm you see two disks: the one Talos will install itself to, and the
≥900GB one `talos/patch-longhorn.yaml`'s `UserVolumeConfig` will claim for
Longhorn. Then:
```sh
talosctl apply-config --insecure \
@@ -73,32 +90,37 @@ talosctl apply-config --insecure \
`--insecure` is required for this one command only: the node has no
certificates yet, so talosctl can't authenticate to it normally. This is
the one moment during setup where you're trusting the network — do this on
a network you control. The node will reboot, install Talos to disk, and
come back up running the configuration you gave it — now *with*
certificates, so every command after this uses the secure path.
a network you control. The node will reboot, install Talos to its system
disk, format and mount its second disk for Longhorn, and come back up
running the configuration you gave it — now *with* certificates, so every
command after this uses the secure path.
## 4. Bootstrap etcd
Repeat this for `<NODE1_IP>`, `<NODE2_IP>`, and `<NODE3_IP>` before moving on.
Talos is installed and Kubernetes components are starting, but the cluster
has no data store yet. This one-time command initializes it:
## 4. Bootstrap etcd — once, total
Talos is installed on all three nodes and Kubernetes components are
starting, but the cluster has no data store yet. This one-time command
initializes it:
```sh
talosctl bootstrap --nodes <NODE_IP>
talosctl bootstrap --nodes <NODE1_IP>
```
Run this exactly once per cluster, against exactly one control-plane node,
even if you later add more. Running it again on an existing cluster can
corrupt it.
Run this **exactly once**, against **exactly one** of your three nodes —
even though all three will end up running etcd as peers, only one needs
telling to *start* the cluster; the other two discover and join the
existing one. Running it again against an existing cluster can corrupt it.
## 5. Get your kubeconfig
```sh
talosctl kubeconfig --nodes <NODE_IP> .
talosctl kubeconfig --nodes <VIP_IP> .
```
Downloads a kubeconfig into the current directory (also gitignored —
it grants full cluster access). Merge it with `~/.kube/config` or point
`KUBECONFIG` at it directly:
it grants full cluster access), pointed at the VIP rather than any one
node. Merge it with `~/.kube/config` or point `KUBECONFIG` at it directly:
```sh
export KUBECONFIG=$PWD/kubeconfig
@@ -110,14 +132,16 @@ export KUBECONFIG=$PWD/kubeconfig
kubectl get nodes
```
Expect to see one node, `Ready`, within a couple of minutes (it takes a
moment for the default CNI — Talos ships Flannel out of the box — to come
up and clear the node's networking checks).
Expect to see **three** nodes, `Ready`, within a couple of minutes (it
takes a moment for the default CNI — Talos ships Flannel out of the box —
to come up and clear each node's networking checks).
```
NAME STATUS ROLES AGE VERSION
talos-... Ready control-plane 90s v1.3x.x
talos-... Ready control-plane 85s v1.3x.x
talos-... Ready control-plane 80s v1.3x.x
```
If it's stuck on `NotReady`, give it another minute before troubleshooting —
first boot does a lot of one-time work.
If any node is stuck on `NotReady`, give it another minute before
troubleshooting — first boot does a lot of one-time work.