From 92c3ad319160123b90b0d91eebf1d8cbb4258ad5 Mon Sep 17 00:00:00 2001 From: Maxwell Morris <48025321+m4xmorris@users.noreply.github.com> Date: Sat, 22 Aug 2026 17:30:45 +0100 Subject: [PATCH] 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 --- .gitignore | 10 ++ CLAUDE.md | 136 +++++++++++++++++ README.md | 108 +++++++++++++ apps/demo.yaml | 52 +++++++ apps/demo/configmap.yaml | 27 ++++ apps/demo/deployment.yaml | 38 +++++ apps/demo/namespace.yaml | 13 ++ apps/demo/service.yaml | 32 ++++ apps/metallb.yaml | 41 +++++ apps/metallb/Chart.yaml | 21 +++ apps/metallb/templates/ipaddresspool.yaml | 29 ++++ apps/metallb/templates/l2advertisement.yaml | 19 +++ apps/metallb/values.yaml | 9 ++ argocd/README.md | 133 ++++++++++++++++ argocd/app-of-apps/app-of-apps.yaml | 47 ++++++ docs/00-what-is-all-this.md | 149 ++++++++++++++++++ docs/01-prerequisites.md | 112 ++++++++++++++ docs/02-talos-bootstrap.md | 72 +++++++++ docs/03-installing-argocd.md | 58 +++++++ docs/04-app-of-apps.md | 119 +++++++++++++++ docs/05-metallb.md | 155 +++++++++++++++++++ docs/06-your-first-change.md | 94 ++++++++++++ docs/07-adding-new-apps.md | 87 +++++++++++ docs/08-growing-up.md | 91 +++++++++++ docs/checklist.md | 90 +++++++++++ docs/glossary.md | 161 ++++++++++++++++++++ docs/network.md | 42 +++++ talos/README.md | 123 +++++++++++++++ talos/patch-controlplane.yaml | 15 ++ 29 files changed, 2083 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 apps/demo.yaml create mode 100644 apps/demo/configmap.yaml create mode 100644 apps/demo/deployment.yaml create mode 100644 apps/demo/namespace.yaml create mode 100644 apps/demo/service.yaml create mode 100644 apps/metallb.yaml create mode 100644 apps/metallb/Chart.yaml create mode 100644 apps/metallb/templates/ipaddresspool.yaml create mode 100644 apps/metallb/templates/l2advertisement.yaml create mode 100644 apps/metallb/values.yaml create mode 100644 argocd/README.md create mode 100644 argocd/app-of-apps/app-of-apps.yaml create mode 100644 docs/00-what-is-all-this.md create mode 100644 docs/01-prerequisites.md create mode 100644 docs/02-talos-bootstrap.md create mode 100644 docs/03-installing-argocd.md create mode 100644 docs/04-app-of-apps.md create mode 100644 docs/05-metallb.md create mode 100644 docs/06-your-first-change.md create mode 100644 docs/07-adding-new-apps.md create mode 100644 docs/08-growing-up.md create mode 100644 docs/checklist.md create mode 100644 docs/glossary.md create mode 100644 docs/network.md create mode 100644 talos/README.md create mode 100644 talos/patch-controlplane.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..36bfe35 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Generated by talosctl gen config — contains private keys and client +# certificates. Never commit these. +_out/ +talosconfig + +# Downloaded by talosctl kubeconfig — grants full cluster access. +kubeconfig + +# OS cruft +.DS_Store diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..fb553e0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,136 @@ +# CLAUDE.md — how to help with this repo + +This repo is the **GitOps Starter Pack**: a teaching project that takes +someone who has never touched Kubernetes, GitOps, Talos, or Argo CD to a +working, single-cluster GitOps setup they actually understand. When you +(Claude) are working in this repo, your primary job is **tutor first, doer +second**. Read this whole file before acting on any request here. + +## Who you're talking to + +An experienced IT professional: fluent with the command line, SSH, +networking, servers, and config files. **Not** fluent in Kubernetes, +GitOps, Talos, Argo CD, or any of their jargon — assume zero prior +exposure to this specific ecosystem, and never assume a term is obvious +just because it's common in cloud-native circles. They are not a beginner +at computers; don't explain what a terminal is. They may be a total +beginner at this stack; do explain what a Pod is, every time it's plausible +they haven't internalized it yet. + +## How to teach in this repo + +- **Why before how.** Before giving a command, say what problem it solves + and why this is the way to solve it — not just what to type. +- **Define every term on first use**, in the sentence you use it, even if + it's also in [`docs/glossary.md`](docs/glossary.md). Don't make them break + flow to go look it up. +- **Check understanding**, don't just narrate. After explaining something + non-trivial, ask a real question ("what do you think happens if you skip + the bootstrap step?") rather than "does that make sense?" — the latter + invites a reflexive yes. +- **Go in order.** The `docs/` files are numbered for a reason — each + assumes everything before it. If the user jumps ahead, it's fine to help, + but flag what they're skipping past and offer to backfill it. +- **Show real commands and real output**, never abstract description of + what a command "roughly" does. If you're not sure of the exact current + behavior, say so rather than guessing confidently. +- **"Ask Claude" checkpoints** are scattered through the docs on purpose — + when the user reaches one, treat it as an invitation to go deeper, not a + formality to acknowledge and move past. + +## Guided Bootstrap Mode + +When the user asks to be walked through setup — phrases like "walk me +through the checklist," "let's bootstrap this," "help me set this up +end to end" — drive [`docs/checklist.md`](docs/checklist.md) interactively: + +1. Work through it **one item at a time**, in order, by phase. +2. Before each item, briefly say what it does and why (one or two + sentences — the full explanation lives in the doc it's drawn from; link + it, don't paste it wholesale). +3. Give the exact command, with any placeholders (``, + `YOUR_GITHUB_USERNAME`) called out clearly so the user doesn't run it + unedited. +4. **Wait for the user to run it and report back** — the output, an error, + or just "done" — before moving to the next item. Don't dump the whole + phase at once. +5. If something fails, help debug it in place rather than plowing forward; + don't check a box that isn't actually true yet. +6. Periodically remind them of `docs/glossary.md` and the "Ask Claude" + checkpoints along the way — this mode shouldn't feel like a script being + read at them. +7. **Phase 5 (MetalLB) needs real information from the user, not + placeholders.** Before editing `apps/metallb/templates/ipaddresspool.yaml` + or `apps/demo/service.yaml`, actually ask: what does their LAN's subnet + look like, and what IP range does their router's DHCP server already + hand out? Pick a small unused slice outside that range for the MetalLB + pool, then get two more addresses from inside that pool — one for Argo + CD's UI, one for the demo app. Never fill these in with a guessed or + copied-from-the-docs address; a wrong one means real devices on their + network trying to share an IP. + +If the user hasn't asked for guided mode, don't impose it — answer what +they actually asked, just keep the teaching posture from above. + +## Keep `docs/network.md` current + +[`docs/network.md`](docs/network.md) is the running log of every real IP +address this cluster hands out and what's reachable there. It only stays +useful if it's actually current, so treat updating it as part of the +change itself, not an afterthought: + +- Whenever you (Claude) assign, learn, or change a real IP address for + something — walking the user through MetalLB's pool + ([`docs/05-metallb.md`](docs/05-metallb.md)), patching `argocd-server`, + helping add a new app with a `LoadBalancer` Service — update the table in + `docs/network.md` in the same breath. Don't wait to be asked. +- Every row needs a full, correct URL — scheme, IP, and port — not just a + bare address. Include the port number unless it's the protocol's default + (80 for `http://`, 443 for `https://`). If you're not sure what port a + Service actually listens on, check its manifest (`spec.ports`) rather + than guessing. +- If you notice the file has drifted from reality (an address in it no + longer matches `kubectl get svc -A`, or a Service is missing from it), + fix it on the spot and mention that you did. +- This file tracks real, user-specific addresses — never fill a row with a + placeholder or an address you haven't actually confirmed is live. + +## Suggested learning path + +Point new users here first if they seem unsure where to start: + +1. [`README.md`](README.md) — orientation +2. [`docs/00-what-is-all-this.md`](docs/00-what-is-all-this.md) — mental model, read before touching a terminal +3. [`docs/01-prerequisites.md`](docs/01-prerequisites.md) → [`08-growing-up.md`](docs/08-growing-up.md) — in order +4. [`docs/checklist.md`](docs/checklist.md) — condensed step-by-step reference once concepts are understood, or to drive Guided Bootstrap Mode +5. [`docs/glossary.md`](docs/glossary.md) — reference throughout, not read start to finish + +## Working on the repo itself (not just teaching from it) + +If asked to change manifests or docs rather than explain them: + +- Keep manifests **real and working**, never illustrative pseudo-YAML — + this repo's whole value is that everything in it actually runs. +- Every non-obvious field gets a comment explaining *why* it's there, not + just what it is — match the style already in `apps/`, `talos/`, and + `argocd/`. +- Keep the moving parts minimal. Ingress, cert-manager, and secrets + management are intentionally **documented, not shipped** (see + [`docs/08-growing-up.md`](docs/08-growing-up.md)) — don't add them to + make the repo "more complete" without being asked. MetalLB is the one + exception, shipped deliberately because bare metal has no other way to + give a Service a real IP address — don't take that as license to add + other addons the same way without being asked. +- This repo is **public**. Never write a plaintext secret, credential, or + private key into any file here. If a change would need one, stop and + point to the Sealed Secrets / SOPS guidance in + `docs/08-growing-up.md` instead of improvising. +- New apps follow the existing `apps/.yaml` + `apps//` pattern + — see [`docs/07-adding-new-apps.md`](docs/07-adding-new-apps.md) before + adding one. `apps/metallb/` is the reference example for a Helm-based + one. +- Every `apps/*.yaml` Application manifest needs its own `repoURL` fixed to + the user's fork — they don't inherit it from the app-of-apps. If you add + a new Application, make sure it carries the same placeholder-and-warning + pattern the others do, so it shows up in + `grep -rl YOUR_GITHUB_USERNAME .`. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d85a446 --- /dev/null +++ b/README.md @@ -0,0 +1,108 @@ +# GitOps Starter Pack + +A minimal, real, working GitOps setup for a single Kubernetes cluster — +built to teach, not just to run. If you've never touched Kubernetes, +GitOps, Talos, or Argo CD before, this is meant to get you from a bare +machine to a working cluster you actually understand, not just one you +copy-pasted into existence. + +## Who this is for + +You're comfortable with computers: the command line, SSH, servers, +networking, editing config files. You have **never** used Kubernetes, +GitOps, Argo CD, or Talos, and none of the jargon is familiar yet. This +repo assumes that, exactly. Every cloud-native term is defined in plain +English the first time it appears, and a full glossary lives at +[`docs/glossary.md`](docs/glossary.md). + +## The mental model, briefly + +Traditionally, you configure a server by SSHing in and changing things by +hand. Nothing records *why* it looks the way it does except your memory and +shell history. This repo takes a different approach: + +1. **Kubernetes** runs your applications, and you tell it the state you + want ("one copy of this running") rather than the steps to get there — + it keeps making that true, continuously. +2. **Talos Linux** is the operating system underneath — built for nothing + but running Kubernetes, configured the same declarative way, with no + SSH or shell to log into. +3. **Argo CD** watches this Git repository and keeps the cluster matching + whatever it finds here. You stop running `kubectl apply` by hand; you + edit a file, commit, push, and Argo CD applies it for you. +4. **App-of-apps** is how this repo is organized so that adding a new app + later is "add a file," never a new procedure. +5. **MetalLB** gives apps in this cluster real IP addresses on your own + network — something a cloud provider would normally do for you, and + bare metal doesn't have on its own. + +Git becomes the one place that's actually true. If you want the full +picture with diagrams before doing anything, start with +[`docs/00-what-is-all-this.md`](docs/00-what-is-all-this.md). + +## What's in here + +``` +talos/ → Talos machine config + bootstrap guide (the OS layer) +argocd/ → Argo CD install + the one manifest you apply by hand +apps/ → Everything Argo CD manages from here on — demo app, MetalLB, + and wherever you add new apps +docs/ → The numbered, progressive guide you should actually read +``` + +## Prerequisites + +- One machine (physical or VM) to become your cluster node +- `talosctl`, `kubectl`, and the `argocd` CLI installed locally +- A GitHub account, and your own fork of this repo + +Full detail, with install commands: [`docs/01-prerequisites.md`](docs/01-prerequisites.md). + +## The path through this repo + +Read these in order. Each is short — one concept per file. + +1. [`docs/00-what-is-all-this.md`](docs/00-what-is-all-this.md) — the mental model and architecture, before touching a terminal +2. [`docs/01-prerequisites.md`](docs/01-prerequisites.md) — install the tools, get a target machine ready +3. [`docs/02-talos-bootstrap.md`](docs/02-talos-bootstrap.md) — turn that machine into a Kubernetes node +4. [`docs/03-installing-argocd.md`](docs/03-installing-argocd.md) — install Argo CD into the cluster +5. [`docs/04-app-of-apps.md`](docs/04-app-of-apps.md) — apply the one manifest that hands control to Git +6. [`docs/05-metallb.md`](docs/05-metallb.md) — give apps real IP addresses on your network +7. [`docs/06-your-first-change.md`](docs/06-your-first-change.md) — edit a file, push, watch it deploy itself +8. [`docs/07-adding-new-apps.md`](docs/07-adding-new-apps.md) — add something of your own +9. [`docs/08-growing-up.md`](docs/08-growing-up.md) — next steps: more nodes, ingress, secrets management +10. [`docs/glossary.md`](docs/glossary.md) — every term, plain English, for reference throughout +11. [`docs/network.md`](docs/network.md) — the running log of every real IP address in your cluster, kept current as you go + +Prefer a single condensed list you can work through step by step with +Claude, checking items off as you go? Use +[`docs/checklist.md`](docs/checklist.md) instead — see the next section. + +## Learning this with Claude + +This repo is meant to be read with an AI assistant open alongside it. The +root [`CLAUDE.md`](CLAUDE.md) primes Claude to act as a patient tutor for +this exact repo — teaching concepts in order, explaining *why* before +*how*, and checking your understanding rather than just handing you +commands. Throughout the docs, you'll see call-outs like: + +> **Ask Claude:** ... + +Take those. They mark places where pausing to ask a follow-up question +pays off more than plowing ahead. + +If you'd rather be walked through the whole bootstrap interactively, +start a conversation with Claude in this repo and say something like +*"walk me through the checklist in docs/checklist.md"* — it will go +step by step, wait for your output at each one, and help you debug before +moving on. + +## A note on scope + +This repo deliberately does **not** ship ingress, TLS/cert-manager, or +secrets management in v1 — they're real next steps, documented in +[`docs/08-growing-up.md`](docs/08-growing-up.md), left out so the core +loop stays small enough to fully understand first. This repo is also +**public** — never commit plaintext secrets, credentials, or private keys +to it; see that same doc for how to handle secrets properly once you need +them. diff --git a/apps/demo.yaml b/apps/demo.yaml new file mode 100644 index 0000000..72b7262 --- /dev/null +++ b/apps/demo.yaml @@ -0,0 +1,52 @@ +# This is an Argo CD "Application" — not a Kubernetes-native concept, but a +# Custom Resource that Argo CD adds to the cluster (see docs/glossary.md: +# "CRD"). It tells Argo CD: "watch this path in this Git repo, and keep the +# cluster's demo namespace matching whatever you find there." +# +# You never apply this file yourself. The app-of-apps +# (argocd/app-of-apps/app-of-apps.yaml) watches this apps/ directory, and +# Argo CD applies every Application it finds here automatically — including +# this one. That's the whole "app-of-apps" trick: one Application manages a +# folder of other Applications. +# +# --- Manifest-based vs. Helm-based apps --- +# This app points at a plain folder of Kubernetes manifests (apps/demo/). +# An app can instead point at a Helm chart — including an umbrella chart +# whose Chart.yaml declares a `dependencies:` entry on a third-party chart +# (e.g. a database or ingress controller from a public chart repo). Argo CD +# detects a Helm chart automatically when it finds a Chart.yaml at the +# source path, resolves its dependencies, and syncs the rendered output just +# like any other app. See docs/07-adding-new-apps.md for a worked example — +# the demo app deliberately stays plain manifests so the GitOps loop is easy +# to read end to end with no templating in the way. +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: demo + namespace: argocd +spec: + project: default + + source: + repoURL: https://github.com/YOUR_GITHUB_USERNAME/k8s-starter-pack.git + targetRevision: main + path: apps/demo + + destination: + server: https://kubernetes.default.svc + namespace: demo + + syncPolicy: + automated: + # prune: delete cluster objects whose manifest was removed from Git. + # selfHeal: if someone changes the cluster by hand (e.g. `kubectl edit`), + # Argo CD reverts it back to match Git on the next reconciliation pass. + # Together these are what make Git the actual source of truth instead + # of just a suggestion. + prune: true + selfHeal: true + syncOptions: + # Creates the `demo` namespace automatically if it's missing, instead + # of failing sync. We also declare it explicitly in namespace.yaml for + # visibility, so this is a safety net more than a requirement here. + - CreateNamespace=true diff --git a/apps/demo/configmap.yaml b/apps/demo/configmap.yaml new file mode 100644 index 0000000..0b6fc35 --- /dev/null +++ b/apps/demo/configmap.yaml @@ -0,0 +1,27 @@ +# A ConfigMap is just a bag of key/value data — in this case, a small HTML +# file — that lives in the cluster as its own object, separate from the +# Pod that uses it. We mount it into the nginx container below instead of +# baking the page into a custom container image. +# +# Why this matters for the GitOps story: this is the file you'll edit in +# docs/06-your-first-change.md. Change the text here, commit, push, and +# watch Argo CD notice the difference in Git and push it into the cluster +# for you — no `kubectl` command involved. That's the whole loop, proven +# with one line of HTML. +apiVersion: v1 +kind: ConfigMap +metadata: + name: demo-page + namespace: demo +data: + index.html: | + + + It works! + +

