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:
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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`
|
||||
Reference in New Issue
Block a user