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
+46 -33
View File
@@ -19,17 +19,20 @@ the real cluster match it. You stop making changes by hand. You edit a file,
push it, and a robot applies it for you — the same way every time, with a
record of every change in `git log`.
## The five pieces, in plain terms
## The six pieces, in plain terms
**Kubernetes** is software that runs on one or more machines and manages
containers for you: starts them, restarts them if they crash, and moves
them to a different machine if one dies (once you have more than one — see
[`docs/08-growing-up.md`](08-growing-up.md)). You describe the end state you
want ("run one copy of this"); Kubernetes' job is to make that true and keep
it true. This is what people mean by **declarative**: you state the
destination, not the steps to get there.
them to a different machine if one dies. You describe the end state you
want ("run one copy of this"); Kubernetes' job is to make that true and
keep it true. This is what people mean by **declarative**: you state the
destination, not the steps to get there. This repo runs three machines
from the start, specifically so that "moves them to a different machine
if one dies" is something you actually get, not just a fact about
Kubernetes in general — see
[`docs/02-talos-bootstrap.md`](02-talos-bootstrap.md) for why three.
**Talos Linux** is the operating system your machine runs. It's built for
**Talos Linux** is the operating system each machine runs. It's built for
exactly one purpose — running Kubernetes — and nothing else. There's no SSH,
no shell, no package manager to fiddle with by hand. You configure it by
sending it a config file over the network (a "machine config"), the same
@@ -47,15 +50,23 @@ manages. Rather than one Application resource pointing at everything, one
Application — the app-of-apps — watches a folder (`apps/`) full of *other*
Application definitions, each pointing at its own small piece. Add a new
app by adding a new file — see
[`docs/07-adding-new-apps.md`](07-adding-new-apps.md).
[`docs/08-adding-new-apps.md`](08-adding-new-apps.md).
**MetalLB** solves a specific problem: on a cloud provider, asking for a
public IP address for a service is one API call away; on your own bare
metal machine, nothing hands those out automatically. MetalLB runs inside
metal machines, nothing hands those out automatically. MetalLB runs inside
the cluster and fills that gap, so apps here can get a real, dedicated IP
address on your network instead of an awkward fixed port. It's pre-installed
in this repo, not a "next step" — see [`docs/05-metallb.md`](05-metallb.md).
**Longhorn** solves the equivalent problem for storage: a cloud provider
gives you durable, replicated disks on demand; bare metal just gives you
whatever's physically bolted into each machine. Longhorn turns a plain
disk on each of your three nodes into one pool of replicated storage any
app can request a slice of — and because it's replicated *across* nodes,
losing one machine doesn't lose the data either. Also pre-installed, not a
"next step" — see [`docs/06-longhorn.md`](06-longhorn.md).
## Architecture — the shape of what you'll build
```
@@ -64,9 +75,11 @@ in this repo, not a "next step" — see [`docs/05-metallb.md`](05-metallb.md).
│ repo, on GitHub) │
│ │
│ apps/demo.yaml │
│ apps/demo/*.yaml │──┐
│ apps/metallb.yaml │ │ watched by
│ apps/metallb/* │ │
│ apps/demo/*.yaml │
│ apps/metallb.yaml │──┐
│ apps/metallb/* │ │ watched by
│ apps/longhorn.yaml │ │
│ apps/longhorn/* │ │
└─────────────────────────┘ │
▼
┌───────────────────┐
@@ -76,29 +89,29 @@ in this repo, not a "next step" — see [`docs/05-metallb.md`](05-metallb.md).
└─────────┬──────────┘
│ applies manifests
▼
┌─────────────────────────────┐
│ Talos Linux node │
│ (control plane + worker, │
│ single node) │
│ │
│ ┌────────────┐ ┌──────────┐ │
│ │demo │ metallb- │ │
│ │namespace: │ system: │ │
│ │nginx Pod ⇄ │ hands out │ │
│ │Service │ a real IP │ │
│ │(LoadBalancer)◄─address to │ │
│ │ │ that Service│ │
│ └────────────┘ └──────────┘ │
└─────────────────────────────┘
┌───────────────────────────────────────────────┐
│ Three Talos Linux nodes (VIP: <VIP_IP>) │
│ each: control plane + worker + etcd peer │
│ │
│ demo namespace: metallb-system: │
│ nginx Pod ⇄ Service hands out a real IP │
│ (LoadBalancer) ◄───────address to that Service │
│ │
│ longhorn-system: each node's 2nd disk │
│ replicates data (≥900GB, UserVolume) │
│ across all 3 nodes ◄──mounted at │
│ /var/mnt/longhorn │
└───────────────────────────────────────────────┘
```
Everything below the Git repo is a *consequence* of what's in Git. If the
box labelled "Talos Linux node" burned to the ground and you rebuilt it from
scratch, replaying this repo against it would recreate everything except the
raw OS bootstrap (docs/02), the one-time Argo CD install (docs/03), and the
handful of network-specific addresses you choose in docs/05 — all
deliberately kept outside the loop, since something has to start it and
nothing in Git can know your LAN's IP layout for you.
Everything below the Git repo is a *consequence* of what's in Git. If any
one of the three node boxes burned to the ground and you replaced that
machine, replaying this repo against it would recreate everything on it
except the raw OS bootstrap (docs/02) and the one-time Argo CD install
(docs/03) — both deliberately kept outside the loop, since something has
to start it. The handful of network-specific addresses you choose along
the way (the VIP, the MetalLB pool) live in `docs/network.md`, not in Git
history alone, since nothing in Git can know your LAN's IP layout for you.
## The reconciliation loop
+56 -21
View File
@@ -5,31 +5,62 @@ PC, or an amd64 VM). If your hardware is arm64 (Raspberry Pi, Apple Silicon
VM), the tools below all publish arm64 builds too; swap the architecture in
whichever install command you use and the rest of this guide is unchanged.
## 1. A target machine
## 1. Three target machines
You need one machine to become your cluster node — a spare physical PC, a
mini PC, or a VM (VirtualBox, UTM, Proxmox, or any hypervisor that can boot
from an ISO). Minimum realistic spec: 2 CPU cores, 4GB RAM, 20GB disk.
This repo builds a **three-node cluster**, with every node acting as both
control plane (the brain: API server, scheduler, the cluster's own
database) and worker (runs your actual apps) at once. Three, not one,
because it's the smallest number where losing any single machine doesn't
take the whole cluster down — see
[`docs/00-what-is-all-this.md`](00-what-is-all-this.md) for why three
specifically, not two or four.
It needs to be reachable over the network from your everyday machine (the
one you'll run `talosctl` and `kubectl` from), and it needs a stable IP
address — see the note about DHCP reservations in
[`talos/README.md`](../talos/README.md).
You need three machines — spare physical PCs, mini PCs, or VMs (VirtualBox,
UTM, Proxmox, or any hypervisor that can boot from an ISO). Minimum
realistic spec per machine: 2 CPU cores, 4GB RAM, plus **two disks each**:
one for Talos itself (20GB+ is plenty), and a second, separate disk of
**at least 900GB** that will be handed entirely to Longhorn
(see [`docs/06-longhorn.md`](06-longhorn.md)) for storage. If your spare
hardware doesn't have a second disk that large, that's a real constraint —
ask Claude about adjusting the size threshold in
[`talos/patch-longhorn.yaml`](../talos/patch-longhorn.yaml) to fit what you
actually have.
You do **not** need to install an OS on it yet. That's what the next doc
covers — Talos gets installed by booting a special ISO and sending it
configuration over the network, not through a normal installer.
Download the Talos amd64 ISO from the
[Talos releases page](https://github.com/siderolabs/talos/releases) (look
for `metal-amd64.iso` under the latest release) and boot your target machine
from it. Leave it sitting at the boot screen — you'll configure it in
Each machine needs to be reachable over the network from your everyday
machine (the one you'll run `talosctl` and `kubectl` from), and each needs
its own stable IP address — see the note about DHCP reservations in
[`talos/README.md`](../talos/README.md). You'll also need one *additional*
unused address for the cluster's shared VIP (Virtual IP) — more on that in
[`docs/02-talos-bootstrap.md`](02-talos-bootstrap.md).
> **Ask Claude:** if you're not sure whether to use a VM or spare physical
You do **not** need to install an OS on any of them yet. That's what the
next doc covers — Talos gets installed by booting a special ISO and
sending it configuration over the network, not through a normal installer.
### Building your Talos image
Stock Talos doesn't include everything Longhorn needs — specifically, the
`iscsi-tools` and `util-linux-tools` **system extensions** (optional
add-ons baked into the boot image itself, since Talos has no package
manager to install them onto a running node). You build a custom image,
called a **schematic**, once, and use it for all three nodes:
1. Go to [factory.talos.dev](https://factory.talos.dev).
2. Under system extensions, select `siderolabs/iscsi-tools` and
`siderolabs/util-linux-tools`.
3. Pick the current stable Talos version and `metal-amd64` as the platform.
4. Download the resulting ISO (the site calls this a "schematic" —
your own custom Talos image, identified by a schematic ID).
Boot **all three** machines from this same ISO, and leave each sitting at
its boot screen — you'll configure them in
[`docs/02-talos-bootstrap.md`](02-talos-bootstrap.md).
> **Ask Claude:** if you're not sure whether to use VMs or spare physical
> hardware, ask — the tradeoffs (snapshots and easy resets vs. more
> realistic networking) are worth five minutes of discussion before you
> commit.
> realistic networking, and whether three VMs on one physical host really
> gives you the resilience three nodes are supposed to buy you) are worth
> five minutes of discussion before you commit.
## 2. talosctl
@@ -101,8 +132,12 @@ that references a repo URL means your fork's URL.
## Checklist
- [ ] Target machine chosen, network-reachable, IP address noted
- [ ] Talos ISO downloaded, machine booted from it and idle at the boot screen
- [ ] Three target machines chosen, each network-reachable, each with a
stable IP address noted, each with a second disk ≥900GB for Longhorn
- [ ] One additional unused IP address set aside for the cluster's VIP
- [ ] Custom Talos schematic built at factory.talos.dev with `iscsi-tools`
and `util-linux-tools`, ISO downloaded
- [ ] All three machines booted from that ISO, idle at the boot screen
- [ ] `talosctl version --client` works
- [ ] `kubectl version --client` works
- [ ] `argocd version --client` works
+83 -31
View File
@@ -1,7 +1,8 @@
# 02 — Bootstrapping Talos
At the end of this doc, you'll have a single-node Kubernetes cluster,
running nothing but Kubernetes itself. No Argo CD yet — that's next.
At the end of this doc, you'll have a three-node Kubernetes cluster,
running nothing but Kubernetes itself (plus the storage prep Longhorn
needs later). No Argo CD yet — that's next.
## Why Talos, specifically
@@ -17,44 +18,92 @@ same declarative way Kubernetes applies your app manifests. Less surface
area, less to secure, less to keep in sync by hand — the same philosophy as
the rest of this stack, one layer lower.
## Single node, on purpose
## Three nodes, each both control plane and worker
A "real" Kubernetes cluster usually has multiple nodes — some running the
**control plane** (the brain: API server, scheduler, the cluster's
database) and others as **workers** (running your actual apps), so that
losing one machine doesn't take the whole cluster down. This repo starts
with exactly one node acting as both, because that's the cheapest way to
learn the concepts that matter for GitOps without needing multiple
machines. [`docs/08-growing-up.md`](08-growing-up.md) covers adding more
nodes once you're comfortable — the config here is deliberately compatible
with that path, not a dead end.
A "real" Kubernetes cluster has multiple nodes — some running the
**control plane** (the brain: API server, scheduler, the cluster's own
database, called etcd) and others as **workers** (running your actual
apps) — so that losing one machine doesn't take the whole cluster down.
This repo starts with three nodes, each acting as *both* control plane and
worker, which gets you real resilience — survive losing any one machine —
without needing more than three.
Why three specifically, and not two: etcd (the control plane's database)
uses **quorum** — a majority vote among its members — to agree on cluster
state, and a majority only means something with an odd number of voters.
With one control-plane node, losing it loses the cluster outright. Two
doesn't help: losing either one leaves one survivor, which is not a
majority of two. Three is the smallest number where losing any *one*
still leaves two — a majority — able to keep agreeing on state. This is
also why [`docs/09-growing-up.md`](09-growing-up.md) — once you're past
this doc — specifically warns against going to four control-plane nodes
later: four doesn't improve on three's fault tolerance, it just makes
quorum math worse.
## The VIP: one address for a cluster that moves
With three control-plane nodes, "where's the API server?" no longer has
one fixed answer — it's whichever nodes happen to be up right now. Talos's
**VIP** (Virtual IP) feature solves this: the three nodes continuously
elect one of themselves to answer on one shared IP address you choose, and
re-elect within seconds if that node goes down. Everything that talks to
this cluster — `talosctl`, `kubectl`, the Application manifests in this
repo — points at the VIP, never at one node's own address. See the
`vip` block in
[`talos/patch-controlplane.yaml`](../talos/patch-controlplane.yaml).
> **Ask Claude:** "help me pick the VIP address for my cluster" before you
> generate any config — same kind of decision as the MetalLB pool in
> [`docs/05-metallb.md`](05-metallb.md): a real, unused address on your
> LAN, outside your DHCP range, that Claude can't guess on its own.
## Preparing storage for Longhorn
Each node also gets a second disk reserved for
[Longhorn](06-longhorn.md), this repo's pre-installed storage layer —
handled by [`talos/patch-longhorn.yaml`](../talos/patch-longhorn.yaml),
applied at the same time as the control-plane patch below. Read that
file's comments; the short version is that Talos finds, formats, and
mounts a disk for Longhorn on its own, using rules you set (not the system
disk, at least 900GB), so there's no manual partitioning step here.
## Steps
Full exact commands live in [`talos/README.md`](../talos/README.md) — read
that alongside this doc; this section explains what's happening at each
stage, that one has copy-paste blocks.
stage, that one has copy-paste blocks. You'll repeat most of these steps
**three times**, once per node — `talosctl README.md` calls out exactly
which commands are "once" vs. "once per node."
1. **Generate machine configuration.** `talosctl gen config` produces
Talos's config files for your cluster, including
[`talos/patch-controlplane.yaml`](../talos/patch-controlplane.yaml) —
read that file, it's short and explains itself — which lets your one
node run workloads as well as the control plane.
Talos's config files for your cluster, with both
[`talos/patch-controlplane.yaml`](../talos/patch-controlplane.yaml)
(scheduling + the VIP) and
[`talos/patch-longhorn.yaml`](../talos/patch-longhorn.yaml) (storage
prep) baked in — read both files, they explain themselves. This
produces one `controlplane.yaml`, applied identically to all three
nodes; there's no separate worker config in this setup, since every
node is both.
2. **Apply the config to your node.** The node, freshly booted from the
Talos ISO, is sitting in "maintenance mode" with no configuration.
`talosctl apply-config` sends it the config generated in step 1. The
node reboots, installs Talos to its disk, and comes up running for real.
2. **Apply the config to each node.** Each node, freshly booted from your
custom Talos ISO, is sitting in "maintenance mode" with no
configuration. `talosctl apply-config` sends it the config from step 1.
Run this once per node, pointed at that node's own address. Each node
reboots, installs Talos to its system disk, formats and mounts its
second disk for Longhorn, and comes up running for real.
3. **Bootstrap etcd.** `talosctl bootstrap` initializes the cluster's data
store. This is a one-time, one-node-ever operation — even in a
multi-node cluster you only ever bootstrap once.
store. This is a one-time, one-node-ever operation, run against exactly
one of your three nodes — even though all three will end up running
etcd, only one of them needs telling to *start* the cluster; the other
two join the existing one once it exists.
4. **Retrieve your kubeconfig.** `talosctl kubeconfig` downloads the
credentials you'll use to talk to Kubernetes with `kubectl` — the
Kubernetes equivalent of the `talosconfig` you got in step 1.
Kubernetes equivalent of the `talosconfig` you got in step 1. Point it
at the VIP, not at any one node.
5. **Verify.** `kubectl get nodes` should show one node, `Ready`.
5. **Verify.** `kubectl get nodes` should show all three nodes, `Ready`.
> **Ask Claude:** "what's the difference between a kubeconfig and a
> talosconfig, and why are they separate files?" is a good one to ask once
@@ -62,11 +111,14 @@ stage, that one has copy-paste blocks.
## Checklist
- [ ] `_out/controlplane.yaml`, `worker.yaml`, and `talosconfig` generated
- [ ] Config applied to the node, node has rebooted and installed Talos
- [ ] `talosctl bootstrap` run exactly once
- [ ] kubeconfig retrieved
- [ ] `kubectl get nodes` shows one `Ready` node
- [ ] VIP address chosen, with Claude's help, for your LAN
- [ ] `_out/controlplane.yaml` and `talosconfig` generated, with both
patches applied
- [ ] Config applied to **all three** nodes; each has rebooted, installed
Talos, and mounted its Longhorn disk
- [ ] `talosctl bootstrap` run exactly once, against exactly one node
- [ ] kubeconfig retrieved, pointed at the VIP
- [ ] `kubectl get nodes` shows all three nodes `Ready`
Once your node is `Ready`, continue to
Once all three nodes are `Ready`, continue to
[`docs/03-installing-argocd.md`](03-installing-argocd.md).
+1 -1
View File
@@ -46,7 +46,7 @@ each stage does:
> **Ask Claude:** "why is a port-forward good enough for now, and what
> would I need instead to reach this Argo CD from somewhere other than my
> own machine?" — a preview of the Ingress topic in
> [`docs/08-growing-up.md`](08-growing-up.md).
> [`docs/09-growing-up.md`](09-growing-up.md).
## Checklist
+41 -36
View File
@@ -22,30 +22,33 @@ new app.
argocd/app-of-apps/app-of-apps.yaml (you apply this ONE file, by hand, once)
│
│ watches apps/, non-recursively
├──────────────┬───────────────┐
▼ ▼ ▼
apps/demo.yaml apps/metallb.yaml (more, later — just add a file)
│ │
▼ ▼
apps/demo/*.yaml apps/metallb/* (the actual manifests / Helm chart)
├───────────────┬────────────────┬───────────────┐
▼ ▼ ▼ ▼
apps/demo.yaml apps/metallb.yaml apps/longhorn.yaml (more, later)
│ │ │
▼ ▼ ▼
apps/demo/*.yaml apps/metallb/* apps/longhorn/* (manifests / Helm charts)
```
Three layers, each watching the next. You only ever touch the bottom two —
the app-of-apps is set-and-forget. Notice `apps/metallb.yaml` is already
here — see [`docs/05-metallb.md`](05-metallb.md) right after this one.
Four layers wide, each watching the next. You only ever touch the bottom
two — the app-of-apps is set-and-forget. Notice `apps/metallb.yaml` and
`apps/longhorn.yaml` are already here — see
[`docs/05-metallb.md`](05-metallb.md) and
[`docs/06-longhorn.md`](06-longhorn.md) right after this one.
## Why `apps/` isn't recursive
Look at [`argocd/app-of-apps/app-of-apps.yaml`](../argocd/app-of-apps/app-of-apps.yaml)
— specifically `directory.recurse: false`. The `apps/` folder contains both
Application manifests (`apps/demo.yaml`, `apps/metallb.yaml`) *and*
subfolders holding the real workload manifests those Applications point to
(`apps/demo/*.yaml`, `apps/metallb/*`). Non-recursive means the
app-of-apps only ever looks at files directly inside `apps/`, never inside
its subfolders — so it only ever sees Application definitions, and leaves
the actual workload manifests to the Application that owns them. Without
this, the app-of-apps would try to apply raw Deployments and Helm chart
files itself, duplicating and conflicting with what those Applications are
Application manifests (`apps/demo.yaml`, `apps/metallb.yaml`,
`apps/longhorn.yaml`) *and* subfolders holding the real workload manifests
those Applications point to (`apps/demo/*.yaml`, `apps/metallb/*`,
`apps/longhorn/*`). Non-recursive means the app-of-apps only ever looks at
files directly inside `apps/`, never inside its subfolders — so it only
ever sees Application definitions, and leaves the actual workload
manifests to the Application that owns them. Without this, the
app-of-apps would try to apply raw Deployments and Helm chart files
itself, duplicating and conflicting with what those Applications are
already doing.
## Every Application needs its own `repoURL`
@@ -59,19 +62,19 @@ that has it, not just the one you apply by hand:
grep -rl YOUR_GITHUB_USERNAME .
```
You should see three files: `argocd/app-of-apps/app-of-apps.yaml`,
`apps/demo.yaml`, and `apps/metallb.yaml`. Edit `spec.source.repoURL` in
each to point at your fork.
You should see four files: `argocd/app-of-apps/app-of-apps.yaml`,
`apps/demo.yaml`, `apps/metallb.yaml`, and `apps/longhorn.yaml`. Edit
`spec.source.repoURL` in each to point at your fork.
## Steps
1. Fix `repoURL` in all three files (above).
1. Fix `repoURL` in all four files (above).
2. Commit and push — this part matters more than it looks: the
app-of-apps reads `apps/demo.yaml` and `apps/metallb.yaml` *from Git*,
not from your local edits, so they have to be pushed before you apply
anything.
app-of-apps reads `apps/demo.yaml`, `apps/metallb.yaml`, and
`apps/longhorn.yaml` *from Git*, not from your local edits, so they
have to be pushed before you apply anything.
```sh
git add argocd/app-of-apps/app-of-apps.yaml apps/demo.yaml apps/metallb.yaml
git add argocd/app-of-apps/app-of-apps.yaml apps/demo.yaml apps/metallb.yaml apps/longhorn.yaml
git commit -m "Point Applications at my fork"
git push
```
@@ -79,13 +82,15 @@ each to point at your fork.
```sh
kubectl apply -f argocd/app-of-apps/app-of-apps.yaml
```
4. Watch it sync, then watch `demo` and `metallb` appear underneath it:
4. Watch it sync, then watch `demo`, `metallb`, and `longhorn` appear
underneath it:
```sh
argocd app list
```
You should see `app-of-apps`, `demo`, and `metallb`, all `Synced` and
`Healthy` within a minute or two (MetalLB's own install takes a little
longer than the demo app).
You should see `app-of-apps`, `demo`, `metallb`, and `longhorn`, all
`Synced` and `Healthy` within a few minutes (MetalLB and Longhorn's own
installs take longer than the demo app — Longhorn especially, since it
has to find and prepare a disk on every node).
5. Confirm the demo app is actually running:
```sh
kubectl -n demo get pods,svc
@@ -95,23 +100,23 @@ each to point at your fork.
hands those out real addresses on bare metal until MetalLB is
configured, which is exactly the next doc.
If `app-of-apps`, `demo`, and `metallb` all show `Synced`/`Healthy`, the
loop — Git → Argo CD → cluster — is already proven end to end; you just
can't see the demo app in a browser yet.
If `app-of-apps`, `demo`, `metallb`, and `longhorn` all show
`Synced`/`Healthy`, the loop — Git → Argo CD → cluster — is already proven
end to end; you just can't see the demo app in a browser yet.
> **Ask Claude:** "walk me through exactly what happened, in order, from the
> moment I ran `kubectl apply -f argocd/app-of-apps/app-of-apps.yaml` to
> these three Applications existing." Making sure you can narrate that
> these four Applications existing." Making sure you can narrate that
> chain yourself is the real checkpoint for this doc — everything from
> here on is a variation on it.
## Checklist
- [ ] `repoURL` fixed in all three files listed above
- [ ] `repoURL` fixed in all four files listed above
- [ ] Fix committed and pushed
- [ ] `app-of-apps` applied and shows `Synced`/`Healthy`
- [ ] `demo` and `metallb` Applications appeared automatically, both
`Synced`/`Healthy`
- [ ] `demo`, `metallb`, and `longhorn` Applications appeared
automatically, all `Synced`/`Healthy`
- [ ] `kubectl -n demo get pods,svc` shows the demo app's Pod running
(Service `EXTERNAL-IP` still `<pending>` — expected for now)
+2 -2
View File
@@ -34,7 +34,7 @@ Helm chart that does two things at once (see
- Pulls in the official upstream MetalLB chart as a dependency — this is
also the real, working example of a Helm-based app mentioned in
[`docs/07-adding-new-apps.md`](07-adding-new-apps.md).
[`docs/08-adding-new-apps.md`](08-adding-new-apps.md).
- Carries two templates of its own
([`apps/metallb/templates/`](../apps/metallb/templates)): an
`IPAddressPool` and an `L2Advertisement` — MetalLB's own custom
@@ -152,4 +152,4 @@ the only place this address gets recorded at all.
- [ ] `kubectl -n demo get svc demo` shows a real `EXTERNAL-IP`, page loads in a browser
- [ ] `argocd-server` patched to `LoadBalancer`, reachable at its own address without port-forwarding
Continue to [`docs/06-your-first-change.md`](06-your-first-change.md).
Continue to [`docs/06-longhorn.md`](06-longhorn.md).
+100
View File
@@ -0,0 +1,100 @@
# 06 — Longhorn: real, replicated storage
The app-of-apps you applied in [`docs/04-app-of-apps.md`](04-app-of-apps.md)
already brought up `longhorn` alongside `demo` and `metallb`. This doc
explains what it does and how to actually use it — there's no address to
pick this time, unlike MetalLB; the interesting decisions already happened
back in [`docs/02-talos-bootstrap.md`](02-talos-bootstrap.md).
## Why this is needed at all
A Kubernetes Pod's own filesystem disappears the moment that Pod does —
fine for a stateless web server, useless for anything that needs to keep
data around (a database, uploaded files, anything you'd be upset to lose).
Kubernetes' answer is a **PersistentVolumeClaim (PVC)**: an app asks for
some amount of storage, and something has to actually provide it. On a
cloud provider, that something is the cloud's own disk service. On bare
metal — your three Talos nodes — nothing provides that on its own, the
same gap MetalLB fills for IP addresses rather than storage.
**Longhorn** is that provider: it turns a plain disk on each node into one
pool of storage, and — because your cluster has three nodes — it
replicates each volume across more than one of them. Lose a node, and the
data backing any volume that had a replica elsewhere is still there. This
is also why Longhorn is genuinely a **three-node** story: with one node,
"replicated storage" has nothing to replicate *to*.
## What's already running
**`apps/longhorn.yaml`** → one Application, pointing at `apps/longhorn`, a
Helm chart that pulls in the official upstream Longhorn chart (see
[`apps/longhorn/Chart.yaml`](../apps/longhorn/Chart.yaml)) — the same
umbrella-chart pattern as MetalLB, just without any extra templates of its
own this time.
The interesting configuration already happened at the Talos layer, back in
[`talos/patch-longhorn.yaml`](../talos/patch-longhorn.yaml): each node
found, formatted, and mounted its own ≥900GB disk at
`/var/mnt/longhorn`. [`apps/longhorn/values.yaml`](../apps/longhorn/values.yaml)
points Longhorn's `defaultDataPath` at that exact path — this is the thing
that makes Longhorn use *that* disk instead of carving space out of each
node's root filesystem (small, and on Talos, not meant for this kind of
data anyway).
## Verify it's actually working
```sh
kubectl -n longhorn-system get pods
```
Expect a `longhorn-manager` and `longhorn-csi-plugin` Pod on each of your
three nodes, plus one `instance-manager` per node once a volume exists,
all `Running`.
```sh
kubectl get storageclass
```
Longhorn's chart creates a default `StorageClass` called `longhorn`
automatically — this is what makes it the thing Kubernetes reaches for
whenever an app asks for storage without naming one specifically.
## Try it: give the demo app real storage
The demo app's HTML currently lives in a ConfigMap (see
[`docs/07-your-first-change.md`](07-your-first-change.md)) — fine for a
few lines of text, but not how a real app would store real data. As a
hands-on check that Longhorn actually works, ask Claude to help you:
1. Add a small `PersistentVolumeClaim` to `apps/demo/`, requesting a
couple of gigabytes with no `storageClassName` set (so it uses
Longhorn's default).
2. Mount it into the demo Deployment somewhere harmless, like
`/usr/share/nginx/html/data`.
3. Push it, let Argo CD sync, and confirm:
```sh
kubectl -n demo get pvc
```
should show your claim `Bound`, and
```sh
kubectl -n longhorn-system get volumes
```
should show a matching volume, `healthy`, with more than one replica.
This isn't part of the checklist below — treat it as an optional exercise
once the pre-installed pieces are confirmed working, not a requirement to
move on.
> **Ask Claude:** "what would happen to that PVC's data if I lost one of
> my three nodes right now?" — worth being able to answer before you trust
> this with anything real.
## Checklist
- [ ] `kubectl -n longhorn-system get pods` shows manager/CSI Pods
`Running` on all three nodes
- [ ] `kubectl get storageclass` shows a default `longhorn` StorageClass
- [ ] (Optional) Gave the demo app a real PVC and confirmed it's `Bound`
with a healthy, replicated Longhorn volume behind it
Continue to [`docs/07-your-first-change.md`](07-your-first-change.md).
@@ -1,4 +1,4 @@
# 06 — Your first change
# 07 — Your first change
Everything so far proved Argo CD can deploy what's in Git. This doc proves
the part that actually matters day to day: changing what's in Git and
@@ -90,5 +90,5 @@ for the loop that just ran.
- [ ] Saw the new text in the browser
- [ ] (Optional) Watched self-healing revert a hand-made change
Continue to [`docs/07-adding-new-apps.md`](07-adding-new-apps.md) to add
Continue to [`docs/08-adding-new-apps.md`](08-adding-new-apps.md) to add
something of your own.
@@ -1,4 +1,4 @@
# 07 — Adding a new app
# 08 — Adding a new app
This is the payoff for the app-of-apps structure: adding something new is
"drop a file here," not a special procedure.
@@ -32,8 +32,13 @@ To add a new app called, say, `whoami`:
`whoami.yaml`, creates the `whoami` Application, which in turn deploys
everything in `apps/whoami/`.
4. Confirm: `argocd app list` should show `whoami` alongside `demo` and
`metallb`.
4. Confirm: `argocd app list` should show `whoami` alongside `demo`,
`metallb`, and `longhorn`.
If your new app needs storage, request it with a plain
`PersistentVolumeClaim` and no `storageClassName` — Longhorn's chart set
itself as the cluster's default StorageClass, so it's used automatically.
See [`docs/06-longhorn.md`](06-longhorn.md).
## Manifest-based vs. Helm-based apps
@@ -83,5 +88,5 @@ address shows up in it.
- [ ] `argocd app list` shows the new app, `Synced`/`Healthy`
- [ ] If it has a `LoadBalancer` Service, its address is recorded in `docs/network.md`
Continue to [`docs/08-growing-up.md`](08-growing-up.md) for what to add
Continue to [`docs/09-growing-up.md`](09-growing-up.md) for what to add
next as this cluster grows beyond a learning setup.
+36 -19
View File
@@ -1,4 +1,4 @@
# 08 — Growing up: what to add next
# 09 — Growing up: what to add next
Everything up to here is deliberately minimal — enough to understand the
whole loop, nothing more. This doc is a map of the natural next steps, each
@@ -7,26 +7,43 @@ for you; each is its own project once you're ready.
## More nodes
Right now, one machine is both control plane and worker
(`allowSchedulingOnControlPlanes: true` in
[`talos/patch-controlplane.yaml`](../talos/patch-controlplane.yaml)). That's
a single point of failure: if that machine dies, the cluster is gone.
Your three nodes already give you real resilience — losing any one still
leaves a majority of control-plane voters (see
[`docs/02-talos-bootstrap.md`](02-talos-bootstrap.md#three-nodes-each-both-control-plane-and-worker))
and, once Longhorn has real volumes, a replica of your data likely
survives too. Growing further is about *capacity*, not fault tolerance you
don't already have — so beyond this point, add plain **worker** nodes, not
more control-plane ones:
To grow beyond it:
1. Boot a second machine from the Talos ISO, same as the first.
2. Generate a **worker** config instead of a control-plane one — you
1. Boot a new machine from the same custom Talos ISO as the first three
(see [`docs/01-prerequisites.md`](01-prerequisites.md)).
2. Apply the **worker** config instead of the control-plane one — you
already have `_out/worker.yaml` from
[`docs/02-talos-bootstrap.md`](02-talos-bootstrap.md), unused until now.
3. `talosctl apply-config --insecure --nodes <NEW_NODE_IP> --file _out/worker.yaml`
4. `kubectl get nodes` should show two nodes shortly after.
[`docs/02-talos-bootstrap.md`](02-talos-bootstrap.md), unused until now:
```sh
talosctl apply-config --insecure --nodes <NEW_NODE_IP> --file _out/worker.yaml
```
3. `kubectl get nodes` should show a fourth node shortly after, without
the `control-plane` role.
For real resilience against a control-plane node failing (not just adding
worker capacity), you'd want an odd number of control-plane nodes — 3 is
the normal minimum — because etcd (the cluster's data store) uses majority
voting to agree on state, and majority voting needs an odd number to avoid
ties. That's a bigger step than this doc covers in detail; ask Claude when
you're actually ready to take it.
### Why not a fourth control-plane node
It's tempting to reach for a fourth control-plane node when you're growing
a cluster you already made resilient with three — but it actually makes
things worse, not better. etcd (the control-plane database) needs a
**majority** of its members to agree before it can do anything, and
majority only means something clean with an odd number: with 3 members, 1
can fail and 2 (a majority) still agree — you can lose one. With 4, you
still need 3 to agree, so you can *still* only lose one — you paid for an
extra machine and gained nothing. Worse, 4 is *more* likely to have
exactly a 2-2 split during a network hiccup, which stalls the cluster
until it's resolved. If you ever do want more control-plane fault
tolerance than three nodes gives you, the next meaningful number is 5 —
not 4.
> **Ask Claude:** "walk me through the quorum math for 3 vs. 4 vs. 5
> control-plane nodes" if you want to see this worked out concretely
> rather than taking it on faith.
## Ingress
@@ -40,7 +57,7 @@ so big, and there's no way to route by hostname (`app1.example.com` vs
An **Ingress controller** is itself just another app you'd deploy the same
way as anything else in this repo (see
[`docs/07-adding-new-apps.md`](07-adding-new-apps.md)) — commonly as a
[`docs/08-adding-new-apps.md`](08-adding-new-apps.md)) — commonly as a
Helm-based app, since most ingress controllers are distributed that way,
same as MetalLB. It typically sits behind one single MetalLB
`LoadBalancer` Service (one IP, total, for every app), and you add an
+37 -21
View File
@@ -1,25 +1,27 @@
# Guided bootstrap checklist
A single-page, ordered checklist covering the entire path from bare
machine to a working GitOps loop. It's meant to be worked through **with
machines to a working GitOps loop. It's meant to be worked through **with
Claude**, one item at a time — ask Claude to walk you through this list,
and it will run each step with you, wait for your output, confirm it
worked, and only then move to the next one. See `CLAUDE.md` at the repo
root for how Claude is primed to do this.
Each item names the doc it's drawn from, in case you want the full
explanation before or after running it. Replace `<NODE_IP>`,
`YOUR_GITHUB_USERNAME`, and the MetalLB IP addresses with your real values
throughout — Claude will remind you if you paste a command still
containing a placeholder, and will ask you for the IP addresses directly
in Phase 5 rather than guessing them.
explanation before or after running it. Replace `<NODE1_IP>`/`<NODE2_IP>`/`<NODE3_IP>`,
`<VIP_IP>`, `YOUR_GITHUB_USERNAME`, and the MetalLB IP addresses with your
real values throughout — Claude will remind you if you paste a command
still containing a placeholder, and will ask you for all of these directly
in Phases 2 and 5 rather than guessing them.
## Phase 1 — Prerequisites ([`01`](01-prerequisites.md))
- [ ] Target machine chosen, reachable on the network, with a stable
(ideally DHCP-reserved) IP address
- [ ] Talos amd64 ISO downloaded, machine booted from it, sitting at the
boot screen
- [ ] Three target machines chosen, each network-reachable, each with a
stable (ideally DHCP-reserved) IP address, each with a second disk
≥900GB set aside for Longhorn
- [ ] Custom Talos schematic built at factory.talos.dev with `iscsi-tools`
and `util-linux-tools`, ISO downloaded
- [ ] All three machines booted from that ISO, sitting at the boot screen
- [ ] `talosctl version --client` runs successfully
- [ ] `kubectl version --client` runs successfully
- [ ] `argocd version --client` runs successfully
@@ -27,12 +29,16 @@ in Phase 5 rather than guessing them.
## Phase 2 — Talos bootstrap ([`02`](02-talos-bootstrap.md), [`talos/README.md`](../talos/README.md))
- [ ] `talosctl gen config gitops-starter https://<NODE_IP>:6443 --output-dir _out --config-patch-control-plane @talos/patch-controlplane.yaml` run — `_out/controlplane.yaml`, `worker.yaml`, `talosconfig` exist
- [ ] **Ask Claude** to help pick: your three nodes' IP addresses (or
confirm DHCP reservations) and a separate VIP address — all based on
your actual LAN/DHCP setup
- [ ] `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-longhorn.yaml` run — `_out/controlplane.yaml`, `worker.yaml`, `talosconfig` exist
- [ ] `TALOSCONFIG` exported (or `talosctl config endpoint`/`node` set) to `_out/talosconfig`
- [ ] `talosctl apply-config --insecure --nodes <NODE_IP> --file _out/controlplane.yaml` run, node rebooted
- [ ] `talosctl bootstrap --nodes <NODE_IP>` run — **exactly once**
- [ ] `talosctl kubeconfig --nodes <NODE_IP> .` run, `KUBECONFIG` pointed at the result
- [ ] `kubectl get nodes` shows one node, `Ready`
- [ ] For **each** of the three nodes: `talosctl get disks --insecure --nodes <NODE_IP>` confirms two disks, then `talosctl apply-config --insecure --nodes <NODE_IP> --file _out/controlplane.yaml` run, node rebooted
- [ ] `talosctl bootstrap --nodes <NODE1_IP>` run — **exactly once**, against **exactly one** node
- [ ] `talosctl kubeconfig --nodes <VIP_IP> .` run, `KUBECONFIG` pointed at the result
- [ ] `kubectl get nodes` shows **all three** nodes, `Ready`
- [ ] `docs/network.md` updated with all three node addresses and the VIP
## Phase 3 — Argo CD install ([`03`](03-installing-argocd.md), [`argocd/README.md`](../argocd/README.md))
@@ -45,13 +51,15 @@ in Phase 5 rather than guessing them.
## Phase 4 — Hand the cluster to Git ([`04`](04-app-of-apps.md))
- [ ] `spec.source.repoURL` fixed in all three placeholder files: run
- [ ] `spec.source.repoURL` fixed in all four placeholder files: run
`grep -rl YOUR_GITHUB_USERNAME .` to find them
(`argocd/app-of-apps/app-of-apps.yaml`, `apps/demo.yaml`, `apps/metallb.yaml`)
(`argocd/app-of-apps/app-of-apps.yaml`, `apps/demo.yaml`,
`apps/metallb.yaml`, `apps/longhorn.yaml`)
- [ ] Edit committed and pushed to your fork's `main` branch
- [ ] `kubectl apply -f argocd/app-of-apps/app-of-apps.yaml`
- [ ] `argocd app list` shows `app-of-apps`, `Synced`/`Healthy`
- [ ] `argocd app list` shows `demo` and `metallb`, appeared automatically, both `Synced`/`Healthy`
- [ ] `argocd app list` shows `demo`, `metallb`, and `longhorn`, appeared
automatically, all `Synced`/`Healthy`
- [ ] `kubectl -n demo get pods,svc` shows the demo app's Pod running
(Service `EXTERNAL-IP` still `<pending>` — expected for now)
@@ -68,7 +76,15 @@ in Phase 5 rather than guessing them.
- [ ] `argocd-server` Service patched to `LoadBalancer` with your chosen Argo CD address (`kubectl -n argocd patch svc argocd-server ...`)
- [ ] Argo CD UI reachable at `https://<that-address>` with no port-forward running
## Phase 6 — Prove the loop ([`06`](06-your-first-change.md))
## Phase 6 — Longhorn: real storage ([`06`](06-longhorn.md))
- [ ] `kubectl -n longhorn-system get pods` shows manager/CSI Pods
`Running` on all three nodes
- [ ] `kubectl get storageclass` shows a default `longhorn` StorageClass
- [ ] (Optional) Gave the demo app a real PVC and confirmed it's `Bound`
with a healthy, replicated Longhorn volume behind it
## Phase 7 — Prove the loop ([`07`](07-your-first-change.md))
- [ ] Edited the `<h1>` text in `apps/demo/configmap.yaml`
- [ ] Committed and pushed
@@ -85,6 +101,6 @@ push" and the page updating. If you can't yet, that's the sign to ask
Claude for a walkthrough of whichever phase felt shaky before calling this
finished.
From here: [`07-adding-new-apps.md`](07-adding-new-apps.md) to add
something of your own, or [`08-growing-up.md`](08-growing-up.md) for the
From here: [`08-adding-new-apps.md`](08-adding-new-apps.md) to add
something of your own, or [`09-growing-up.md`](09-growing-up.md) for the
bigger next steps (more nodes, ingress, secrets management).
+61 -7
View File
@@ -29,7 +29,7 @@ steps — installing Argo CD, applying the app-of-apps — that get the
GitOps loop started before Git takes over.
**cluster** — A group of one or more machines (nodes) working together as
one Kubernetes system. This repo's cluster starts as a single node.
one Kubernetes system. This repo's cluster starts as three nodes.
**CNI (Container Network Interface)** — The plugin responsible for giving
Pods network connectivity and IP addresses. Talos ships Flannel, a CNI, by
@@ -42,9 +42,9 @@ demo app's HTML page.
**control plane** — The set of components that run the cluster itself
(API server, scheduler, etcd) as opposed to your applications. Normally
kept on separate machines from your workloads; this repo's single-node
setup deliberately blurs that line for simplicity — see
[`talos/patch-controlplane.yaml`](../talos/patch-controlplane.yaml).
kept on separate machines from your workloads; this repo's three nodes
deliberately blur that line, each acting as control plane and worker at
once — see [`talos/patch-controlplane.yaml`](../talos/patch-controlplane.yaml).
**CRD (Custom Resource Definition)** — A way to teach Kubernetes about a
new *kind* of object beyond its built-ins (Pod, Service, etc.). Argo CD's
@@ -74,13 +74,19 @@ the real system to match it. The core idea this whole repo demonstrates.
**Ingress** — A Kubernetes object that routes external HTTP(S) traffic to
services inside the cluster by hostname/path, via an Ingress controller.
Not used in this repo's v1 — see
[`docs/08-growing-up.md`](08-growing-up.md).
[`docs/09-growing-up.md`](09-growing-up.md).
**IPAddressPool** — A MetalLB custom resource (see CRD) declaring which IP
addresses it's allowed to hand out to `LoadBalancer` Services. Configured
in [`apps/metallb/templates/ipaddresspool.yaml`](../apps/metallb/templates/ipaddresspool.yaml)
— see [`docs/05-metallb.md`](05-metallb.md).
**iSCSI** — A protocol for accessing block storage over a network as if it
were a local disk. Longhorn's underlying engine uses it, which is why
nodes need the `iscsi-tools` Talos system extension (see below) baked
into their image before Longhorn will work — see
[`docs/01-prerequisites.md`](01-prerequisites.md).
**kubeconfig** — A file holding the credentials and connection info
`kubectl` (and other Kubernetes tools) use to talk to a specific cluster.
Retrieved with `talosctl kubeconfig` in this setup.
@@ -96,6 +102,12 @@ cloud provider this provisions an actual cloud load balancer; on bare
metal (this repo) MetalLB fills that role instead — see
[`docs/05-metallb.md`](05-metallb.md).
**Longhorn** — Software installed in this repo (`apps/longhorn.yaml`) that
turns a plain disk on each node into one pool of storage, replicated
across nodes, so apps can request real persistent storage on bare metal —
a job a cloud provider's disk service would normally do. See
[`docs/06-longhorn.md`](06-longhorn.md).
**machine config** — Talos's equivalent of a Kubernetes manifest, but for
the operating system itself: a declarative file describing how a node
should be configured, generated with `talosctl gen config` and sent to the
@@ -116,19 +128,37 @@ independently.
**node** — A single machine (physical or virtual) that's part of a
Kubernetes cluster.
**PersistentVolumeClaim (PVC)** — How an app asks Kubernetes for storage:
"I need this much space," without caring which disk or which node
provides it. Longhorn is what actually fulfills that request in this
repo — see [`docs/06-longhorn.md`](06-longhorn.md).
**Pod** — The smallest deployable unit in Kubernetes: one or more
containers that run together, sharing network and storage. You'll rarely
create Pods directly — a Deployment creates and manages them for you.
**quorum** — The minimum number of etcd members (a majority) that have to
agree before the cluster's control plane can do anything. This is why
control-plane node counts matter, and why 3 is a meaningfully different
number than 2 or 4 — see
[`docs/02-talos-bootstrap.md`](02-talos-bootstrap.md#three-nodes-each-both-control-plane-and-worker)
and [`docs/09-growing-up.md`](09-growing-up.md#why-not-a-fourth-control-plane-node).
**reconciliation** — The repeating process of comparing desired state
(Git, for Argo CD) against actual state (the live cluster) and correcting
any difference. See the diagram in
[`docs/00-what-is-all-this.md`](00-what-is-all-this.md#the-reconciliation-loop).
**schematic** — A custom Talos boot image, built at
[factory.talos.dev](https://factory.talos.dev) by selecting which system
extensions (see below) it should include. This repo's nodes need one with
`iscsi-tools` and `util-linux-tools` for Longhorn to work — see
[`docs/01-prerequisites.md`](01-prerequisites.md).
**Secret** — A Kubernetes object for holding sensitive data. Its values are
only base64-*encoded*, not encrypted — never commit a raw Secret manifest
containing real credentials to Git. See
[`docs/08-growing-up.md`](08-growing-up.md) for how to do this safely.
[`docs/09-growing-up.md`](09-growing-up.md) for how to do this safely.
**selfHeal** — An Argo CD sync policy setting: if the live cluster drifts
from what's in Git (e.g. someone runs `kubectl edit`), Argo CD reverts it
@@ -141,6 +171,12 @@ uses a `LoadBalancer` Service for the demo app, backed by MetalLB — see
[`apps/demo/service.yaml`](../apps/demo/service.yaml) and
[`docs/05-metallb.md`](05-metallb.md).
**StorageClass** — A Kubernetes object describing a "kind" of storage
that's available (who provides it, how). Longhorn's chart registers itself
as the cluster's default StorageClass, so a plain
`PersistentVolumeClaim` with no class named gets Longhorn storage
automatically — see [`docs/06-longhorn.md`](06-longhorn.md).
**sync** — Argo CD's term for actually applying a change (a reconciliation
pass that found and fixed a difference). "Synced" means the cluster
currently matches Git.
@@ -153,9 +189,27 @@ waits for a wave to be healthy before starting the next. Used in
apply before the `IPAddressPool`/`L2Advertisement` that depend on them —
see [`docs/05-metallb.md`](05-metallb.md).
**Talos Linux** — The operating system this repo's node(s) run: built only
**system extension** — An optional add-on baked into a Talos boot image at
build time (see "schematic," above), since Talos has no package manager to
install one onto an already-running node. This repo's nodes need
`iscsi-tools` and `util-linux-tools` for Longhorn.
**Talos Linux** — The operating system this repo's nodes run: built only
to run Kubernetes, configured entirely declaratively over the network, with
no SSH or shell. See [`docs/02-talos-bootstrap.md`](02-talos-bootstrap.md).
**talosconfig** — The credentials file `talosctl` uses to talk to a Talos
node's API — the Talos-level equivalent of a kubeconfig.
**UserVolumeConfig** — A standalone Talos configuration resource (not part
of the normal `machine:`/`cluster:` structure) that tells Talos to find,
format, and mount a disk on its own, based on rules you give it (which
disk to pick, minimum size). Used in
[`talos/patch-longhorn.yaml`](../talos/patch-longhorn.yaml) to prepare
each node's storage for Longhorn.
**VIP (Virtual IP)** — One IP address that Talos keeps pointed at whichever
of several control-plane nodes is currently healthy, re-electing within
seconds if that node goes down. Everything that talks to this cluster
should use the VIP, not any one node's own address — see
[`docs/02-talos-bootstrap.md`](02-talos-bootstrap.md#the-vip-one-address-for-a-cluster-that-moves).
+17 -3
View File
@@ -13,6 +13,21 @@ assigned or changed, so you (or Claude) never have to reverse-engineer
> instruction that tells Claude to do this on its own when it makes such a
> change itself.
## The three nodes and the VIP
None of these are from the MetalLB pool below — they're the machines
themselves and the shared address Talos elects between them. Set in
[`talos/README.md`](../talos/README.md); see
[`docs/02-talos-bootstrap.md`](02-talos-bootstrap.md) for what the VIP is
and why it's separate from any one node's address.
| Address | What | Port | URL |
|---|---|---|---|
| `EDIT ME` | Node 1 | 6443 | `https://EDIT ME:6443` |
| `EDIT ME` | Node 2 | 6443 | `https://EDIT ME:6443` |
| `EDIT ME` | Node 3 | 6443 | `https://EDIT ME:6443` |
| `EDIT ME` | Cluster VIP (use this one, not a node's own address) | 6443 | `https://EDIT ME:6443` |
## MetalLB pool
| | |
@@ -20,16 +35,15 @@ assigned or changed, so you (or Claude) never have to reverse-engineer
| Range | `EDIT ME — e.g. 192.168.1.240-192.168.1.250` |
| Configured in | [`apps/metallb/templates/ipaddresspool.yaml`](../apps/metallb/templates/ipaddresspool.yaml) |
## Allocated addresses
## Allocated addresses (from the MetalLB pool)
| Address | What | Namespace | Port(s) | URL | Source |
|---|---|---|---|---|---|
| `<NODE_IP>` | Talos node — Kubernetes API server | — | 6443 | `https://<NODE_IP>:6443` | Not from the MetalLB pool — the node's own address, set in [`talos/README.md`](../talos/README.md) |
| `EDIT ME` | Argo CD UI/API (`argocd-server`) | `argocd` | 443 | `https://EDIT ME` | Patched to `LoadBalancer` by hand — see [`docs/05-metallb.md`](05-metallb.md) |
| `EDIT ME` | Demo app | `demo` | 80 | `http://EDIT ME` | [`apps/demo/service.yaml`](../apps/demo/service.yaml) |
Add a row here every time you add a new app with a `LoadBalancer` Service
(see [`docs/07-adding-new-apps.md`](07-adding-new-apps.md)). Leave the URL
(see [`docs/08-adding-new-apps.md`](08-adding-new-apps.md)). Leave the URL
column's port number off only when it's the protocol default (80 for
`http://`, 443 for `https://`) — include it otherwise.