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:
co-authored by
Claude Sonnet 5
parent
92c3ad3191
commit
110bd97cca
+46
-33
@@ -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
@@ -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
@@ -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).
|
||||
|
||||
@@ -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
@@ -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
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user