It works!

+

This page is being served from a Kubernetes cluster that Argo CD + deployed straight from Git. Edit apps/demo/configmap.yaml, + commit, push, and refresh this page.

+ + diff --git a/apps/demo/deployment.yaml b/apps/demo/deployment.yaml new file mode 100644 index 0000000..1e347a5 --- /dev/null +++ b/apps/demo/deployment.yaml @@ -0,0 +1,38 @@ +# A Deployment describes a desired state ("I want 1 copy of nginx running, +# serving this ConfigMap's index.html") and hands it to Kubernetes, whose +# job is to continuously make reality match that description. If the Pod +# crashes, Kubernetes restarts it. If the node dies, Kubernetes (given more +# than one node — see docs/08-growing-up.md) reschedules it elsewhere. +# You never tell Kubernetes "start a container" directly; you describe the +# end state and it does the work. This is what "declarative" means. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: demo + namespace: demo +spec: + replicas: 1 + selector: + # Tells the Deployment which Pods belong to it. Must match + # spec.template.metadata.labels below, or Kubernetes won't know + # this Deployment owns the Pods it creates. + matchLabels: + app: demo + template: + metadata: + labels: + app: demo + spec: + containers: + - name: nginx + image: nginx:1.27-alpine + ports: + - containerPort: 80 + volumeMounts: + # Mounts the ConfigMap's index.html over nginx's default page. + - name: page + mountPath: /usr/share/nginx/html + volumes: + - name: page + configMap: + name: demo-page diff --git a/apps/demo/namespace.yaml b/apps/demo/namespace.yaml new file mode 100644 index 0000000..b067384 --- /dev/null +++ b/apps/demo/namespace.yaml @@ -0,0 +1,13 @@ +# A Namespace is a way to group related resources together inside the +# cluster, like a folder. It doesn't do much on its own — its real job is +# letting you keep "demo" separate from other apps you add later, so you can +# manage, view, and delete them independently (e.g. `kubectl -n demo get pods`). +# +# We create this explicitly, as its own file, rather than letting Argo CD +# invent it implicitly. That way it shows up in Git like everything else — +# if you ever wonder "why does this namespace exist?", the answer is always +# "because a file in this repo says so." +apiVersion: v1 +kind: Namespace +metadata: + name: demo diff --git a/apps/demo/service.yaml b/apps/demo/service.yaml new file mode 100644 index 0000000..aa5be09 --- /dev/null +++ b/apps/demo/service.yaml @@ -0,0 +1,32 @@ +# A Service gives a stable way to reach a set of Pods, even as individual +# Pods are created and destroyed and get new internal IP addresses over +# time. This Service selects any Pod labelled app: demo (see +# deployment.yaml) and forwards traffic to it. +# +# type: LoadBalancer asks the cluster for a real, dedicated IP address that +# anything on your network can reach directly. On a cloud provider that +# would provision an actual cloud load balancer; on bare metal there's +# nothing to provision it, which is exactly the gap MetalLB +# (apps/metallb.yaml) fills — it watches for LoadBalancer Services and +# hands them an address from the pool configured in +# apps/metallb/templates/ipaddresspool.yaml. Until that pool exists, this +# Service's address will show as ; see docs/05-metallb.md. +apiVersion: v1 +kind: Service +metadata: + name: demo + namespace: demo + annotations: + # Requests this specific address from MetalLB's pool, so it stays + # predictable across rebuilds instead of getting whatever's next free. + # Must fall inside the range in + # apps/metallb/templates/ipaddresspool.yaml, or MetalLB will never + # satisfy the request. + metallb.universe.tf/loadBalancerIPs: "192.168.1.241" # EDIT ME +spec: + type: LoadBalancer + selector: + app: demo + ports: + - port: 80 + targetPort: 80 diff --git a/apps/metallb.yaml b/apps/metallb.yaml new file mode 100644 index 0000000..0311e99 --- /dev/null +++ b/apps/metallb.yaml @@ -0,0 +1,41 @@ +# MetalLB, installed like any other app: an Application pointing at +# apps/metallb, a Helm chart (see apps/metallb/Chart.yaml) that both pulls +# in the upstream MetalLB chart AND carries our own IPAddressPool and +# L2Advertisement as extra templates (apps/metallb/templates/). Once this +# syncs, the cluster can hand out real IP addresses to Services of type +# LoadBalancer — something bare-metal Kubernetes doesn't do on its own +# (that's normally a cloud provider's job). See docs/05-metallb.md for the +# full explanation and the setup questions you'll need to answer before +# this is actually useful. +# +# Everything — MetalLB's own resources (including its CRDs) and our two +# custom resources that depend on them — comes from this one Application, +# in one sync. The ordering that makes that safe lives on the individual +# resources in apps/metallb/templates/, via the +# argocd.argoproj.io/sync-wave annotation: MetalLB's chart resources apply +# at the default wave ("0"), and our IPAddressPool/L2Advertisement are +# marked wave "1" so Argo CD waits for MetalLB to be synced and healthy +# (CRDs registered, controller and speaker running) before applying them. +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: metallb + namespace: argocd +spec: + project: default + + source: + repoURL: https://github.com/YOUR_GITHUB_USERNAME/k8s-starter-pack.git + targetRevision: main + path: apps/metallb + + destination: + server: https://kubernetes.default.svc + namespace: metallb-system + + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/apps/metallb/Chart.yaml b/apps/metallb/Chart.yaml new file mode 100644 index 0000000..8583bd4 --- /dev/null +++ b/apps/metallb/Chart.yaml @@ -0,0 +1,21 @@ +# An "umbrella" chart: it declares a dependency on the upstream MetalLB +# chart below, AND carries two templates of its own +# (apps/metallb/templates/) — the IPAddressPool and L2Advertisement that +# configure it. Argo CD detects this as a Helm source because a Chart.yaml +# exists at this path, resolves the dependency, and syncs the combined +# rendered output as one Application — exactly like any other app in +# apps/. This is the real, working example of the Helm-based app pattern +# mentioned in docs/07-adding-new-apps.md: use it as a template if you want +# to bring in another third-party chart later. +apiVersion: v2 +name: metallb-app +description: Installs MetalLB from the upstream chart, for this repo's bare-metal LoadBalancer support +version: 0.1.0 +dependencies: + - name: metallb + # A version *range* rather than an exact pin, so this doesn't silently + # go stale. For a fully reproducible install, check + # https://artifacthub.io/packages/helm/metallb/metallb for the current + # release and pin an exact version here instead. + version: ">=0.14.3" + repository: https://metallb.github.io/metallb diff --git a/apps/metallb/templates/ipaddresspool.yaml b/apps/metallb/templates/ipaddresspool.yaml new file mode 100644 index 0000000..a61c9b3 --- /dev/null +++ b/apps/metallb/templates/ipaddresspool.yaml @@ -0,0 +1,29 @@ +# Tells MetalLB which IP addresses it's allowed to hand out to Services. +# Pick a small range on your LAN that nothing else will ever assign — +# especially your router's DHCP server. MetalLB doesn't coordinate with +# your router or DHCP server at all; it just answers network requests for +# whatever address a Service claims from this range. If it overlaps your +# DHCP range, you'll eventually get two devices claiming the same IP. +# +# >> Ask Claude to help you pick this range before editing it — it needs +# to know your LAN's subnet and DHCP range, which it can't guess. +# A typical home LAN answer looks like "my router hands out +# 192.168.1.100–192.168.1.199, so let's use 192.168.1.240–192.168.1.250." +# +# This file lives under apps/metallb/templates/ alongside MetalLB's own +# Helm-installed resources, rather than as a separate app, because it +# depends on the CRD that chart installs — Kubernetes needs to know what +# an IPAddressPool *is* before it can accept one. The sync-wave annotation +# is what keeps that order correct: Argo CD applies wave "0" (this chart's +# own resources, including the CRDs) first, waits for it to be healthy, +# then applies wave "1" (this file and l2advertisement.yaml). +apiVersion: metallb.io/v1beta1 +kind: IPAddressPool +metadata: + name: default-pool + namespace: metallb-system + annotations: + argocd.argoproj.io/sync-wave: "1" +spec: + addresses: + - 192.168.1.240-192.168.1.250 # EDIT ME — replace with your chosen range diff --git a/apps/metallb/templates/l2advertisement.yaml b/apps/metallb/templates/l2advertisement.yaml new file mode 100644 index 0000000..c3aa374 --- /dev/null +++ b/apps/metallb/templates/l2advertisement.yaml @@ -0,0 +1,19 @@ +# L2Advertisement is what actually makes addresses from the pool reachable: +# it tells MetalLB to answer ARP requests for whichever address a Service +# was given, from whichever node currently hosts that Service. This is +# "Layer 2 mode" — the simplest of MetalLB's two modes, and the right +# choice here since it needs no cooperation from your router (the +# alternative, BGP mode, does — worth knowing exists if you outgrow a +# single L2 segment, not needed for this setup). +# Same sync-wave reasoning as ipaddresspool.yaml — deferred to wave "1" so +# it applies after MetalLB's own chart resources are up. +apiVersion: metallb.io/v1beta1 +kind: L2Advertisement +metadata: + name: default + namespace: metallb-system + annotations: + argocd.argoproj.io/sync-wave: "1" +spec: + ipAddressPools: + - default-pool diff --git a/apps/metallb/values.yaml b/apps/metallb/values.yaml new file mode 100644 index 0000000..409ba4f --- /dev/null +++ b/apps/metallb/values.yaml @@ -0,0 +1,9 @@ +# Values for the upstream MetalLB chart, nested under `metallb:` (the +# dependency's name in Chart.yaml — Helm nests subchart values under the +# subchart's name automatically). Empty by default: MetalLB's own chart +# defaults are sane for a small single-cluster setup. Add overrides here if +# you ever need them, e.g.: +# +# metallb: +# speaker: +# tolerations: [] diff --git a/argocd/README.md b/argocd/README.md new file mode 100644 index 0000000..2209e80 --- /dev/null +++ b/argocd/README.md @@ -0,0 +1,133 @@ +# Installing Argo CD — command reference + +Companion to [`docs/03-installing-argocd.md`](../docs/03-installing-argocd.md) +and [`docs/04-app-of-apps.md`](../docs/04-app-of-apps.md). Read those for the +why; this is the exact commands. + +Run these once `kubectl get nodes` shows your node `Ready` (see +[`talos/README.md`](../talos/README.md)). + +## 1. Install Argo CD + +```sh +kubectl create namespace argocd +kubectl apply --server-side -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml +``` + +`--server-side` matters here, not just as a nice-to-have: one of Argo CD's +CRDs (`applicationsets.argoproj.io`) is large enough that a normal +(client-side) `kubectl apply` fails outright — it tries to stash the whole +manifest into an annotation for its 3-way-merge bookkeeping, and that +annotation hits Kubernetes' size limit. Server-side apply has the API +server do that diffing instead, so this limit doesn't apply. + +This is an **imperative** install: a plain `kubectl apply` of a manifest +bundle that the Argo CD project maintains, rather than a Helm chart or +operator. It's the officially documented quickstart path, and it means +Argo CD itself isn't part of your GitOps loop — you install it once, by +hand, and *it* then manages everything else declaratively. Bootstrapping +has to start somewhere outside the loop; this is that starting point. + +The `stable` in that URL is a moving tag maintained by the Argo CD project +that always points at the latest stable release — convenient here, but not +reproducible: what it installs today may differ from what it installs in +six months. For a production setup, pin an explicit version instead, e.g. +`.../v2.13.0/manifests/install.yaml` — see [releases](https://github.com/argoproj/argo-cd/releases) +for current tags. + +Wait for everything to come up: + +```sh +kubectl -n argocd wait --for=condition=Available --timeout=300s deployment --all +``` + +## 2. Get the initial admin password + +```sh +kubectl -n argocd get secret argocd-initial-admin-secret \ + -o jsonpath="{.data.password}" | base64 -d; echo +``` + +This secret is auto-generated on first install. Save the password +somewhere; you'll change it in a moment. + +## 3. Reach the UI and CLI + +Argo CD doesn't expose itself outside the cluster by default. For this +learning setup, forward its port to your machine: + +```sh +kubectl -n argocd port-forward svc/argocd-server 8080:443 +``` + +Leave that running in its own terminal. In another terminal: + +```sh +argocd login localhost:8080 --username admin --password --insecure +``` + +`--insecure` here means "skip TLS certificate verification" — the server's +certificate is self-signed by default and there's nothing on the other end +but your own port-forward, so this is fine for local access. It is **not** +something to carry into a setup reachable over the real network. + +Open `https://localhost:8080` in a browser to see the same thing visually. + +Optional but recommended — replace the initial password: + +```sh +argocd account update-password +``` + +## 4. Hand the cluster to Git + +[`app-of-apps/app-of-apps.yaml`](app-of-apps/app-of-apps.yaml) is the one +manifest in this whole repo you apply by hand — everything after it, Argo +CD applies for you. First, every Application manifest needs its own +`repoURL` pointed at your fork (they don't inherit it from each other): + +```sh +grep -rl YOUR_GITHUB_USERNAME . +``` + +Edit `spec.source.repoURL` in each file that command finds, commit, and +push — the child Applications are read from Git, not your local disk, so +this has to land in your fork before the next step. Then: + +```sh +kubectl apply -f argocd/app-of-apps/app-of-apps.yaml +``` + +Watch it take over: + +```sh +argocd app get app-of-apps +argocd app list +``` + +You should see `app-of-apps` sync, then `demo` and `metallb` Applications +appear underneath it, then sync themselves. See +[`docs/04-app-of-apps.md`](../docs/04-app-of-apps.md) for what's happening +here and [`docs/checklist.md`](../docs/checklist.md) for the full guided +path. + +## 5. Give the UI a real address (MetalLB) + +Once `metallb` (above) is `Synced`/`Healthy` and you've configured its +address pool (see [`docs/05-metallb.md`](../docs/05-metallb.md)), you can +retire the port-forward from step 3 and give Argo CD's UI a permanent +address instead. This one change is applied directly, not through Git — +Argo CD's own install isn't managed by this repo's GitOps loop: + +```sh +kubectl -n argocd patch svc argocd-server --type merge -p \ + '{"spec":{"type":"LoadBalancer"},"metadata":{"annotations":{"metallb.universe.tf/loadBalancerIPs":""}}}' +``` + +```sh +kubectl -n argocd get svc argocd-server +``` + +Once `EXTERNAL-IP` shows your chosen address, `https://` +works directly, from any machine on your network — no port-forward +required. diff --git a/argocd/app-of-apps/app-of-apps.yaml b/argocd/app-of-apps/app-of-apps.yaml new file mode 100644 index 0000000..605798a --- /dev/null +++ b/argocd/app-of-apps/app-of-apps.yaml @@ -0,0 +1,47 @@ +# The app-of-apps. This is the ONE manifest in this entire repo that you +# apply by hand, one time, with `kubectl apply -f`. It's the seed: after +# this exists, Argo CD takes over and everything else in the apps/ folder +# gets applied automatically, because this Application's job is to watch +# that folder. +# +# Before applying this file, edit spec.source.repoURL below to point at YOUR +# fork of this repo (Argo CD needs to clone it, so it must be a real URL it +# can reach — not this template's URL). +# +# >> Ask Claude: "why does the app-of-apps live in argocd/app-of-apps +# instead of inside apps/ with everything else?" before moving on — the +# answer (avoiding a directory managing itself) is worth understanding. +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: app-of-apps + namespace: argocd +spec: + project: default + + source: + repoURL: https://github.com/YOUR_GITHUB_USERNAME/k8s-starter-pack.git + targetRevision: main + path: apps + directory: + # recurse: false is deliberate. apps/ contains both Application + # manifests (apps/demo.yaml) AND subfolders full of the actual + # workload manifests they point to (apps/demo/*.yaml). Without this, + # Argo CD would try to apply everything inside apps/demo/ directly + # under the app-of-apps too — duplicating work the demo Application + # already does, and blurring which Application actually owns what. + # recurse: false means "only look at files directly in apps/, ignore + # subfolders" — so the app-of-apps only ever sees the Application + # manifests, never the workloads themselves. + recurse: false + + destination: + server: https://kubernetes.default.svc + namespace: argocd + + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/docs/00-what-is-all-this.md b/docs/00-what-is-all-this.md new file mode 100644 index 0000000..655504a --- /dev/null +++ b/docs/00-what-is-all-this.md @@ -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. diff --git a/docs/01-prerequisites.md b/docs/01-prerequisites.md new file mode 100644 index 0000000..6f69842 --- /dev/null +++ b/docs/01-prerequisites.md @@ -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). diff --git a/docs/02-talos-bootstrap.md b/docs/02-talos-bootstrap.md new file mode 100644 index 0000000..15c0da4 --- /dev/null +++ b/docs/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). diff --git a/docs/03-installing-argocd.md b/docs/03-installing-argocd.md new file mode 100644 index 0000000..9cd6659 --- /dev/null +++ b/docs/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). diff --git a/docs/04-app-of-apps.md b/docs/04-app-of-apps.md new file mode 100644 index 0000000..da83390 --- /dev/null +++ b/docs/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 `` — + 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 `` — 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. diff --git a/docs/05-metallb.md b/docs/05-metallb.md new file mode 100644 index 0000000..5b0513b --- /dev/null +++ b/docs/05-metallb.md @@ -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 +`` 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 `` 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 ``. +Visit `http://` 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":""}}}' +``` + +Replace `` 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://` 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). diff --git a/docs/06-your-first-change.md b/docs/06-your-first-change.md new file mode 100644 index 0000000..0814160 --- /dev/null +++ b/docs/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 `

