Files
vps-k8s/docs/04-app-of-apps.md
T
Maxwell MorrisandClaude Sonnet 5 110bd97cca Move to a three-node cluster with a Talos VIP; add Longhorn
Every node is now both control plane and worker, giving real tolerance
for losing any single machine (etcd quorum needs 3, not 1). A Talos VIP
gives the cluster one stable address regardless of which node answers.

Also pre-installs Longhorn for replicated storage, backed by a Talos
UserVolume on each node's second disk (>=900GB, non-system) rather than
the default path — mirroring the pattern from the homescale repo.

Docs renumbered to make room for 06-longhorn.md; checklist, glossary,
network map, and CLAUDE.md's guided-bootstrap instructions updated to
match, including asking the user for node IPs, VIP, and disk info before
generating any Talos config.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 18:25:38 +01:00

5.5 KiB

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/) 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 apps/longhorn.yaml  (more, later)
        │               │                │
        ▼               ▼                ▼
   apps/demo/*.yaml  apps/metallb/*   apps/longhorn/*    (manifests / Helm charts)

Four layers wide, each watching the next. You only ever touch the bottom two — the app-of-apps is set-and-forget. Notice apps/metallb.yaml and apps/longhorn.yaml are already here — see docs/05-metallb.md and docs/06-longhorn.md right after this one.

Why apps/ isn't recursive

Look at 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, apps/longhorn.yaml) and subfolders holding the real workload manifests those Applications point to (apps/demo/*.yaml, apps/metallb/*, apps/longhorn/*). Non-recursive means the app-of-apps only ever looks at files directly inside apps/, never inside its subfolders — so it only ever sees Application definitions, and leaves the actual workload manifests to the Application that owns them. Without this, the app-of-apps would try to apply raw Deployments and Helm chart files itself, duplicating and conflicting with what those Applications are already doing.

Every Application needs its own repoURL

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:

grep -rl YOUR_GITHUB_USERNAME .

You should see four files: argocd/app-of-apps/app-of-apps.yaml, apps/demo.yaml, apps/metallb.yaml, and apps/longhorn.yaml. Edit spec.source.repoURL in each to point at your fork.

Steps

  1. Fix repoURL in all four files (above).
  2. Commit and push — this part matters more than it looks: the app-of-apps reads apps/demo.yaml, apps/metallb.yaml, and apps/longhorn.yaml from Git, not from your local edits, so they have to be pushed before you apply anything.
    git add argocd/app-of-apps/app-of-apps.yaml apps/demo.yaml apps/metallb.yaml apps/longhorn.yaml
    git commit -m "Point Applications at my fork"
    git push
    
  3. Apply the app-of-apps — the one manifest you apply by hand:
    kubectl apply -f argocd/app-of-apps/app-of-apps.yaml
    
  4. Watch it sync, then watch demo, metallb, and longhorn appear underneath it:
    argocd app list
    
    You should see app-of-apps, demo, metallb, and longhorn, all Synced and Healthy within a few minutes (MetalLB and Longhorn's own installs take longer than the demo app — Longhorn especially, since it has to find and prepare a disk on every node).
  5. Confirm the demo app is actually running:
    kubectl -n demo get pods,svc
    
    You'll notice the Service's EXTERNAL-IP column says <pending> — that's expected. The demo app is a LoadBalancer Service, and nothing hands those out real addresses on bare metal until MetalLB is configured, which is exactly the next doc.

If app-of-apps, demo, metallb, and longhorn all show Synced/Healthy, the loop — Git → Argo CD → cluster — is already proven end to end; you just can't see the demo app in a browser yet.

Ask Claude: "walk me through exactly what happened, in order, from the moment I ran kubectl apply -f argocd/app-of-apps/app-of-apps.yaml to these four Applications existing." Making sure you can narrate that chain yourself is the real checkpoint for this doc — everything from here on is a variation on it.

Checklist

  • repoURL fixed in all four files listed above
  • Fix committed and pushed
  • app-of-apps applied and shows Synced/Healthy
  • demo, metallb, and longhorn Applications appeared automatically, all Synced/Healthy
  • kubectl -n demo get pods,svc shows the demo app's Pod running (Service EXTERNAL-IP still <pending> — expected for now)

Continue to docs/05-metallb.md to give the demo app — and Argo CD's own UI — a real address.