Initial commit: GitOps Starter Pack

A teaching-first, single-cluster GitOps setup: Talos Linux + Argo CD with
an app-of-apps layout, MetalLB pre-installed for real LoadBalancer IPs on
bare metal, a demo app, and a progressive docs/ walkthrough with a
CLAUDE.md tutor persona and guided bootstrap checklist.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Maxwell Morris
2026-08-22 17:30:45 +01:00
co-authored by Claude Sonnet 5
commit 92c3ad3191
29 changed files with 2083 additions and 0 deletions
+149
View File
@@ -0,0 +1,149 @@
# 00 — What is all this?
You're comfortable with servers, SSH, config files, and the command line.
You've probably never touched Kubernetes. This doc gives you the mental
model before you type a single command — everything after this builds on
it.
## The problem this stack solves
Say you run a few services on a server. The traditional way: SSH in,
install things, edit config files by hand, restart services, and remember
what you did (or don't, and regret it later). It works, but nothing
records *why* the server looks the way it does — the truth lives only on
that one machine, in your head, and in shell history if you're lucky.
This stack solves that by making a **Git repository** the single source of
truth for "what should be running," and using software to continuously make
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
**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.
**Talos Linux** is the operating system your 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
declarative idea as Kubernetes itself, just one layer lower. Less to
install, less to secure, less that can drift out of a known state.
**Argo CD** is the piece that watches a Git repository — this one — and
continuously makes the cluster match whatever it finds there. This is
called **GitOps**: Git isn't just where your config *lives*, it's where
change *happens*. You don't run `kubectl apply` for day-to-day changes;
you edit a file, commit, push, and Argo CD does the applying.
**App of apps** is the pattern this repo uses to organize what Argo CD
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).
**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
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).
## Architecture — the shape of what you'll build
```
┌─────────────────────────┐
│ Git repository (this │
│ repo, on GitHub) │
│ │
│ apps/demo.yaml │
│ apps/demo/*.yaml │──┐
│ apps/metallb.yaml │ │ watched by
│ apps/metallb/* │ │
└─────────────────────────┘ │
▼
┌───────────────────┐
│ Argo CD │ runs inside the cluster
│ (installed once, │
│ imperatively) │
└─────────┬──────────┘
│ 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│ │
│ └────────────┘ └──────────┘ │
└─────────────────────────────┘
```
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.
## The reconciliation loop
This is the mechanism that makes GitOps actually work, not just a nice
diagram. Argo CD repeats this forever, by default every few minutes (and
instantly, if you ask it to):
```
┌────────────────────────────────────────────────┐
│ │
│ 1. Read desired state from Git │
│ │ │
│ ▼ │
│ 2. Read actual state from the cluster │
│ │ │
│ ▼ │
│ 3. Diff them │
│ │ │
│ ┌────┴────┐ │
│ │ │ │
│ no diff diff found │
│ │ │ │
│ │ ▼ │
│ │ 4. Apply the change so the │
│ │ cluster matches Git │
│ │ │ │
│ └────┬─────┘ │
│ ▼ │
│ wait, then repeat │
│ │
└────────────────────────────────────────────────┘
```
"Reconciliation" just means step 3 and 4: noticing a difference between what
should exist and what does, and fixing it. This loop is also what makes
Argo CD self-healing — if someone changes the cluster by hand (`kubectl edit`
during an emergency, say), the very next loop notices the drift from Git and
reverts it. Git wins, always. That's the whole point.
> **Ask Claude:** before moving on, try asking "why does Git winning matter —
> what goes wrong without it?" It's worth being able to answer in your own
> words before you start building.
## What's next
Continue to [`docs/01-prerequisites.md`](01-prerequisites.md), or jump
straight to [`docs/checklist.md`](checklist.md) if you'd rather work through
a condensed step-by-step list with Claude alongside you.
+112
View File
@@ -0,0 +1,112 @@
# 01 — Prerequisites
Everything here targets **amd64** (standard 64-bit x86 — a spare PC, mini
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
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.
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 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
[`docs/02-talos-bootstrap.md`](02-talos-bootstrap.md).
> **Ask Claude:** if you're not sure whether to use a VM or spare physical
> hardware, ask — the tradeoffs (snapshots and easy resets vs. more
> realistic networking) are worth five minutes of discussion before you
> commit.
## 2. talosctl
The CLI you use to configure and manage Talos nodes.
**macOS (Homebrew):**
```sh
brew install siderolabs/tap/talosctl
```
**Linux:**
```sh
curl -sL https://talos.dev/install | sh
```
Verify:
```sh
talosctl version --client
```
## 3. kubectl
The standard Kubernetes CLI — you'll use it constantly, for this and every
future Kubernetes cluster you touch.
**macOS (Homebrew):**
```sh
brew install kubectl
```
**Linux:** follow the
[official install instructions](https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/)
for your distribution.
Verify:
```sh
kubectl version --client
```
## 4. argocd CLI
The command-line client for Argo CD — used mainly to log in and inspect
Applications; most day-to-day work happens through Git, not this CLI.
**macOS (Homebrew):**
```sh
brew install argocd
```
**Linux:**
Download the `argocd-linux-amd64` binary from the
[Argo CD releases page](https://github.com/argoproj/argo-cd/releases), then:
```sh
chmod +x argocd-linux-amd64
sudo mv argocd-linux-amd64 /usr/local/bin/argocd
```
Verify:
```sh
argocd version --client
```
## 5. A GitHub account and a fork of this repo
This whole system works by Argo CD reading a Git repository you control.
Fork this repo (or create a new repo from it) on GitHub, and clone *your*
copy — not the original — to work from. Every command later in this guide
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
- [ ] `talosctl version --client` works
- [ ] `kubectl version --client` works
- [ ] `argocd version --client` works
- [ ] You have your own fork of this repo cloned locally
Once all of these are true, continue to
[`docs/02-talos-bootstrap.md`](02-talos-bootstrap.md).
+72
View File
@@ -0,0 +1,72 @@
# 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.
## Why Talos, specifically
Most guides have you install a general-purpose Linux distribution, then
install Kubernetes on top by hand or with a tool like kubeadm — meaning
there's a whole normal OS underneath, with a package manager, an SSH
daemon, and every other thing that can drift out of a known-good state or
need patching. Talos throws that away: it's an OS whose only job is running
Kubernetes. There's no SSH, no shell to log into, no packages to manage —
you configure it entirely by sending it a **machine config** file over the
network (the `talosctl` commands below), and it applies that config the
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
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.
## 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.
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.
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.
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.
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.
5. **Verify.** `kubectl get nodes` should show one node, `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
> you've got both — the split trips people up the first time.
## 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
Once your node is `Ready`, continue to
[`docs/03-installing-argocd.md`](03-installing-argocd.md).
+58
View File
@@ -0,0 +1,58 @@
# 03 — Installing Argo CD
Your cluster is alive but empty — it can run containers, but nothing is
telling it to run anything yet. This doc installs Argo CD, the piece that
watches Git and does that telling from now on.
## Why this one step is imperative
Everything else in this repo — every app, every future addition — gets
deployed *by* Argo CD, driven from Git. Argo CD itself is the one exception:
something has to exist in the cluster *before* it can start reading Git, so
installing it can't itself be a GitOps-managed step. This is normal, and
every GitOps setup has some version of this same bootstrap moment. We
install it the straightforward way the Argo CD project documents — one
`kubectl apply` of their maintained manifest bundle — rather than adding an
extra tool (Helm, an operator) just to install the GitOps tool itself.
## What you're installing
Argo CD is itself a set of things running inside your cluster: an API
server, a piece that watches Git repos, a piece that computes diffs against
the live cluster, and a UI. You're about to install all of it with one
command, into its own `argocd` namespace, so it stays cleanly separated
from the apps it will go on to manage.
## Steps
Exact commands: [`argocd/README.md`](../argocd/README.md). Summary of what
each stage does:
1. **Install.** `kubectl apply` the official manifest bundle into a new
`argocd` namespace. This creates Argo CD's own pods *and* a handful of
CRDs (see [`docs/glossary.md`](glossary.md)) — the most important being
`Application`, the resource type you'll use for every app from here on,
including the demo app and the app-of-apps.
2. **Get the initial admin password.** Auto-generated on first install and
stored as a Kubernetes Secret.
3. **Reach it.** Port-forward the Argo CD UI/API to your local machine and
log in with the `argocd` CLI (and optionally, a browser).
4. **Apply the app-of-apps.** This is the moment the GitOps loop actually
starts — see [`docs/04-app-of-apps.md`](04-app-of-apps.md) next.
> **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).
## Checklist
- [ ] Argo CD pods all `Running`/`Ready` in the `argocd` namespace
- [ ] Initial admin password retrieved
- [ ] Logged in via `argocd login` (CLI) and/or the UI in a browser
Once you're logged in, continue to
[`docs/04-app-of-apps.md`](04-app-of-apps.md).
+119
View File
@@ -0,0 +1,119 @@
# 04 — The app-of-apps pattern
This is the last manual step. After this, Git drives everything.
## The idea
An Argo CD `Application` is a resource that says "watch this path in this
Git repo, and keep the cluster matching what's there." The simplest
possible setup is one Application per app you want to run — but then every
new app means one more manual `kubectl apply`, which quietly puts you back
to making changes by hand.
**App-of-apps** fixes this by making Applications themselves something Argo
CD manages. One Application — the app-of-apps — watches a folder
([`apps/`](../apps)) full of *other* Application manifests. Add a new app by
adding a new file to that folder and pushing it — the app-of-apps notices
the new file and creates the Application it describes, which in turn
deploys the app. No `kubectl apply` required past this point, ever, for a
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)
```
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.
## 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
already doing.
## Every Application needs its own `repoURL`
Applications don't inherit `repoURL` from whatever applied them — each one
declares its own source repo independently. That means the placeholder
`https://github.com/YOUR_GITHUB_USERNAME/...` needs fixing in *every* file
that has it, not just the one you apply by hand:
```sh
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.
## Steps
1. Fix `repoURL` in all three 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.
```sh
git add argocd/app-of-apps/app-of-apps.yaml apps/demo.yaml apps/metallb.yaml
git commit -m "Point Applications at my fork"
git push
```
3. Apply the app-of-apps — the one manifest you apply by hand:
```sh
kubectl apply -f argocd/app-of-apps/app-of-apps.yaml
```
4. Watch it sync, then watch `demo` and `metallb` 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).
5. Confirm the demo app is actually running:
```sh
kubectl -n demo get pods,svc
```
You'll notice the Service's `EXTERNAL-IP` column says `<pending>` —
that's expected. The demo app is a `LoadBalancer` Service, and nothing
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.
> **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
> 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
- [ ] Fix committed and pushed
- [ ] `app-of-apps` applied and shows `Synced`/`Healthy`
- [ ] `demo` and `metallb` Applications appeared automatically, both
`Synced`/`Healthy`
- [ ] `kubectl -n demo get pods,svc` shows the demo app's Pod running
(Service `EXTERNAL-IP` still `<pending>` — expected for now)
Continue to [`docs/05-metallb.md`](05-metallb.md) to give the demo app —
and Argo CD's own UI — a real address.
+155
View File
@@ -0,0 +1,155 @@
# 05 — MetalLB: getting real IP addresses on bare metal
The app-of-apps you just applied ([`docs/04-app-of-apps.md`](04-app-of-apps.md))
already brought up `metallb` alongside `demo`. This doc explains what it
does, walks you through the one piece only you can decide (which IP
addresses to actually use), and finishes by giving Argo CD's own UI a real
address too.
## Why this is needed at all
On a cloud provider, a Kubernetes Service of `type: LoadBalancer` triggers
the cloud to provision an actual load balancer and hand your Service a
real, routable IP automatically. Bare metal — your Talos node, sitting on
your own network — has no such thing behind it. Without help, a
`LoadBalancer` Service on bare metal just sits with its address stuck at
`<pending>` forever.
**MetalLB** is that help: software that runs inside your cluster,
watches for `LoadBalancer` Services, and hands them a real address from a
range *you* configure — then makes that address actually reachable on your
network. It's the standard, widely-used answer to this exact gap, which is
why it's installed as a pre-installed app in this repo rather than left as
a "next step."
`apps/demo/service.yaml` is already `type: LoadBalancer` — that's what you
saw sitting at `<pending>` if you looked closely in the last doc. This doc
is what turns that into a real, reachable address.
## What's already running
**`apps/metallb.yaml`** → one Application, pointing at `apps/metallb`, a
Helm chart that does two things at once (see
[`apps/metallb/Chart.yaml`](../apps/metallb/Chart.yaml)):
- 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).
- Carries two templates of its own
([`apps/metallb/templates/`](../apps/metallb/templates)): an
`IPAddressPool` and an `L2Advertisement` — MetalLB's own custom
resources, which tell it which addresses it's allowed to use and how to
advertise them. Right now these still contain **placeholder** IP
addresses that won't work on your network — that's what you're about to
fix.
Both the upstream chart and our two extra resources sync together as one
Application. Look at the `sync-wave` annotation on
[`apps/metallb/templates/ipaddresspool.yaml`](../apps/metallb/templates/ipaddresspool.yaml) —
it's what keeps this safe despite being one sync: MetalLB's own resources
(including its CRDs) apply first, and Argo CD waits for them to be healthy
before applying our two resources, which depend on those CRDs existing.
## Answer three questions before editing anything
> **Ask Claude:** literally ask — "help me pick MetalLB's IP range and the
> addresses for Argo CD and the demo app." This is the one part of this
> whole repo that depends entirely on *your* network, which nothing in
> this repo can know in advance. Have your router's admin page or DHCP
> settings handy; Claude will ask what your LAN's subnet is and what range
> your DHCP server already hands out, so the pool it suggests doesn't
> collide with anything.
You need three decisions:
1. **The pool range** — a handful of addresses (5–10 is plenty for this
repo) on your LAN's subnet that your DHCP server will never hand to
anything else. Commonly a slice just above or below your DHCP range,
e.g. if your router hands out `.100`–`.199`, something like
`.240`–`.250` is a safe, unused slice.
2. **Argo CD's UI address** — one address from that pool.
3. **The demo app's address** — a different address from that pool.
## Edit the placeholders
1. [`apps/metallb/templates/ipaddresspool.yaml`](../apps/metallb/templates/ipaddresspool.yaml)
— replace the `addresses` range with your chosen pool.
2. [`apps/demo/service.yaml`](../apps/demo/service.yaml) — replace the
`metallb.universe.tf/loadBalancerIPs` annotation with the demo app's
chosen address (must be inside the pool from step 1).
3. [`docs/network.md`](network.md) — record the pool range and the demo
app's address in the tables there (Argo CD's address goes in once you've
picked it below).
4. Commit and push all three:
```sh
git add apps/metallb/templates/ipaddresspool.yaml apps/demo/service.yaml docs/network.md
git commit -m "Configure MetalLB's address pool for my network"
git push
```
5. Force a sync rather than waiting for the next poll:
```sh
argocd app sync metallb demo
```
## Verify
```sh
kubectl -n demo get svc demo
```
`EXTERNAL-IP` should now show the address you chose instead of `<pending>`.
Visit `http://<that-address>` in a browser — same "It works!" page as
before, now on its own real IP instead of a NodePort.
```sh
kubectl -n metallb-system get ipaddresspools,l2advertisements
```
Confirms your pool and advertisement were applied as expected.
## Give Argo CD's UI a real address too
So far you've reached the Argo CD UI through `kubectl port-forward`
(see [`docs/03-installing-argocd.md`](03-installing-argocd.md)). Now that
MetalLB is running, you can give it a permanent address instead. Argo CD's
own install isn't managed through this repo's GitOps loop (it's the
imperative bootstrap step — see `docs/03`), so this one change is applied
directly rather than through Git:
```sh
kubectl -n argocd patch svc argocd-server --type merge -p \
'{"spec":{"type":"LoadBalancer"},"metadata":{"annotations":{"metallb.universe.tf/loadBalancerIPs":"<ARGOCD_IP>"}}}'
```
Replace `<ARGOCD_IP>` with the address you chose in step 2 above. Then:
```sh
kubectl -n argocd get svc argocd-server
```
Once `EXTERNAL-IP` shows your chosen address, visit
`https://<that-address>` directly — no port-forward needed from now on.
(You'll still hit the same self-signed certificate warning as before; see
[`docs/03-installing-argocd.md`](03-installing-argocd.md) for why that's
expected here.)
Update [`docs/network.md`](network.md) one more time with this address —
commit and push it. This one's a genuine exception to "everything's driven
by Git": the patch command above isn't a file in this repo, so this file is
the only place this address gets recorded at all.
> **Ask Claude:** "why did the demo app get its LoadBalancer IP through
> Git, but Argo CD's didn't?" — a good check that the boundary between
> "managed by this repo" and "the imperative bootstrap that starts it"
> has actually landed.
## Checklist
- [ ] Chosen a pool range, an Argo CD UI address, and a demo app address,
with Claude's help, that fit your actual network
- [ ] `apps/metallb/templates/ipaddresspool.yaml` edited to your real range
- [ ] `apps/demo/service.yaml` edited to your real demo address
- [ ] `docs/network.md` updated with the pool range and both addresses
- [ ] Committed, pushed, synced
- [ ] `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).
+94
View File
@@ -0,0 +1,94 @@
# 06 — 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
watching the cluster follow, with no `kubectl apply` from you at all.
## Steps
1. Open [`apps/demo/configmap.yaml`](../apps/demo/configmap.yaml) and edit
the HTML — change the `<h1>` text to something of your own.
2. Commit and push, exactly like any other Git change:
```sh
git add apps/demo/configmap.yaml
git commit -m "Personalize the demo page"
git push
```
3. By default, Argo CD polls Git every three minutes, so your change will
show up on its own — but you can force it immediately:
```sh
argocd app sync demo
```
4. Watch it happen:
```sh
argocd app get demo
```
You'll see Argo CD detect that the live ConfigMap no longer matches
Git, and correct it.
5. Refresh `http://<your-demo-app-IP>` (the MetalLB address you gave it in
[`docs/05-metallb.md`](05-metallb.md)). Your new text should be there. Note
that nginx itself never restarted — Kubernetes noticed the mounted
ConfigMap changed and updated the file inside the running Pod's
filesystem automatically. (This is specific to volume-mounted
ConfigMaps; not every kind of change is this seamless — a change to the
Deployment's image, for instance, does trigger a Pod restart, by
design.)
Give it up to a minute if the page doesn't look different yet.
`argocd app get demo` shows the *ConfigMap object* updating right away,
but kubelet — the agent on the node that actually keeps a Pod's mounted
files in sync — only re-checks mounted ConfigMaps on its own periodic
cycle, not instantly. A blank refresh isn't a sign anything's wrong;
give it a moment and refresh again.
## What just proved itself
You changed a file, pushed it, and never touched the cluster directly. That
gap between "commit" and "running" being closed automatically, without you
doing the applying, *is* GitOps. Everything from here on — every future app
you add — works exactly this way: edit, commit, push, done.
## Try triggering self-healing
Optional, but worth seeing once. Reintroduce drift by hand:
```sh
kubectl -n demo patch configmap demo-page --type merge \
-p '{"data":{"index.html":"<h1>hand-edited, not from Git</h1>"}}'
```
Watch the ConfigMap *object* itself, not the browser, for this one — it's
the more reliable way to see the timing: Argo CD's self-heal typically
reverts a hand-edit within a second or two, which is faster than kubelet's
own mount-sync cycle usually gets around to reflecting a change in the
served page at all (see the note above). Check the object directly:
```sh
kubectl -n demo get configmap demo-page -o jsonpath='{.data.index\.html}'
```
Run that once right after the `patch` above and you may catch your
hand-edited text; run it again a couple of seconds later and it'll already
be back to what's in Git — with no action from you. That's `selfHeal:
true` from `apps/demo.yaml` in action. See
[`docs/00-what-is-all-this.md`](00-what-is-all-this.md#the-reconciliation-loop)
for the loop that just ran.
> **Ask Claude:** "if selfHeal is on, how would I ever safely debug
> something by changing it live in the cluster first?" — a real question
> people run into once this clicks, and worth understanding before you hit
> it for real.
## Checklist
- [ ] Edited `apps/demo/configmap.yaml`, committed, pushed
- [ ] Saw the change reflected in `argocd app get demo`
- [ ] 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
something of your own.
+87
View File
@@ -0,0 +1,87 @@
# 07 — 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.
## The pattern
Every app in this repo is two things sitting side by side in
[`apps/`](../apps):
```
apps/
demo.yaml ← the Application (tells Argo CD where to look)
demo/ ← the actual manifests (what gets deployed)
```
To add a new app called, say, `whoami`:
1. Create `apps/whoami/` and put its Kubernetes manifests in there — a
Deployment, a Service, whatever it needs. Use `apps/demo/` as a
template for structure and comment style.
2. Create `apps/whoami.yaml`, copied from [`apps/demo.yaml`](../apps/demo.yaml)
with the names changed: `metadata.name`, `spec.source.repoURL` (still
your fork — see [`docs/04-app-of-apps.md`](04-app-of-apps.md#every-application-needs-its-own-repourl)
for why every Application needs this set individually), `spec.source.path`
(→ `apps/whoami`), and `spec.destination.namespace`.
3. Commit and push both. That's it — no `kubectl apply`. The app-of-apps
(already watching `apps/` — see
[`docs/04-app-of-apps.md`](04-app-of-apps.md)) notices the new
`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`.
## Manifest-based vs. Helm-based apps
`apps/demo/` is plain Kubernetes manifests — no templating, nothing to
render — which is why it's the easiest one to read start to finish while
you're still learning what each field does.
An app's folder can be a **Helm chart** instead. Argo CD detects this
automatically: if it finds a `Chart.yaml` at the source path, it treats
that app as a Helm chart rather than plain manifests. This matters most
when you want to pull in a *third-party* chart — a database, an ingress
controller, anything with a public Helm chart already maintained by
someone else — without copying their manifests into your repo by hand. You
declare it as a dependency and let Helm (via Argo CD) resolve it.
You don't have to imagine this one — it's already running.
[`apps/metallb/`](../apps/metallb) is exactly this pattern, for real: its
[`Chart.yaml`](../apps/metallb/Chart.yaml) declares a dependency on the
upstream MetalLB chart, and it even goes a step further by also carrying
its own templates ([`apps/metallb/templates/`](../apps/metallb/templates))
for two custom resources that configure MetalLB once it's installed —
useful to see if you ever need an app that's *mostly* someone else's chart
but with a couple of your own resources layered on top. See
[`docs/05-metallb.md`](05-metallb.md) for the full walkthrough, and
[`apps/metallb.yaml`](../apps/metallb.yaml) for the matching Application —
notice it's structurally identical to `apps/demo.yaml`; Argo CD figures out
it's a Helm chart on its own from the presence of `Chart.yaml`, no new
syntax required in the Application resource itself.
> **Ask Claude:** "walk me through how I'd bring in [some real chart you
> have in mind] as a Helm-based app in this structure" whenever you're
> ready to try one of your own — `apps/metallb/` is a real template to
> copy from.
If the new app has its own `LoadBalancer` Service (see
[`docs/05-metallb.md`](05-metallb.md)), pick its address from the free
addresses listed in [`docs/network.md`](network.md), and add a row for it
there once it's up — that file only stays useful if every app that gets an
address shows up in it.
## Checklist
- [ ] New app's manifests live in `apps/<name>/`
- [ ] New `apps/<name>.yaml` Application created, modeled on `demo.yaml`
- [ ] `repoURL` set to your fork, not left as a placeholder
- [ ] Pushed to Git
- [ ] `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
next as this cluster grows beyond a learning setup.
+91
View File
@@ -0,0 +1,91 @@
# 08 — 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
with why you'd want it and roughly what it involves. None of it is built
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.
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
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.
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.
## Ingress
Right now, thanks to MetalLB ([`docs/05-metallb.md`](05-metallb.md)), the
demo app gets its own dedicated IP address
(see [`apps/demo/service.yaml`](../apps/demo/service.yaml)). That's a real
improvement over NodePort, but it doesn't scale past a handful of apps
either: every app still needs its own IP address out of a pool that's only
so big, and there's no way to route by hostname (`app1.example.com` vs
`app2.example.com` sharing one IP on port 443).
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
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
`Ingress` resource per app instead of giving each one its own
`LoadBalancer` Service — trading "one IP per app" for "one IP, many
hostnames."
## cert-manager
Once you have Ingress and real hostnames, you'll want HTTPS. `cert-manager`
is the standard tool for this — it watches for certificate requests and
automatically obtains and renews them (commonly via Let's Encrypt). Also
just another app in this repo's structure once you get there.
## Secrets management
**This repo is public. Never commit a plaintext secret, credential, token,
or private key to it — anyone can read your Git history, forever, even if
you delete the file in a later commit.**
Kubernetes' own `Secret` resource does not encrypt anything — it just
base64-encodes data, which is not encryption, only encoding (trivially
reversible by anyone). None of the manifests in this repo need secrets yet,
which is exactly why this hasn't come up so far. When you add something
that does (a database password, an API key), you need one of:
- **[Sealed Secrets](https://github.com/bitnami-labs/sealed-secrets)** —
encrypts a secret using a key that only your cluster holds, producing a
`SealedSecret` file that's safe to commit; the cluster decrypts it into a
normal Secret on the fly.
- **[SOPS](https://github.com/getsops/sops)** (often paired with Argo CD via
a plugin) — encrypts values inside a normal-looking YAML file using a key
from a system like age or a cloud KMS, decrypted at sync time.
Either is a reasonable choice; don't improvise your own approach. Ask
Claude to walk you through setting one up when you have a real secret to
manage — it's a short, well-trodden path once you're at that point.
> **Ask Claude:** if you're about to commit anything you're even slightly
> unsure about, paste it (or describe it) and ask first. Cheaper than a
> `git filter-branch` later.
## Where to go from here
You've now seen the whole shape: Talos underneath, Argo CD watching Git,
app-of-apps organizing what it manages. Everything in this doc is more of
the same pattern — a new app, a new file, a push. Pick whichever expansion
is actually useful to you and start there.
+90
View File
@@ -0,0 +1,90 @@
# 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
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.
## 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
- [ ] `talosctl version --client` runs successfully
- [ ] `kubectl version --client` runs successfully
- [ ] `argocd version --client` runs successfully
- [ ] Repo forked on GitHub and cloned locally as **your own copy**
## 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
- [ ] `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`
## Phase 3 — Argo CD install ([`03`](03-installing-argocd.md), [`argocd/README.md`](../argocd/README.md))
- [ ] `kubectl create namespace argocd`
- [ ] `kubectl apply --server-side -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml`
- [ ] `kubectl -n argocd wait --for=condition=Available --timeout=300s deployment --all` completes
- [ ] Initial admin password retrieved
- [ ] `kubectl -n argocd port-forward svc/argocd-server 8080:443` running in its own terminal
- [ ] `argocd login localhost:8080 --username admin --password <pw> --insecure` succeeds
## Phase 4 — Hand the cluster to Git ([`04`](04-app-of-apps.md))
- [ ] `spec.source.repoURL` fixed in all three 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`)
- [ ] 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`
- [ ] `kubectl -n demo get pods,svc` shows the demo app's Pod running
(Service `EXTERNAL-IP` still `<pending>` — expected for now)
## Phase 5 — MetalLB: real IP addresses ([`05`](05-metallb.md))
- [ ] **Ask Claude** to help pick: a MetalLB pool range, an address for
Argo CD's UI, and an address for the demo app — all based on your
actual LAN/DHCP setup
- [ ] `apps/metallb/templates/ipaddresspool.yaml` edited to your real pool range
- [ ] `apps/demo/service.yaml` edited to your real demo app address
- [ ] `docs/network.md` updated with the pool range and both addresses (Argo CD's added once patched, below)
- [ ] Committed, pushed, and synced (`argocd app sync metallb demo`)
- [ ] `kubectl -n demo get svc demo` shows a real `EXTERNAL-IP`; page loads at `http://<that-address>`
- [ ] `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))
- [ ] Edited the `<h1>` text in `apps/demo/configmap.yaml`
- [ ] Committed and pushed
- [ ] `argocd app sync demo` (or waited for auto-poll) shows the change applied
- [ ] Refreshed the browser — new text visible
- [ ] (Optional) Triggered self-heal: hand-edited the ConfigMap with `kubectl patch`, confirmed Argo CD reverted it
## You're done when
Every box above is checked, you've watched a change you made in a text
editor show up in a browser without ever running `kubectl apply` yourself
for it, and you can explain in your own words what happened between "git
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
bigger next steps (more nodes, ingress, secrets management).
+161
View File
@@ -0,0 +1,161 @@
# Glossary
Every term this repo uses, in plain English. If you hit a word in the docs
or a YAML comment that isn't here, ask Claude to add it.
**API server** — The part of Kubernetes every other piece (and every
`kubectl` command) talks to. It's the front door: all reads and writes to
the cluster's state go through it.
**Application (Argo CD)** — A Custom Resource (see CRD, below) that Argo CD
adds to Kubernetes. It tells Argo CD "watch this path in this Git repo, and
keep the cluster matching it." Not a Kubernetes-native concept — it only
exists once Argo CD is installed.
**app-of-apps** — The pattern this repo uses: one Application (itself
called the app-of-apps) watches a folder of *other* Application manifests,
so adding a new app is "add a file," not "run a new command." See
[`docs/04-app-of-apps.md`](04-app-of-apps.md).
**ARP (Address Resolution Protocol)** — How devices on the same local
network find each other's hardware addresses. MetalLB's L2 mode works by
answering ARP requests for whichever IP address it's handed out, which is
what makes that address reachable from the rest of your LAN.
**bootstrap** — The one-time setup that has to happen *before* the normal
system can take over and manage itself. Used here for `talosctl bootstrap`
(initializes etcd, once, ever) and more generally for the handful of manual
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.
**CNI (Container Network Interface)** — The plugin responsible for giving
Pods network connectivity and IP addresses. Talos ships Flannel, a CNI, by
default — you don't install one yourself in this setup.
**ConfigMap** — A Kubernetes object that holds configuration data (text,
small files) as its own resource, separate from the Pod that uses it. Lets
you change configuration without rebuilding a container image. Used for the
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).
**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
install adds a CRD for `Application`, which is what lets you write
`kind: Application` in YAML at all.
**declarative** — Describing the state you want ("one copy of nginx
running"), and letting software figure out and continuously enforce the
steps to get and stay there — as opposed to **imperative**, where you issue
the individual commands yourself ("start this container now"). Kubernetes,
Talos, and Argo CD are all declarative; the one-time install steps in this
repo (installing Argo CD itself, applying the app-of-apps) are the
deliberate imperative exceptions that get the declarative system started.
**Deployment** — A Kubernetes object describing how many copies of a
container should run and how to run them. You manage Deployments; they, in
turn, manage the actual Pods.
**etcd** — The database Kubernetes' control plane uses to store all
cluster state. `talosctl bootstrap` initializes it, once, on cluster
creation.
**GitOps** — Using a Git repository as the source of truth for a system's
desired state, with automation (here, Argo CD) continuously reconciling
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).
**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).
**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.
**L2Advertisement** — A MetalLB custom resource that tells it to advertise
addresses from a given `IPAddressPool` using ARP (see above) — "Layer 2
mode," MetalLB's simplest mode and the one this repo uses. Configured in
[`apps/metallb/templates/l2advertisement.yaml`](../apps/metallb/templates/l2advertisement.yaml).
**LoadBalancer (Service type)** — A Service that requests a real, dedicated
external IP address, reachable directly from outside the cluster. On a
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).
**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
node with `talosctl apply-config`.
**manifest** — A YAML (or JSON) file describing a Kubernetes object's
desired state. Every `.yaml` file in `apps/` is a manifest.
**MetalLB** — Software installed in this repo (`apps/metallb.yaml`) that
gives bare-metal Kubernetes clusters the ability to hand out real IP
addresses to `LoadBalancer` Services — a job a cloud provider would
normally do. See [`docs/05-metallb.md`](05-metallb.md).
**namespace** — A way to group related objects inside a cluster, like a
folder. Lets you keep different apps' resources organized and manageable
independently.
**node** — A single machine (physical or virtual) that's part of a
Kubernetes cluster.
**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.
**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).
**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.
**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
back to match Git on its next pass, rather than only fixing drift when you
manually trigger a sync.
**Service** — A Kubernetes object giving a stable network address for a
set of Pods, even as individual Pods are created and destroyed. This repo
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).
**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.
**sync-wave** — An Argo CD annotation
(`argocd.argoproj.io/sync-wave`) that stages the order resources are
applied within one Application's sync: lower numbers first, and Argo CD
waits for a wave to be healthy before starting the next. Used in
`apps/metallb/templates/` so MetalLB's own resources (including its CRDs)
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
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.
+42
View File
@@ -0,0 +1,42 @@
# Network map
Every real IP address this cluster hands out, and exactly what's reachable
there. This file isn't a template with the rest of the repo — it's a log
of *your* actual setup, meant to be updated every time an address is
assigned or changed, so you (or Claude) never have to reverse-engineer
"wait, what's running on .241 again?" from `kubectl` output.
> **Keep this current.** If you're working with Claude and an IP changes —
> a new app gets a `LoadBalancer` address, the MetalLB pool is resized,
> Argo CD's address changes — ask it to update this file, or just point it
> here after any networking change. See `CLAUDE.md` for the standing
> instruction that tells Claude to do this on its own when it makes such a
> change itself.
## MetalLB pool
| | |
|---|---|
| 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
| 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
column's port number off only when it's the protocol default (80 for
`http://`, 443 for `https://`) — include it otherwise.
## Free in the pool
Addresses from the range above that aren't spoken for yet. Update this
alongside the table above so it's obvious at a glance what's safe to hand
to the next app.
- `EDIT ME`