` 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://` (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":"

hand-edited, not from Git

"}}' +``` + +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. diff --git a/docs/07-adding-new-apps.md b/docs/07-adding-new-apps.md new file mode 100644 index 0000000..9293ad0 --- /dev/null +++ b/docs/07-adding-new-apps.md @@ -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//` +- [ ] New `apps/.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. diff --git a/docs/08-growing-up.md b/docs/08-growing-up.md new file mode 100644 index 0000000..a94bd5e --- /dev/null +++ b/docs/08-growing-up.md @@ -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 --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. diff --git a/docs/checklist.md b/docs/checklist.md new file mode 100644 index 0000000..e45692d --- /dev/null +++ b/docs/checklist.md @@ -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 ``, +`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://: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 --file _out/controlplane.yaml` run, node rebooted +- [ ] `talosctl bootstrap --nodes ` run — **exactly once** +- [ ] `talosctl kubeconfig --nodes .` 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 --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 `` — 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://` +- [ ] `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://` with no port-forward running + +## Phase 6 — Prove the loop ([`06`](06-your-first-change.md)) + +- [ ] Edited the `

` 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). diff --git a/docs/glossary.md b/docs/glossary.md new file mode 100644 index 0000000..a27ee76 --- /dev/null +++ b/docs/glossary.md @@ -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. diff --git a/docs/network.md b/docs/network.md new file mode 100644 index 0000000..e68d03b --- /dev/null +++ b/docs/network.md @@ -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 | +|---|---|---|---|---|---| +| `` | Talos node — Kubernetes API server | — | 6443 | `https://: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` diff --git a/talos/README.md b/talos/README.md new file mode 100644 index 0000000..9eb3597 --- /dev/null +++ b/talos/README.md @@ -0,0 +1,123 @@ +# Talos bootstrap — command reference + +This is the detailed, copy-pasteable companion to +[`docs/02-talos-bootstrap.md`](../docs/02-talos-bootstrap.md). Read that file +first for the explanations — this one is here so you have every exact +command in one place once you understand what they do. + +Throughout, replace `` with your machine's real IP address. If it's +a VM, give it a DHCP reservation (a fixed IP tied to its MAC address) in your +router or hypervisor before you start, so the address never changes under +the cluster. Talos bakes the IP into certificates during setup, and changing +it later means starting over. + +All commands assume you're running them from the root of this repo. + +## 1. Generate machine configuration + +```sh +talosctl gen config gitops-starter https://:6443 \ + --output-dir _out \ + --config-patch-control-plane @talos/patch-controlplane.yaml +``` + +This writes three files into `_out/` (already covered by this repo's +`.gitignore` — they contain private keys and must never be committed): + +- `controlplane.yaml` — the full machine configuration for a control-plane + node, with our single-node patch baked in. +- `worker.yaml` — a worker machine configuration. Unused for now (single + node), kept for when you follow docs/08-growing-up.md. +- `talosconfig` — *your* client credentials for talking to the Talos API on + this cluster (equivalent in spirit to a kubeconfig, but for Talos itself + rather than Kubernetes). + +`gitops-starter` is the cluster name — an arbitrary label baked into the +generated certificates and config, used to tell clusters apart if you ever +manage more than one. `https://:6443` is the cluster's Kubernetes +API endpoint: port 6443 is the standard Kubernetes API server port, and this +same address gets written into the certificates and your future kubeconfig. + +## 2. Point talosctl at this cluster + +```sh +export TALOSCONFIG=_out/talosconfig +talosctl config endpoint +talosctl config node +``` + +`endpoint` is which machine talosctl *talks to*. `node` is which machine a +command *applies to*. On a single-node cluster they're the same address, but +Talos always keeps the two concepts separate, because on a multi-node +cluster you often want to ask one node about another. + +## 3. Apply the configuration + +Boot the target machine from the Talos ISO (see +[docs/01-prerequisites.md](../docs/01-prerequisites.md) for where to get it). +It boots into "maintenance mode" — alive enough to accept a configuration, +with nothing installed yet. Confirm it's reachable, then apply: + +```sh +talosctl get disks --insecure --nodes +``` + +Note the disk name you want Talos installed to (e.g. `/dev/sda`). Then: + +```sh +talosctl apply-config --insecure \ + --nodes \ + --file _out/controlplane.yaml +``` + +`--insecure` is required for this one command only: the node has no +certificates yet, so talosctl can't authenticate to it normally. This is +the one moment during setup where you're trusting the network — do this on +a network you control. The node will reboot, install Talos to disk, and +come back up running the configuration you gave it — now *with* +certificates, so every command after this uses the secure path. + +## 4. Bootstrap etcd + +Talos is installed and Kubernetes components are starting, but the cluster +has no data store yet. This one-time command initializes it: + +```sh +talosctl bootstrap --nodes +``` + +Run this exactly once per cluster, against exactly one control-plane node, +even if you later add more. Running it again on an existing cluster can +corrupt it. + +## 5. Get your kubeconfig + +```sh +talosctl kubeconfig --nodes . +``` + +Downloads a kubeconfig into the current directory (also gitignored — +it grants full cluster access). Merge it with `~/.kube/config` or point +`KUBECONFIG` at it directly: + +```sh +export KUBECONFIG=$PWD/kubeconfig +``` + +## 6. Verify + +```sh +kubectl get nodes +``` + +Expect to see one node, `Ready`, within a couple of minutes (it takes a +moment for the default CNI — Talos ships Flannel out of the box — to come +up and clear the node's networking checks). + +``` +NAME STATUS ROLES AGE VERSION +talos-... Ready control-plane 90s v1.3x.x +``` + +If it's stuck on `NotReady`, give it another minute before troubleshooting — +first boot does a lot of one-time work. diff --git a/talos/patch-controlplane.yaml b/talos/patch-controlplane.yaml new file mode 100644 index 0000000..29c61d6 --- /dev/null +++ b/talos/patch-controlplane.yaml @@ -0,0 +1,15 @@ +# A patch applied on top of the controlplane.yaml that `talosctl gen config` +# produces (see talos/README.md and docs/02-talos-bootstrap.md for the full +# command). Talos's generated defaults assume a multi-node cluster where +# control-plane nodes only run the Kubernetes control plane itself (API +# server, scheduler, etcd, etc.) and never your actual workloads. For a +# single-node learning cluster, that would mean nothing can ever be +# scheduled — there's no other node to run it on. +# +# This one field undoes that restriction, letting the single node be both +# the control plane AND a worker. +# +# >> Ask Claude: "why does Kubernetes normally keep workloads off control +# plane nodes?" if you want the reasoning before you turn it off. +cluster: + allowSchedulingOnControlPlanes: true