Initial commit: GitOps Starter Pack
A teaching-first, single-cluster GitOps setup: Talos Linux + Argo CD with an app-of-apps layout, MetalLB pre-installed for real LoadBalancer IPs on bare metal, a demo app, and a progressive docs/ walkthrough with a CLAUDE.md tutor persona and guided bootstrap checklist. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+10
@@ -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
|
||||
@@ -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 (`<NODE_IP>`,
|
||||
`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/<name>.yaml` + `apps/<name>/` 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 .`.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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: |
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head><title>It works!</title></head>
|
||||
<body style="font-family: sans-serif; text-align: center; margin-top: 10%;">
|
||||
<h1>It works!</h1>
|
||||
<p>This page is being served from a Kubernetes cluster that Argo CD
|
||||
deployed straight from Git. Edit <code>apps/demo/configmap.yaml</code>,
|
||||
commit, push, and refresh this page.</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 <pending>; 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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: []
|
||||
@@ -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 <password-from-above> --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":"<ARGOCD_IP>"}}}'
|
||||
```
|
||||
|
||||
```sh
|
||||
kubectl -n argocd get svc argocd-server
|
||||
```
|
||||
|
||||
Once `EXTERNAL-IP` shows your chosen address, `https://<that-address>`
|
||||
works directly, from any machine on your network — no port-forward
|
||||
required.
|
||||
@@ -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
|
||||
@@ -0,0 +1,149 @@
|
||||
# 00 — What is all this?
|
||||
|
||||
You're comfortable with servers, SSH, config files, and the command line.
|
||||
You've probably never touched Kubernetes. This doc gives you the mental
|
||||
model before you type a single command — everything after this builds on
|
||||
it.
|
||||
|
||||
## The problem this stack solves
|
||||
|
||||
Say you run a few services on a server. The traditional way: SSH in,
|
||||
install things, edit config files by hand, restart services, and remember
|
||||
what you did (or don't, and regret it later). It works, but nothing
|
||||
records *why* the server looks the way it does — the truth lives only on
|
||||
that one machine, in your head, and in shell history if you're lucky.
|
||||
|
||||
This stack solves that by making a **Git repository** the single source of
|
||||
truth for "what should be running," and using software to continuously make
|
||||
the real cluster match it. You stop making changes by hand. You edit a file,
|
||||
push it, and a robot applies it for you — the same way every time, with a
|
||||
record of every change in `git log`.
|
||||
|
||||
## The five pieces, in plain terms
|
||||
|
||||
**Kubernetes** is software that runs on one or more machines and manages
|
||||
containers for you: starts them, restarts them if they crash, and moves
|
||||
them to a different machine if one dies (once you have more than one — see
|
||||
[`docs/08-growing-up.md`](08-growing-up.md)). You describe the end state you
|
||||
want ("run one copy of this"); Kubernetes' job is to make that true and keep
|
||||
it true. This is what people mean by **declarative**: you state the
|
||||
destination, not the steps to get there.
|
||||
|
||||
**Talos Linux** is the operating system your machine runs. It's built for
|
||||
exactly one purpose — running Kubernetes — and nothing else. There's no SSH,
|
||||
no shell, no package manager to fiddle with by hand. You configure it by
|
||||
sending it a config file over the network (a "machine config"), the same
|
||||
declarative idea as Kubernetes itself, just one layer lower. Less to
|
||||
install, less to secure, less that can drift out of a known state.
|
||||
|
||||
**Argo CD** is the piece that watches a Git repository — this one — and
|
||||
continuously makes the cluster match whatever it finds there. This is
|
||||
called **GitOps**: Git isn't just where your config *lives*, it's where
|
||||
change *happens*. You don't run `kubectl apply` for day-to-day changes;
|
||||
you edit a file, commit, push, and Argo CD does the applying.
|
||||
|
||||
**App of apps** is the pattern this repo uses to organize what Argo CD
|
||||
manages. Rather than one Application resource pointing at everything, one
|
||||
Application — the app-of-apps — watches a folder (`apps/`) full of *other*
|
||||
Application definitions, each pointing at its own small piece. Add a new
|
||||
app by adding a new file — see
|
||||
[`docs/07-adding-new-apps.md`](07-adding-new-apps.md).
|
||||
|
||||
**MetalLB** solves a specific problem: on a cloud provider, asking for a
|
||||
public IP address for a service is one API call away; on your own bare
|
||||
metal machine, nothing hands those out automatically. MetalLB runs inside
|
||||
the cluster and fills that gap, so apps here can get a real, dedicated IP
|
||||
address on your network instead of an awkward fixed port. It's pre-installed
|
||||
in this repo, not a "next step" — see [`docs/05-metallb.md`](05-metallb.md).
|
||||
|
||||
## Architecture — the shape of what you'll build
|
||||
|
||||
```
|
||||
┌─────────────────────────┐
|
||||
│ Git repository (this │
|
||||
│ repo, on GitHub) │
|
||||
│ │
|
||||
│ apps/demo.yaml │
|
||||
│ apps/demo/*.yaml │──┐
|
||||
│ apps/metallb.yaml │ │ watched by
|
||||
│ apps/metallb/* │ │
|
||||
└─────────────────────────┘ │
|
||||
▼
|
||||
┌───────────────────┐
|
||||
│ Argo CD │ runs inside the cluster
|
||||
│ (installed once, │
|
||||
│ imperatively) │
|
||||
└─────────┬──────────┘
|
||||
│ applies manifests
|
||||
▼
|
||||
┌─────────────────────────────┐
|
||||
│ Talos Linux node │
|
||||
│ (control plane + worker, │
|
||||
│ single node) │
|
||||
│ │
|
||||
│ ┌────────────┐ ┌──────────┐ │
|
||||
│ │demo │ metallb- │ │
|
||||
│ │namespace: │ system: │ │
|
||||
│ │nginx Pod ⇄ │ hands out │ │
|
||||
│ │Service │ a real IP │ │
|
||||
│ │(LoadBalancer)◄─address to │ │
|
||||
│ │ │ that Service│ │
|
||||
│ └────────────┘ └──────────┘ │
|
||||
└─────────────────────────────┘
|
||||
```
|
||||
|
||||
Everything below the Git repo is a *consequence* of what's in Git. If the
|
||||
box labelled "Talos Linux node" burned to the ground and you rebuilt it from
|
||||
scratch, replaying this repo against it would recreate everything except the
|
||||
raw OS bootstrap (docs/02), the one-time Argo CD install (docs/03), and the
|
||||
handful of network-specific addresses you choose in docs/05 — all
|
||||
deliberately kept outside the loop, since something has to start it and
|
||||
nothing in Git can know your LAN's IP layout for you.
|
||||
|
||||
## The reconciliation loop
|
||||
|
||||
This is the mechanism that makes GitOps actually work, not just a nice
|
||||
diagram. Argo CD repeats this forever, by default every few minutes (and
|
||||
instantly, if you ask it to):
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ 1. Read desired state from Git │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 2. Read actual state from the cluster │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 3. Diff them │
|
||||
│ │ │
|
||||
│ ┌────┴────┐ │
|
||||
│ │ │ │
|
||||
│ no diff diff found │
|
||||
│ │ │ │
|
||||
│ │ ▼ │
|
||||
│ │ 4. Apply the change so the │
|
||||
│ │ cluster matches Git │
|
||||
│ │ │ │
|
||||
│ └────┬─────┘ │
|
||||
│ ▼ │
|
||||
│ wait, then repeat │
|
||||
│ │
|
||||
└────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
"Reconciliation" just means step 3 and 4: noticing a difference between what
|
||||
should exist and what does, and fixing it. This loop is also what makes
|
||||
Argo CD self-healing — if someone changes the cluster by hand (`kubectl edit`
|
||||
during an emergency, say), the very next loop notices the drift from Git and
|
||||
reverts it. Git wins, always. That's the whole point.
|
||||
|
||||
> **Ask Claude:** before moving on, try asking "why does Git winning matter —
|
||||
> what goes wrong without it?" It's worth being able to answer in your own
|
||||
> words before you start building.
|
||||
|
||||
## What's next
|
||||
|
||||
Continue to [`docs/01-prerequisites.md`](01-prerequisites.md), or jump
|
||||
straight to [`docs/checklist.md`](checklist.md) if you'd rather work through
|
||||
a condensed step-by-step list with Claude alongside you.
|
||||
@@ -0,0 +1,112 @@
|
||||
# 01 — Prerequisites
|
||||
|
||||
Everything here targets **amd64** (standard 64-bit x86 — a spare PC, mini
|
||||
PC, or an amd64 VM). If your hardware is arm64 (Raspberry Pi, Apple Silicon
|
||||
VM), the tools below all publish arm64 builds too; swap the architecture in
|
||||
whichever install command you use and the rest of this guide is unchanged.
|
||||
|
||||
## 1. A target machine
|
||||
|
||||
You need one machine to become your cluster node — a spare physical PC, a
|
||||
mini PC, or a VM (VirtualBox, UTM, Proxmox, or any hypervisor that can boot
|
||||
from an ISO). Minimum realistic spec: 2 CPU cores, 4GB RAM, 20GB disk.
|
||||
|
||||
It needs to be reachable over the network from your everyday machine (the
|
||||
one you'll run `talosctl` and `kubectl` from), and it needs a stable IP
|
||||
address — see the note about DHCP reservations in
|
||||
[`talos/README.md`](../talos/README.md).
|
||||
|
||||
You do **not** need to install an OS on it yet. That's what the next doc
|
||||
covers — Talos gets installed by booting a special ISO and sending it
|
||||
configuration over the network, not through a normal installer.
|
||||
|
||||
Download the Talos amd64 ISO from the
|
||||
[Talos releases page](https://github.com/siderolabs/talos/releases) (look
|
||||
for `metal-amd64.iso` under the latest release) and boot your target machine
|
||||
from it. Leave it sitting at the boot screen — you'll configure it in
|
||||
[`docs/02-talos-bootstrap.md`](02-talos-bootstrap.md).
|
||||
|
||||
> **Ask Claude:** if you're not sure whether to use a VM or spare physical
|
||||
> hardware, ask — the tradeoffs (snapshots and easy resets vs. more
|
||||
> realistic networking) are worth five minutes of discussion before you
|
||||
> commit.
|
||||
|
||||
## 2. talosctl
|
||||
|
||||
The CLI you use to configure and manage Talos nodes.
|
||||
|
||||
**macOS (Homebrew):**
|
||||
```sh
|
||||
brew install siderolabs/tap/talosctl
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
```sh
|
||||
curl -sL https://talos.dev/install | sh
|
||||
```
|
||||
|
||||
Verify:
|
||||
```sh
|
||||
talosctl version --client
|
||||
```
|
||||
|
||||
## 3. kubectl
|
||||
|
||||
The standard Kubernetes CLI — you'll use it constantly, for this and every
|
||||
future Kubernetes cluster you touch.
|
||||
|
||||
**macOS (Homebrew):**
|
||||
```sh
|
||||
brew install kubectl
|
||||
```
|
||||
|
||||
**Linux:** follow the
|
||||
[official install instructions](https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/)
|
||||
for your distribution.
|
||||
|
||||
Verify:
|
||||
```sh
|
||||
kubectl version --client
|
||||
```
|
||||
|
||||
## 4. argocd CLI
|
||||
|
||||
The command-line client for Argo CD — used mainly to log in and inspect
|
||||
Applications; most day-to-day work happens through Git, not this CLI.
|
||||
|
||||
**macOS (Homebrew):**
|
||||
```sh
|
||||
brew install argocd
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
Download the `argocd-linux-amd64` binary from the
|
||||
[Argo CD releases page](https://github.com/argoproj/argo-cd/releases), then:
|
||||
```sh
|
||||
chmod +x argocd-linux-amd64
|
||||
sudo mv argocd-linux-amd64 /usr/local/bin/argocd
|
||||
```
|
||||
|
||||
Verify:
|
||||
```sh
|
||||
argocd version --client
|
||||
```
|
||||
|
||||
## 5. A GitHub account and a fork of this repo
|
||||
|
||||
This whole system works by Argo CD reading a Git repository you control.
|
||||
Fork this repo (or create a new repo from it) on GitHub, and clone *your*
|
||||
copy — not the original — to work from. Every command later in this guide
|
||||
that references a repo URL means your fork's URL.
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Target machine chosen, network-reachable, IP address noted
|
||||
- [ ] Talos ISO downloaded, machine booted from it and idle at the boot screen
|
||||
- [ ] `talosctl version --client` works
|
||||
- [ ] `kubectl version --client` works
|
||||
- [ ] `argocd version --client` works
|
||||
- [ ] You have your own fork of this repo cloned locally
|
||||
|
||||
Once all of these are true, continue to
|
||||
[`docs/02-talos-bootstrap.md`](02-talos-bootstrap.md).
|
||||
@@ -0,0 +1,72 @@
|
||||
# 02 — Bootstrapping Talos
|
||||
|
||||
At the end of this doc, you'll have a single-node Kubernetes cluster,
|
||||
running nothing but Kubernetes itself. No Argo CD yet — that's next.
|
||||
|
||||
## Why Talos, specifically
|
||||
|
||||
Most guides have you install a general-purpose Linux distribution, then
|
||||
install Kubernetes on top by hand or with a tool like kubeadm — meaning
|
||||
there's a whole normal OS underneath, with a package manager, an SSH
|
||||
daemon, and every other thing that can drift out of a known-good state or
|
||||
need patching. Talos throws that away: it's an OS whose only job is running
|
||||
Kubernetes. There's no SSH, no shell to log into, no packages to manage —
|
||||
you configure it entirely by sending it a **machine config** file over the
|
||||
network (the `talosctl` commands below), and it applies that config the
|
||||
same declarative way Kubernetes applies your app manifests. Less surface
|
||||
area, less to secure, less to keep in sync by hand — the same philosophy as
|
||||
the rest of this stack, one layer lower.
|
||||
|
||||
## Single node, on purpose
|
||||
|
||||
A "real" Kubernetes cluster usually has multiple nodes — some running the
|
||||
**control plane** (the brain: API server, scheduler, the cluster's
|
||||
database) and others as **workers** (running your actual apps), so that
|
||||
losing one machine doesn't take the whole cluster down. This repo starts
|
||||
with exactly one node acting as both, because that's the cheapest way to
|
||||
learn the concepts that matter for GitOps without needing multiple
|
||||
machines. [`docs/08-growing-up.md`](08-growing-up.md) covers adding more
|
||||
nodes once you're comfortable — the config here is deliberately compatible
|
||||
with that path, not a dead end.
|
||||
|
||||
## Steps
|
||||
|
||||
Full exact commands live in [`talos/README.md`](../talos/README.md) — read
|
||||
that alongside this doc; this section explains what's happening at each
|
||||
stage, that one has copy-paste blocks.
|
||||
|
||||
1. **Generate machine configuration.** `talosctl gen config` produces
|
||||
Talos's config files for your cluster, including
|
||||
[`talos/patch-controlplane.yaml`](../talos/patch-controlplane.yaml) —
|
||||
read that file, it's short and explains itself — which lets your one
|
||||
node run workloads as well as the control plane.
|
||||
|
||||
2. **Apply the config to your node.** The node, freshly booted from the
|
||||
Talos ISO, is sitting in "maintenance mode" with no configuration.
|
||||
`talosctl apply-config` sends it the config generated in step 1. The
|
||||
node reboots, installs Talos to its disk, and comes up running for real.
|
||||
|
||||
3. **Bootstrap etcd.** `talosctl bootstrap` initializes the cluster's data
|
||||
store. This is a one-time, one-node-ever operation — even in a
|
||||
multi-node cluster you only ever bootstrap once.
|
||||
|
||||
4. **Retrieve your kubeconfig.** `talosctl kubeconfig` downloads the
|
||||
credentials you'll use to talk to Kubernetes with `kubectl` — the
|
||||
Kubernetes equivalent of the `talosconfig` you got in step 1.
|
||||
|
||||
5. **Verify.** `kubectl get nodes` should show one node, `Ready`.
|
||||
|
||||
> **Ask Claude:** "what's the difference between a kubeconfig and a
|
||||
> talosconfig, and why are they separate files?" is a good one to ask once
|
||||
> you've got both — the split trips people up the first time.
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] `_out/controlplane.yaml`, `worker.yaml`, and `talosconfig` generated
|
||||
- [ ] Config applied to the node, node has rebooted and installed Talos
|
||||
- [ ] `talosctl bootstrap` run exactly once
|
||||
- [ ] kubeconfig retrieved
|
||||
- [ ] `kubectl get nodes` shows one `Ready` node
|
||||
|
||||
Once your node is `Ready`, continue to
|
||||
[`docs/03-installing-argocd.md`](03-installing-argocd.md).
|
||||
@@ -0,0 +1,58 @@
|
||||
# 03 — Installing Argo CD
|
||||
|
||||
Your cluster is alive but empty — it can run containers, but nothing is
|
||||
telling it to run anything yet. This doc installs Argo CD, the piece that
|
||||
watches Git and does that telling from now on.
|
||||
|
||||
## Why this one step is imperative
|
||||
|
||||
Everything else in this repo — every app, every future addition — gets
|
||||
deployed *by* Argo CD, driven from Git. Argo CD itself is the one exception:
|
||||
something has to exist in the cluster *before* it can start reading Git, so
|
||||
installing it can't itself be a GitOps-managed step. This is normal, and
|
||||
every GitOps setup has some version of this same bootstrap moment. We
|
||||
install it the straightforward way the Argo CD project documents — one
|
||||
`kubectl apply` of their maintained manifest bundle — rather than adding an
|
||||
extra tool (Helm, an operator) just to install the GitOps tool itself.
|
||||
|
||||
## What you're installing
|
||||
|
||||
Argo CD is itself a set of things running inside your cluster: an API
|
||||
server, a piece that watches Git repos, a piece that computes diffs against
|
||||
the live cluster, and a UI. You're about to install all of it with one
|
||||
command, into its own `argocd` namespace, so it stays cleanly separated
|
||||
from the apps it will go on to manage.
|
||||
|
||||
## Steps
|
||||
|
||||
Exact commands: [`argocd/README.md`](../argocd/README.md). Summary of what
|
||||
each stage does:
|
||||
|
||||
1. **Install.** `kubectl apply` the official manifest bundle into a new
|
||||
`argocd` namespace. This creates Argo CD's own pods *and* a handful of
|
||||
CRDs (see [`docs/glossary.md`](glossary.md)) — the most important being
|
||||
`Application`, the resource type you'll use for every app from here on,
|
||||
including the demo app and the app-of-apps.
|
||||
|
||||
2. **Get the initial admin password.** Auto-generated on first install and
|
||||
stored as a Kubernetes Secret.
|
||||
|
||||
3. **Reach it.** Port-forward the Argo CD UI/API to your local machine and
|
||||
log in with the `argocd` CLI (and optionally, a browser).
|
||||
|
||||
4. **Apply the app-of-apps.** This is the moment the GitOps loop actually
|
||||
starts — see [`docs/04-app-of-apps.md`](04-app-of-apps.md) next.
|
||||
|
||||
> **Ask Claude:** "why is a port-forward good enough for now, and what
|
||||
> would I need instead to reach this Argo CD from somewhere other than my
|
||||
> own machine?" — a preview of the Ingress topic in
|
||||
> [`docs/08-growing-up.md`](08-growing-up.md).
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Argo CD pods all `Running`/`Ready` in the `argocd` namespace
|
||||
- [ ] Initial admin password retrieved
|
||||
- [ ] Logged in via `argocd login` (CLI) and/or the UI in a browser
|
||||
|
||||
Once you're logged in, continue to
|
||||
[`docs/04-app-of-apps.md`](04-app-of-apps.md).
|
||||
@@ -0,0 +1,119 @@
|
||||
# 04 — The app-of-apps pattern
|
||||
|
||||
This is the last manual step. After this, Git drives everything.
|
||||
|
||||
## The idea
|
||||
|
||||
An Argo CD `Application` is a resource that says "watch this path in this
|
||||
Git repo, and keep the cluster matching what's there." The simplest
|
||||
possible setup is one Application per app you want to run — but then every
|
||||
new app means one more manual `kubectl apply`, which quietly puts you back
|
||||
to making changes by hand.
|
||||
|
||||
**App-of-apps** fixes this by making Applications themselves something Argo
|
||||
CD manages. One Application — the app-of-apps — watches a folder
|
||||
([`apps/`](../apps)) full of *other* Application manifests. Add a new app by
|
||||
adding a new file to that folder and pushing it — the app-of-apps notices
|
||||
the new file and creates the Application it describes, which in turn
|
||||
deploys the app. No `kubectl apply` required past this point, ever, for a
|
||||
new app.
|
||||
|
||||
```
|
||||
argocd/app-of-apps/app-of-apps.yaml (you apply this ONE file, by hand, once)
|
||||
│
|
||||
│ watches apps/, non-recursively
|
||||
├──────────────┬───────────────┐
|
||||
▼ ▼ ▼
|
||||
apps/demo.yaml apps/metallb.yaml (more, later — just add a file)
|
||||
│ │
|
||||
▼ ▼
|
||||
apps/demo/*.yaml apps/metallb/* (the actual manifests / Helm chart)
|
||||
```
|
||||
|
||||
Three layers, each watching the next. You only ever touch the bottom two —
|
||||
the app-of-apps is set-and-forget. Notice `apps/metallb.yaml` is already
|
||||
here — see [`docs/05-metallb.md`](05-metallb.md) right after this one.
|
||||
|
||||
## Why `apps/` isn't recursive
|
||||
|
||||
Look at [`argocd/app-of-apps/app-of-apps.yaml`](../argocd/app-of-apps/app-of-apps.yaml)
|
||||
— specifically `directory.recurse: false`. The `apps/` folder contains both
|
||||
Application manifests (`apps/demo.yaml`, `apps/metallb.yaml`) *and*
|
||||
subfolders holding the real workload manifests those Applications point to
|
||||
(`apps/demo/*.yaml`, `apps/metallb/*`). Non-recursive means the
|
||||
app-of-apps only ever looks at files directly inside `apps/`, never inside
|
||||
its subfolders — so it only ever sees Application definitions, and leaves
|
||||
the actual workload manifests to the Application that owns them. Without
|
||||
this, the app-of-apps would try to apply raw Deployments and Helm chart
|
||||
files itself, duplicating and conflicting with what those Applications are
|
||||
already doing.
|
||||
|
||||
## Every Application needs its own `repoURL`
|
||||
|
||||
Applications don't inherit `repoURL` from whatever applied them — each one
|
||||
declares its own source repo independently. That means the placeholder
|
||||
`https://github.com/YOUR_GITHUB_USERNAME/...` needs fixing in *every* file
|
||||
that has it, not just the one you apply by hand:
|
||||
|
||||
```sh
|
||||
grep -rl YOUR_GITHUB_USERNAME .
|
||||
```
|
||||
|
||||
You should see three files: `argocd/app-of-apps/app-of-apps.yaml`,
|
||||
`apps/demo.yaml`, and `apps/metallb.yaml`. Edit `spec.source.repoURL` in
|
||||
each to point at your fork.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Fix `repoURL` in all three files (above).
|
||||
2. Commit and push — this part matters more than it looks: the
|
||||
app-of-apps reads `apps/demo.yaml` and `apps/metallb.yaml` *from Git*,
|
||||
not from your local edits, so they have to be pushed before you apply
|
||||
anything.
|
||||
```sh
|
||||
git add argocd/app-of-apps/app-of-apps.yaml apps/demo.yaml apps/metallb.yaml
|
||||
git commit -m "Point Applications at my fork"
|
||||
git push
|
||||
```
|
||||
3. Apply the app-of-apps — the one manifest you apply by hand:
|
||||
```sh
|
||||
kubectl apply -f argocd/app-of-apps/app-of-apps.yaml
|
||||
```
|
||||
4. Watch it sync, then watch `demo` and `metallb` appear underneath it:
|
||||
```sh
|
||||
argocd app list
|
||||
```
|
||||
You should see `app-of-apps`, `demo`, and `metallb`, all `Synced` and
|
||||
`Healthy` within a minute or two (MetalLB's own install takes a little
|
||||
longer than the demo app).
|
||||
5. Confirm the demo app is actually running:
|
||||
```sh
|
||||
kubectl -n demo get pods,svc
|
||||
```
|
||||
You'll notice the Service's `EXTERNAL-IP` column says `<pending>` —
|
||||
that's expected. The demo app is a `LoadBalancer` Service, and nothing
|
||||
hands those out real addresses on bare metal until MetalLB is
|
||||
configured, which is exactly the next doc.
|
||||
|
||||
If `app-of-apps`, `demo`, and `metallb` all show `Synced`/`Healthy`, the
|
||||
loop — Git → Argo CD → cluster — is already proven end to end; you just
|
||||
can't see the demo app in a browser yet.
|
||||
|
||||
> **Ask Claude:** "walk me through exactly what happened, in order, from the
|
||||
> moment I ran `kubectl apply -f argocd/app-of-apps/app-of-apps.yaml` to
|
||||
> these three Applications existing." Making sure you can narrate that
|
||||
> chain yourself is the real checkpoint for this doc — everything from
|
||||
> here on is a variation on it.
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] `repoURL` fixed in all three files listed above
|
||||
- [ ] Fix committed and pushed
|
||||
- [ ] `app-of-apps` applied and shows `Synced`/`Healthy`
|
||||
- [ ] `demo` and `metallb` Applications appeared automatically, both
|
||||
`Synced`/`Healthy`
|
||||
- [ ] `kubectl -n demo get pods,svc` shows the demo app's Pod running
|
||||
(Service `EXTERNAL-IP` still `<pending>` — expected for now)
|
||||
|
||||
Continue to [`docs/05-metallb.md`](05-metallb.md) to give the demo app —
|
||||
and Argo CD's own UI — a real address.
|
||||
@@ -0,0 +1,155 @@
|
||||
# 05 — MetalLB: getting real IP addresses on bare metal
|
||||
|
||||
The app-of-apps you just applied ([`docs/04-app-of-apps.md`](04-app-of-apps.md))
|
||||
already brought up `metallb` alongside `demo`. This doc explains what it
|
||||
does, walks you through the one piece only you can decide (which IP
|
||||
addresses to actually use), and finishes by giving Argo CD's own UI a real
|
||||
address too.
|
||||
|
||||
## Why this is needed at all
|
||||
|
||||
On a cloud provider, a Kubernetes Service of `type: LoadBalancer` triggers
|
||||
the cloud to provision an actual load balancer and hand your Service a
|
||||
real, routable IP automatically. Bare metal — your Talos node, sitting on
|
||||
your own network — has no such thing behind it. Without help, a
|
||||
`LoadBalancer` Service on bare metal just sits with its address stuck at
|
||||
`<pending>` forever.
|
||||
|
||||
**MetalLB** is that help: software that runs inside your cluster,
|
||||
watches for `LoadBalancer` Services, and hands them a real address from a
|
||||
range *you* configure — then makes that address actually reachable on your
|
||||
network. It's the standard, widely-used answer to this exact gap, which is
|
||||
why it's installed as a pre-installed app in this repo rather than left as
|
||||
a "next step."
|
||||
|
||||
`apps/demo/service.yaml` is already `type: LoadBalancer` — that's what you
|
||||
saw sitting at `<pending>` if you looked closely in the last doc. This doc
|
||||
is what turns that into a real, reachable address.
|
||||
|
||||
## What's already running
|
||||
|
||||
**`apps/metallb.yaml`** → one Application, pointing at `apps/metallb`, a
|
||||
Helm chart that does two things at once (see
|
||||
[`apps/metallb/Chart.yaml`](../apps/metallb/Chart.yaml)):
|
||||
|
||||
- Pulls in the official upstream MetalLB chart as a dependency — this is
|
||||
also the real, working example of a Helm-based app mentioned in
|
||||
[`docs/07-adding-new-apps.md`](07-adding-new-apps.md).
|
||||
- Carries two templates of its own
|
||||
([`apps/metallb/templates/`](../apps/metallb/templates)): an
|
||||
`IPAddressPool` and an `L2Advertisement` — MetalLB's own custom
|
||||
resources, which tell it which addresses it's allowed to use and how to
|
||||
advertise them. Right now these still contain **placeholder** IP
|
||||
addresses that won't work on your network — that's what you're about to
|
||||
fix.
|
||||
|
||||
Both the upstream chart and our two extra resources sync together as one
|
||||
Application. Look at the `sync-wave` annotation on
|
||||
[`apps/metallb/templates/ipaddresspool.yaml`](../apps/metallb/templates/ipaddresspool.yaml) —
|
||||
it's what keeps this safe despite being one sync: MetalLB's own resources
|
||||
(including its CRDs) apply first, and Argo CD waits for them to be healthy
|
||||
before applying our two resources, which depend on those CRDs existing.
|
||||
|
||||
## Answer three questions before editing anything
|
||||
|
||||
> **Ask Claude:** literally ask — "help me pick MetalLB's IP range and the
|
||||
> addresses for Argo CD and the demo app." This is the one part of this
|
||||
> whole repo that depends entirely on *your* network, which nothing in
|
||||
> this repo can know in advance. Have your router's admin page or DHCP
|
||||
> settings handy; Claude will ask what your LAN's subnet is and what range
|
||||
> your DHCP server already hands out, so the pool it suggests doesn't
|
||||
> collide with anything.
|
||||
|
||||
You need three decisions:
|
||||
|
||||
1. **The pool range** — a handful of addresses (5–10 is plenty for this
|
||||
repo) on your LAN's subnet that your DHCP server will never hand to
|
||||
anything else. Commonly a slice just above or below your DHCP range,
|
||||
e.g. if your router hands out `.100`–`.199`, something like
|
||||
`.240`–`.250` is a safe, unused slice.
|
||||
2. **Argo CD's UI address** — one address from that pool.
|
||||
3. **The demo app's address** — a different address from that pool.
|
||||
|
||||
## Edit the placeholders
|
||||
|
||||
1. [`apps/metallb/templates/ipaddresspool.yaml`](../apps/metallb/templates/ipaddresspool.yaml)
|
||||
— replace the `addresses` range with your chosen pool.
|
||||
2. [`apps/demo/service.yaml`](../apps/demo/service.yaml) — replace the
|
||||
`metallb.universe.tf/loadBalancerIPs` annotation with the demo app's
|
||||
chosen address (must be inside the pool from step 1).
|
||||
3. [`docs/network.md`](network.md) — record the pool range and the demo
|
||||
app's address in the tables there (Argo CD's address goes in once you've
|
||||
picked it below).
|
||||
4. Commit and push all three:
|
||||
```sh
|
||||
git add apps/metallb/templates/ipaddresspool.yaml apps/demo/service.yaml docs/network.md
|
||||
git commit -m "Configure MetalLB's address pool for my network"
|
||||
git push
|
||||
```
|
||||
5. Force a sync rather than waiting for the next poll:
|
||||
```sh
|
||||
argocd app sync metallb demo
|
||||
```
|
||||
|
||||
## Verify
|
||||
|
||||
```sh
|
||||
kubectl -n demo get svc demo
|
||||
```
|
||||
`EXTERNAL-IP` should now show the address you chose instead of `<pending>`.
|
||||
Visit `http://<that-address>` in a browser — same "It works!" page as
|
||||
before, now on its own real IP instead of a NodePort.
|
||||
|
||||
```sh
|
||||
kubectl -n metallb-system get ipaddresspools,l2advertisements
|
||||
```
|
||||
Confirms your pool and advertisement were applied as expected.
|
||||
|
||||
## Give Argo CD's UI a real address too
|
||||
|
||||
So far you've reached the Argo CD UI through `kubectl port-forward`
|
||||
(see [`docs/03-installing-argocd.md`](03-installing-argocd.md)). Now that
|
||||
MetalLB is running, you can give it a permanent address instead. Argo CD's
|
||||
own install isn't managed through this repo's GitOps loop (it's the
|
||||
imperative bootstrap step — see `docs/03`), so this one change is applied
|
||||
directly rather than through Git:
|
||||
|
||||
```sh
|
||||
kubectl -n argocd patch svc argocd-server --type merge -p \
|
||||
'{"spec":{"type":"LoadBalancer"},"metadata":{"annotations":{"metallb.universe.tf/loadBalancerIPs":"<ARGOCD_IP>"}}}'
|
||||
```
|
||||
|
||||
Replace `<ARGOCD_IP>` with the address you chose in step 2 above. Then:
|
||||
|
||||
```sh
|
||||
kubectl -n argocd get svc argocd-server
|
||||
```
|
||||
|
||||
Once `EXTERNAL-IP` shows your chosen address, visit
|
||||
`https://<that-address>` directly — no port-forward needed from now on.
|
||||
(You'll still hit the same self-signed certificate warning as before; see
|
||||
[`docs/03-installing-argocd.md`](03-installing-argocd.md) for why that's
|
||||
expected here.)
|
||||
|
||||
Update [`docs/network.md`](network.md) one more time with this address —
|
||||
commit and push it. This one's a genuine exception to "everything's driven
|
||||
by Git": the patch command above isn't a file in this repo, so this file is
|
||||
the only place this address gets recorded at all.
|
||||
|
||||
> **Ask Claude:** "why did the demo app get its LoadBalancer IP through
|
||||
> Git, but Argo CD's didn't?" — a good check that the boundary between
|
||||
> "managed by this repo" and "the imperative bootstrap that starts it"
|
||||
> has actually landed.
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Chosen a pool range, an Argo CD UI address, and a demo app address,
|
||||
with Claude's help, that fit your actual network
|
||||
- [ ] `apps/metallb/templates/ipaddresspool.yaml` edited to your real range
|
||||
- [ ] `apps/demo/service.yaml` edited to your real demo address
|
||||
- [ ] `docs/network.md` updated with the pool range and both addresses
|
||||
- [ ] Committed, pushed, synced
|
||||
- [ ] `kubectl -n demo get svc demo` shows a real `EXTERNAL-IP`, page loads in a browser
|
||||
- [ ] `argocd-server` patched to `LoadBalancer`, reachable at its own address without port-forwarding
|
||||
|
||||
Continue to [`docs/06-your-first-change.md`](06-your-first-change.md).
|
||||
@@ -0,0 +1,94 @@
|
||||
# 06 — Your first change
|
||||
|
||||
Everything so far proved Argo CD can deploy what's in Git. This doc proves
|
||||
the part that actually matters day to day: changing what's in Git and
|
||||
watching the cluster follow, with no `kubectl apply` from you at all.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Open [`apps/demo/configmap.yaml`](../apps/demo/configmap.yaml) and edit
|
||||
the HTML — change the `<h1>` text to something of your own.
|
||||
|
||||
2. Commit and push, exactly like any other Git change:
|
||||
```sh
|
||||
git add apps/demo/configmap.yaml
|
||||
git commit -m "Personalize the demo page"
|
||||
git push
|
||||
```
|
||||
|
||||
3. By default, Argo CD polls Git every three minutes, so your change will
|
||||
show up on its own — but you can force it immediately:
|
||||
```sh
|
||||
argocd app sync demo
|
||||
```
|
||||
|
||||
4. Watch it happen:
|
||||
```sh
|
||||
argocd app get demo
|
||||
```
|
||||
You'll see Argo CD detect that the live ConfigMap no longer matches
|
||||
Git, and correct it.
|
||||
|
||||
5. Refresh `http://<your-demo-app-IP>` (the MetalLB address you gave it in
|
||||
[`docs/05-metallb.md`](05-metallb.md)). Your new text should be there. Note
|
||||
that nginx itself never restarted — Kubernetes noticed the mounted
|
||||
ConfigMap changed and updated the file inside the running Pod's
|
||||
filesystem automatically. (This is specific to volume-mounted
|
||||
ConfigMaps; not every kind of change is this seamless — a change to the
|
||||
Deployment's image, for instance, does trigger a Pod restart, by
|
||||
design.)
|
||||
|
||||
Give it up to a minute if the page doesn't look different yet.
|
||||
`argocd app get demo` shows the *ConfigMap object* updating right away,
|
||||
but kubelet — the agent on the node that actually keeps a Pod's mounted
|
||||
files in sync — only re-checks mounted ConfigMaps on its own periodic
|
||||
cycle, not instantly. A blank refresh isn't a sign anything's wrong;
|
||||
give it a moment and refresh again.
|
||||
|
||||
## What just proved itself
|
||||
|
||||
You changed a file, pushed it, and never touched the cluster directly. That
|
||||
gap between "commit" and "running" being closed automatically, without you
|
||||
doing the applying, *is* GitOps. Everything from here on — every future app
|
||||
you add — works exactly this way: edit, commit, push, done.
|
||||
|
||||
## Try triggering self-healing
|
||||
|
||||
Optional, but worth seeing once. Reintroduce drift by hand:
|
||||
|
||||
```sh
|
||||
kubectl -n demo patch configmap demo-page --type merge \
|
||||
-p '{"data":{"index.html":"<h1>hand-edited, not from Git</h1>"}}'
|
||||
```
|
||||
|
||||
Watch the ConfigMap *object* itself, not the browser, for this one — it's
|
||||
the more reliable way to see the timing: Argo CD's self-heal typically
|
||||
reverts a hand-edit within a second or two, which is faster than kubelet's
|
||||
own mount-sync cycle usually gets around to reflecting a change in the
|
||||
served page at all (see the note above). Check the object directly:
|
||||
|
||||
```sh
|
||||
kubectl -n demo get configmap demo-page -o jsonpath='{.data.index\.html}'
|
||||
```
|
||||
|
||||
Run that once right after the `patch` above and you may catch your
|
||||
hand-edited text; run it again a couple of seconds later and it'll already
|
||||
be back to what's in Git — with no action from you. That's `selfHeal:
|
||||
true` from `apps/demo.yaml` in action. See
|
||||
[`docs/00-what-is-all-this.md`](00-what-is-all-this.md#the-reconciliation-loop)
|
||||
for the loop that just ran.
|
||||
|
||||
> **Ask Claude:** "if selfHeal is on, how would I ever safely debug
|
||||
> something by changing it live in the cluster first?" — a real question
|
||||
> people run into once this clicks, and worth understanding before you hit
|
||||
> it for real.
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Edited `apps/demo/configmap.yaml`, committed, pushed
|
||||
- [ ] Saw the change reflected in `argocd app get demo`
|
||||
- [ ] Saw the new text in the browser
|
||||
- [ ] (Optional) Watched self-healing revert a hand-made change
|
||||
|
||||
Continue to [`docs/07-adding-new-apps.md`](07-adding-new-apps.md) to add
|
||||
something of your own.
|
||||
@@ -0,0 +1,87 @@
|
||||
# 07 — Adding a new app
|
||||
|
||||
This is the payoff for the app-of-apps structure: adding something new is
|
||||
"drop a file here," not a special procedure.
|
||||
|
||||
## The pattern
|
||||
|
||||
Every app in this repo is two things sitting side by side in
|
||||
[`apps/`](../apps):
|
||||
|
||||
```
|
||||
apps/
|
||||
demo.yaml ← the Application (tells Argo CD where to look)
|
||||
demo/ ← the actual manifests (what gets deployed)
|
||||
```
|
||||
|
||||
To add a new app called, say, `whoami`:
|
||||
|
||||
1. Create `apps/whoami/` and put its Kubernetes manifests in there — a
|
||||
Deployment, a Service, whatever it needs. Use `apps/demo/` as a
|
||||
template for structure and comment style.
|
||||
|
||||
2. Create `apps/whoami.yaml`, copied from [`apps/demo.yaml`](../apps/demo.yaml)
|
||||
with the names changed: `metadata.name`, `spec.source.repoURL` (still
|
||||
your fork — see [`docs/04-app-of-apps.md`](04-app-of-apps.md#every-application-needs-its-own-repourl)
|
||||
for why every Application needs this set individually), `spec.source.path`
|
||||
(→ `apps/whoami`), and `spec.destination.namespace`.
|
||||
|
||||
3. Commit and push both. That's it — no `kubectl apply`. The app-of-apps
|
||||
(already watching `apps/` — see
|
||||
[`docs/04-app-of-apps.md`](04-app-of-apps.md)) notices the new
|
||||
`whoami.yaml`, creates the `whoami` Application, which in turn deploys
|
||||
everything in `apps/whoami/`.
|
||||
|
||||
4. Confirm: `argocd app list` should show `whoami` alongside `demo` and
|
||||
`metallb`.
|
||||
|
||||
## Manifest-based vs. Helm-based apps
|
||||
|
||||
`apps/demo/` is plain Kubernetes manifests — no templating, nothing to
|
||||
render — which is why it's the easiest one to read start to finish while
|
||||
you're still learning what each field does.
|
||||
|
||||
An app's folder can be a **Helm chart** instead. Argo CD detects this
|
||||
automatically: if it finds a `Chart.yaml` at the source path, it treats
|
||||
that app as a Helm chart rather than plain manifests. This matters most
|
||||
when you want to pull in a *third-party* chart — a database, an ingress
|
||||
controller, anything with a public Helm chart already maintained by
|
||||
someone else — without copying their manifests into your repo by hand. You
|
||||
declare it as a dependency and let Helm (via Argo CD) resolve it.
|
||||
|
||||
You don't have to imagine this one — it's already running.
|
||||
[`apps/metallb/`](../apps/metallb) is exactly this pattern, for real: its
|
||||
[`Chart.yaml`](../apps/metallb/Chart.yaml) declares a dependency on the
|
||||
upstream MetalLB chart, and it even goes a step further by also carrying
|
||||
its own templates ([`apps/metallb/templates/`](../apps/metallb/templates))
|
||||
for two custom resources that configure MetalLB once it's installed —
|
||||
useful to see if you ever need an app that's *mostly* someone else's chart
|
||||
but with a couple of your own resources layered on top. See
|
||||
[`docs/05-metallb.md`](05-metallb.md) for the full walkthrough, and
|
||||
[`apps/metallb.yaml`](../apps/metallb.yaml) for the matching Application —
|
||||
notice it's structurally identical to `apps/demo.yaml`; Argo CD figures out
|
||||
it's a Helm chart on its own from the presence of `Chart.yaml`, no new
|
||||
syntax required in the Application resource itself.
|
||||
|
||||
> **Ask Claude:** "walk me through how I'd bring in [some real chart you
|
||||
> have in mind] as a Helm-based app in this structure" whenever you're
|
||||
> ready to try one of your own — `apps/metallb/` is a real template to
|
||||
> copy from.
|
||||
|
||||
If the new app has its own `LoadBalancer` Service (see
|
||||
[`docs/05-metallb.md`](05-metallb.md)), pick its address from the free
|
||||
addresses listed in [`docs/network.md`](network.md), and add a row for it
|
||||
there once it's up — that file only stays useful if every app that gets an
|
||||
address shows up in it.
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] New app's manifests live in `apps/<name>/`
|
||||
- [ ] New `apps/<name>.yaml` Application created, modeled on `demo.yaml`
|
||||
- [ ] `repoURL` set to your fork, not left as a placeholder
|
||||
- [ ] Pushed to Git
|
||||
- [ ] `argocd app list` shows the new app, `Synced`/`Healthy`
|
||||
- [ ] If it has a `LoadBalancer` Service, its address is recorded in `docs/network.md`
|
||||
|
||||
Continue to [`docs/08-growing-up.md`](08-growing-up.md) for what to add
|
||||
next as this cluster grows beyond a learning setup.
|
||||
@@ -0,0 +1,91 @@
|
||||
# 08 — Growing up: what to add next
|
||||
|
||||
Everything up to here is deliberately minimal — enough to understand the
|
||||
whole loop, nothing more. This doc is a map of the natural next steps, each
|
||||
with why you'd want it and roughly what it involves. None of it is built
|
||||
for you; each is its own project once you're ready.
|
||||
|
||||
## More nodes
|
||||
|
||||
Right now, one machine is both control plane and worker
|
||||
(`allowSchedulingOnControlPlanes: true` in
|
||||
[`talos/patch-controlplane.yaml`](../talos/patch-controlplane.yaml)). That's
|
||||
a single point of failure: if that machine dies, the cluster is gone.
|
||||
|
||||
To grow beyond it:
|
||||
|
||||
1. Boot a second machine from the Talos ISO, same as the first.
|
||||
2. Generate a **worker** config instead of a control-plane one — you
|
||||
already have `_out/worker.yaml` from
|
||||
[`docs/02-talos-bootstrap.md`](02-talos-bootstrap.md), unused until now.
|
||||
3. `talosctl apply-config --insecure --nodes <NEW_NODE_IP> --file _out/worker.yaml`
|
||||
4. `kubectl get nodes` should show two nodes shortly after.
|
||||
|
||||
For real resilience against a control-plane node failing (not just adding
|
||||
worker capacity), you'd want an odd number of control-plane nodes — 3 is
|
||||
the normal minimum — because etcd (the cluster's data store) uses majority
|
||||
voting to agree on state, and majority voting needs an odd number to avoid
|
||||
ties. That's a bigger step than this doc covers in detail; ask Claude when
|
||||
you're actually ready to take it.
|
||||
|
||||
## Ingress
|
||||
|
||||
Right now, thanks to MetalLB ([`docs/05-metallb.md`](05-metallb.md)), the
|
||||
demo app gets its own dedicated IP address
|
||||
(see [`apps/demo/service.yaml`](../apps/demo/service.yaml)). That's a real
|
||||
improvement over NodePort, but it doesn't scale past a handful of apps
|
||||
either: every app still needs its own IP address out of a pool that's only
|
||||
so big, and there's no way to route by hostname (`app1.example.com` vs
|
||||
`app2.example.com` sharing one IP on port 443).
|
||||
|
||||
An **Ingress controller** is itself just another app you'd deploy the same
|
||||
way as anything else in this repo (see
|
||||
[`docs/07-adding-new-apps.md`](07-adding-new-apps.md)) — commonly as a
|
||||
Helm-based app, since most ingress controllers are distributed that way,
|
||||
same as MetalLB. It typically sits behind one single MetalLB
|
||||
`LoadBalancer` Service (one IP, total, for every app), and you add an
|
||||
`Ingress` resource per app instead of giving each one its own
|
||||
`LoadBalancer` Service — trading "one IP per app" for "one IP, many
|
||||
hostnames."
|
||||
|
||||
## cert-manager
|
||||
|
||||
Once you have Ingress and real hostnames, you'll want HTTPS. `cert-manager`
|
||||
is the standard tool for this — it watches for certificate requests and
|
||||
automatically obtains and renews them (commonly via Let's Encrypt). Also
|
||||
just another app in this repo's structure once you get there.
|
||||
|
||||
## Secrets management
|
||||
|
||||
**This repo is public. Never commit a plaintext secret, credential, token,
|
||||
or private key to it — anyone can read your Git history, forever, even if
|
||||
you delete the file in a later commit.**
|
||||
|
||||
Kubernetes' own `Secret` resource does not encrypt anything — it just
|
||||
base64-encodes data, which is not encryption, only encoding (trivially
|
||||
reversible by anyone). None of the manifests in this repo need secrets yet,
|
||||
which is exactly why this hasn't come up so far. When you add something
|
||||
that does (a database password, an API key), you need one of:
|
||||
|
||||
- **[Sealed Secrets](https://github.com/bitnami-labs/sealed-secrets)** —
|
||||
encrypts a secret using a key that only your cluster holds, producing a
|
||||
`SealedSecret` file that's safe to commit; the cluster decrypts it into a
|
||||
normal Secret on the fly.
|
||||
- **[SOPS](https://github.com/getsops/sops)** (often paired with Argo CD via
|
||||
a plugin) — encrypts values inside a normal-looking YAML file using a key
|
||||
from a system like age or a cloud KMS, decrypted at sync time.
|
||||
|
||||
Either is a reasonable choice; don't improvise your own approach. Ask
|
||||
Claude to walk you through setting one up when you have a real secret to
|
||||
manage — it's a short, well-trodden path once you're at that point.
|
||||
|
||||
> **Ask Claude:** if you're about to commit anything you're even slightly
|
||||
> unsure about, paste it (or describe it) and ask first. Cheaper than a
|
||||
> `git filter-branch` later.
|
||||
|
||||
## Where to go from here
|
||||
|
||||
You've now seen the whole shape: Talos underneath, Argo CD watching Git,
|
||||
app-of-apps organizing what it manages. Everything in this doc is more of
|
||||
the same pattern — a new app, a new file, a push. Pick whichever expansion
|
||||
is actually useful to you and start there.
|
||||
@@ -0,0 +1,90 @@
|
||||
# Guided bootstrap checklist
|
||||
|
||||
A single-page, ordered checklist covering the entire path from bare
|
||||
machine to a working GitOps loop. It's meant to be worked through **with
|
||||
Claude**, one item at a time — ask Claude to walk you through this list,
|
||||
and it will run each step with you, wait for your output, confirm it
|
||||
worked, and only then move to the next one. See `CLAUDE.md` at the repo
|
||||
root for how Claude is primed to do this.
|
||||
|
||||
Each item names the doc it's drawn from, in case you want the full
|
||||
explanation before or after running it. Replace `<NODE_IP>`,
|
||||
`YOUR_GITHUB_USERNAME`, and the MetalLB IP addresses with your real values
|
||||
throughout — Claude will remind you if you paste a command still
|
||||
containing a placeholder, and will ask you for the IP addresses directly
|
||||
in Phase 5 rather than guessing them.
|
||||
|
||||
## Phase 1 — Prerequisites ([`01`](01-prerequisites.md))
|
||||
|
||||
- [ ] Target machine chosen, reachable on the network, with a stable
|
||||
(ideally DHCP-reserved) IP address
|
||||
- [ ] Talos amd64 ISO downloaded, machine booted from it, sitting at the
|
||||
boot screen
|
||||
- [ ] `talosctl version --client` runs successfully
|
||||
- [ ] `kubectl version --client` runs successfully
|
||||
- [ ] `argocd version --client` runs successfully
|
||||
- [ ] Repo forked on GitHub and cloned locally as **your own copy**
|
||||
|
||||
## Phase 2 — Talos bootstrap ([`02`](02-talos-bootstrap.md), [`talos/README.md`](../talos/README.md))
|
||||
|
||||
- [ ] `talosctl gen config gitops-starter https://<NODE_IP>:6443 --output-dir _out --config-patch-control-plane @talos/patch-controlplane.yaml` run — `_out/controlplane.yaml`, `worker.yaml`, `talosconfig` exist
|
||||
- [ ] `TALOSCONFIG` exported (or `talosctl config endpoint`/`node` set) to `_out/talosconfig`
|
||||
- [ ] `talosctl apply-config --insecure --nodes <NODE_IP> --file _out/controlplane.yaml` run, node rebooted
|
||||
- [ ] `talosctl bootstrap --nodes <NODE_IP>` run — **exactly once**
|
||||
- [ ] `talosctl kubeconfig --nodes <NODE_IP> .` run, `KUBECONFIG` pointed at the result
|
||||
- [ ] `kubectl get nodes` shows one node, `Ready`
|
||||
|
||||
## Phase 3 — Argo CD install ([`03`](03-installing-argocd.md), [`argocd/README.md`](../argocd/README.md))
|
||||
|
||||
- [ ] `kubectl create namespace argocd`
|
||||
- [ ] `kubectl apply --server-side -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml`
|
||||
- [ ] `kubectl -n argocd wait --for=condition=Available --timeout=300s deployment --all` completes
|
||||
- [ ] Initial admin password retrieved
|
||||
- [ ] `kubectl -n argocd port-forward svc/argocd-server 8080:443` running in its own terminal
|
||||
- [ ] `argocd login localhost:8080 --username admin --password <pw> --insecure` succeeds
|
||||
|
||||
## Phase 4 — Hand the cluster to Git ([`04`](04-app-of-apps.md))
|
||||
|
||||
- [ ] `spec.source.repoURL` fixed in all three placeholder files: run
|
||||
`grep -rl YOUR_GITHUB_USERNAME .` to find them
|
||||
(`argocd/app-of-apps/app-of-apps.yaml`, `apps/demo.yaml`, `apps/metallb.yaml`)
|
||||
- [ ] Edit committed and pushed to your fork's `main` branch
|
||||
- [ ] `kubectl apply -f argocd/app-of-apps/app-of-apps.yaml`
|
||||
- [ ] `argocd app list` shows `app-of-apps`, `Synced`/`Healthy`
|
||||
- [ ] `argocd app list` shows `demo` and `metallb`, appeared automatically, both `Synced`/`Healthy`
|
||||
- [ ] `kubectl -n demo get pods,svc` shows the demo app's Pod running
|
||||
(Service `EXTERNAL-IP` still `<pending>` — expected for now)
|
||||
|
||||
## Phase 5 — MetalLB: real IP addresses ([`05`](05-metallb.md))
|
||||
|
||||
- [ ] **Ask Claude** to help pick: a MetalLB pool range, an address for
|
||||
Argo CD's UI, and an address for the demo app — all based on your
|
||||
actual LAN/DHCP setup
|
||||
- [ ] `apps/metallb/templates/ipaddresspool.yaml` edited to your real pool range
|
||||
- [ ] `apps/demo/service.yaml` edited to your real demo app address
|
||||
- [ ] `docs/network.md` updated with the pool range and both addresses (Argo CD's added once patched, below)
|
||||
- [ ] Committed, pushed, and synced (`argocd app sync metallb demo`)
|
||||
- [ ] `kubectl -n demo get svc demo` shows a real `EXTERNAL-IP`; page loads at `http://<that-address>`
|
||||
- [ ] `argocd-server` Service patched to `LoadBalancer` with your chosen Argo CD address (`kubectl -n argocd patch svc argocd-server ...`)
|
||||
- [ ] Argo CD UI reachable at `https://<that-address>` with no port-forward running
|
||||
|
||||
## Phase 6 — Prove the loop ([`06`](06-your-first-change.md))
|
||||
|
||||
- [ ] Edited the `<h1>` text in `apps/demo/configmap.yaml`
|
||||
- [ ] Committed and pushed
|
||||
- [ ] `argocd app sync demo` (or waited for auto-poll) shows the change applied
|
||||
- [ ] Refreshed the browser — new text visible
|
||||
- [ ] (Optional) Triggered self-heal: hand-edited the ConfigMap with `kubectl patch`, confirmed Argo CD reverted it
|
||||
|
||||
## You're done when
|
||||
|
||||
Every box above is checked, you've watched a change you made in a text
|
||||
editor show up in a browser without ever running `kubectl apply` yourself
|
||||
for it, and you can explain in your own words what happened between "git
|
||||
push" and the page updating. If you can't yet, that's the sign to ask
|
||||
Claude for a walkthrough of whichever phase felt shaky before calling this
|
||||
finished.
|
||||
|
||||
From here: [`07-adding-new-apps.md`](07-adding-new-apps.md) to add
|
||||
something of your own, or [`08-growing-up.md`](08-growing-up.md) for the
|
||||
bigger next steps (more nodes, ingress, secrets management).
|
||||
@@ -0,0 +1,161 @@
|
||||
# Glossary
|
||||
|
||||
Every term this repo uses, in plain English. If you hit a word in the docs
|
||||
or a YAML comment that isn't here, ask Claude to add it.
|
||||
|
||||
**API server** — The part of Kubernetes every other piece (and every
|
||||
`kubectl` command) talks to. It's the front door: all reads and writes to
|
||||
the cluster's state go through it.
|
||||
|
||||
**Application (Argo CD)** — A Custom Resource (see CRD, below) that Argo CD
|
||||
adds to Kubernetes. It tells Argo CD "watch this path in this Git repo, and
|
||||
keep the cluster matching it." Not a Kubernetes-native concept — it only
|
||||
exists once Argo CD is installed.
|
||||
|
||||
**app-of-apps** — The pattern this repo uses: one Application (itself
|
||||
called the app-of-apps) watches a folder of *other* Application manifests,
|
||||
so adding a new app is "add a file," not "run a new command." See
|
||||
[`docs/04-app-of-apps.md`](04-app-of-apps.md).
|
||||
|
||||
**ARP (Address Resolution Protocol)** — How devices on the same local
|
||||
network find each other's hardware addresses. MetalLB's L2 mode works by
|
||||
answering ARP requests for whichever IP address it's handed out, which is
|
||||
what makes that address reachable from the rest of your LAN.
|
||||
|
||||
**bootstrap** — The one-time setup that has to happen *before* the normal
|
||||
system can take over and manage itself. Used here for `talosctl bootstrap`
|
||||
(initializes etcd, once, ever) and more generally for the handful of manual
|
||||
steps — installing Argo CD, applying the app-of-apps — that get the
|
||||
GitOps loop started before Git takes over.
|
||||
|
||||
**cluster** — A group of one or more machines (nodes) working together as
|
||||
one Kubernetes system. This repo's cluster starts as a single node.
|
||||
|
||||
**CNI (Container Network Interface)** — The plugin responsible for giving
|
||||
Pods network connectivity and IP addresses. Talos ships Flannel, a CNI, by
|
||||
default — you don't install one yourself in this setup.
|
||||
|
||||
**ConfigMap** — A Kubernetes object that holds configuration data (text,
|
||||
small files) as its own resource, separate from the Pod that uses it. Lets
|
||||
you change configuration without rebuilding a container image. Used for the
|
||||
demo app's HTML page.
|
||||
|
||||
**control plane** — The set of components that run the cluster itself
|
||||
(API server, scheduler, etcd) as opposed to your applications. Normally
|
||||
kept on separate machines from your workloads; this repo's single-node
|
||||
setup deliberately blurs that line for simplicity — see
|
||||
[`talos/patch-controlplane.yaml`](../talos/patch-controlplane.yaml).
|
||||
|
||||
**CRD (Custom Resource Definition)** — A way to teach Kubernetes about a
|
||||
new *kind* of object beyond its built-ins (Pod, Service, etc.). Argo CD's
|
||||
install adds a CRD for `Application`, which is what lets you write
|
||||
`kind: Application` in YAML at all.
|
||||
|
||||
**declarative** — Describing the state you want ("one copy of nginx
|
||||
running"), and letting software figure out and continuously enforce the
|
||||
steps to get and stay there — as opposed to **imperative**, where you issue
|
||||
the individual commands yourself ("start this container now"). Kubernetes,
|
||||
Talos, and Argo CD are all declarative; the one-time install steps in this
|
||||
repo (installing Argo CD itself, applying the app-of-apps) are the
|
||||
deliberate imperative exceptions that get the declarative system started.
|
||||
|
||||
**Deployment** — A Kubernetes object describing how many copies of a
|
||||
container should run and how to run them. You manage Deployments; they, in
|
||||
turn, manage the actual Pods.
|
||||
|
||||
**etcd** — The database Kubernetes' control plane uses to store all
|
||||
cluster state. `talosctl bootstrap` initializes it, once, on cluster
|
||||
creation.
|
||||
|
||||
**GitOps** — Using a Git repository as the source of truth for a system's
|
||||
desired state, with automation (here, Argo CD) continuously reconciling
|
||||
the real system to match it. The core idea this whole repo demonstrates.
|
||||
|
||||
**Ingress** — A Kubernetes object that routes external HTTP(S) traffic to
|
||||
services inside the cluster by hostname/path, via an Ingress controller.
|
||||
Not used in this repo's v1 — see
|
||||
[`docs/08-growing-up.md`](08-growing-up.md).
|
||||
|
||||
**IPAddressPool** — A MetalLB custom resource (see CRD) declaring which IP
|
||||
addresses it's allowed to hand out to `LoadBalancer` Services. Configured
|
||||
in [`apps/metallb/templates/ipaddresspool.yaml`](../apps/metallb/templates/ipaddresspool.yaml)
|
||||
— see [`docs/05-metallb.md`](05-metallb.md).
|
||||
|
||||
**kubeconfig** — A file holding the credentials and connection info
|
||||
`kubectl` (and other Kubernetes tools) use to talk to a specific cluster.
|
||||
Retrieved with `talosctl kubeconfig` in this setup.
|
||||
|
||||
**L2Advertisement** — A MetalLB custom resource that tells it to advertise
|
||||
addresses from a given `IPAddressPool` using ARP (see above) — "Layer 2
|
||||
mode," MetalLB's simplest mode and the one this repo uses. Configured in
|
||||
[`apps/metallb/templates/l2advertisement.yaml`](../apps/metallb/templates/l2advertisement.yaml).
|
||||
|
||||
**LoadBalancer (Service type)** — A Service that requests a real, dedicated
|
||||
external IP address, reachable directly from outside the cluster. On a
|
||||
cloud provider this provisions an actual cloud load balancer; on bare
|
||||
metal (this repo) MetalLB fills that role instead — see
|
||||
[`docs/05-metallb.md`](05-metallb.md).
|
||||
|
||||
**machine config** — Talos's equivalent of a Kubernetes manifest, but for
|
||||
the operating system itself: a declarative file describing how a node
|
||||
should be configured, generated with `talosctl gen config` and sent to the
|
||||
node with `talosctl apply-config`.
|
||||
|
||||
**manifest** — A YAML (or JSON) file describing a Kubernetes object's
|
||||
desired state. Every `.yaml` file in `apps/` is a manifest.
|
||||
|
||||
**MetalLB** — Software installed in this repo (`apps/metallb.yaml`) that
|
||||
gives bare-metal Kubernetes clusters the ability to hand out real IP
|
||||
addresses to `LoadBalancer` Services — a job a cloud provider would
|
||||
normally do. See [`docs/05-metallb.md`](05-metallb.md).
|
||||
|
||||
**namespace** — A way to group related objects inside a cluster, like a
|
||||
folder. Lets you keep different apps' resources organized and manageable
|
||||
independently.
|
||||
|
||||
**node** — A single machine (physical or virtual) that's part of a
|
||||
Kubernetes cluster.
|
||||
|
||||
**Pod** — The smallest deployable unit in Kubernetes: one or more
|
||||
containers that run together, sharing network and storage. You'll rarely
|
||||
create Pods directly — a Deployment creates and manages them for you.
|
||||
|
||||
**reconciliation** — The repeating process of comparing desired state
|
||||
(Git, for Argo CD) against actual state (the live cluster) and correcting
|
||||
any difference. See the diagram in
|
||||
[`docs/00-what-is-all-this.md`](00-what-is-all-this.md#the-reconciliation-loop).
|
||||
|
||||
**Secret** — A Kubernetes object for holding sensitive data. Its values are
|
||||
only base64-*encoded*, not encrypted — never commit a raw Secret manifest
|
||||
containing real credentials to Git. See
|
||||
[`docs/08-growing-up.md`](08-growing-up.md) for how to do this safely.
|
||||
|
||||
**selfHeal** — An Argo CD sync policy setting: if the live cluster drifts
|
||||
from what's in Git (e.g. someone runs `kubectl edit`), Argo CD reverts it
|
||||
back to match Git on its next pass, rather than only fixing drift when you
|
||||
manually trigger a sync.
|
||||
|
||||
**Service** — A Kubernetes object giving a stable network address for a
|
||||
set of Pods, even as individual Pods are created and destroyed. This repo
|
||||
uses a `LoadBalancer` Service for the demo app, backed by MetalLB — see
|
||||
[`apps/demo/service.yaml`](../apps/demo/service.yaml) and
|
||||
[`docs/05-metallb.md`](05-metallb.md).
|
||||
|
||||
**sync** — Argo CD's term for actually applying a change (a reconciliation
|
||||
pass that found and fixed a difference). "Synced" means the cluster
|
||||
currently matches Git.
|
||||
|
||||
**sync-wave** — An Argo CD annotation
|
||||
(`argocd.argoproj.io/sync-wave`) that stages the order resources are
|
||||
applied within one Application's sync: lower numbers first, and Argo CD
|
||||
waits for a wave to be healthy before starting the next. Used in
|
||||
`apps/metallb/templates/` so MetalLB's own resources (including its CRDs)
|
||||
apply before the `IPAddressPool`/`L2Advertisement` that depend on them —
|
||||
see [`docs/05-metallb.md`](05-metallb.md).
|
||||
|
||||
**Talos Linux** — The operating system this repo's node(s) run: built only
|
||||
to run Kubernetes, configured entirely declaratively over the network, with
|
||||
no SSH or shell. See [`docs/02-talos-bootstrap.md`](02-talos-bootstrap.md).
|
||||
|
||||
**talosconfig** — The credentials file `talosctl` uses to talk to a Talos
|
||||
node's API — the Talos-level equivalent of a kubeconfig.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Network map
|
||||
|
||||
Every real IP address this cluster hands out, and exactly what's reachable
|
||||
there. This file isn't a template with the rest of the repo — it's a log
|
||||
of *your* actual setup, meant to be updated every time an address is
|
||||
assigned or changed, so you (or Claude) never have to reverse-engineer
|
||||
"wait, what's running on .241 again?" from `kubectl` output.
|
||||
|
||||
> **Keep this current.** If you're working with Claude and an IP changes —
|
||||
> a new app gets a `LoadBalancer` address, the MetalLB pool is resized,
|
||||
> Argo CD's address changes — ask it to update this file, or just point it
|
||||
> here after any networking change. See `CLAUDE.md` for the standing
|
||||
> instruction that tells Claude to do this on its own when it makes such a
|
||||
> change itself.
|
||||
|
||||
## MetalLB pool
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Range | `EDIT ME — e.g. 192.168.1.240-192.168.1.250` |
|
||||
| Configured in | [`apps/metallb/templates/ipaddresspool.yaml`](../apps/metallb/templates/ipaddresspool.yaml) |
|
||||
|
||||
## Allocated addresses
|
||||
|
||||
| Address | What | Namespace | Port(s) | URL | Source |
|
||||
|---|---|---|---|---|---|
|
||||
| `<NODE_IP>` | Talos node — Kubernetes API server | — | 6443 | `https://<NODE_IP>:6443` | Not from the MetalLB pool — the node's own address, set in [`talos/README.md`](../talos/README.md) |
|
||||
| `EDIT ME` | Argo CD UI/API (`argocd-server`) | `argocd` | 443 | `https://EDIT ME` | Patched to `LoadBalancer` by hand — see [`docs/05-metallb.md`](05-metallb.md) |
|
||||
| `EDIT ME` | Demo app | `demo` | 80 | `http://EDIT ME` | [`apps/demo/service.yaml`](../apps/demo/service.yaml) |
|
||||
|
||||
Add a row here every time you add a new app with a `LoadBalancer` Service
|
||||
(see [`docs/07-adding-new-apps.md`](07-adding-new-apps.md)). Leave the URL
|
||||
column's port number off only when it's the protocol default (80 for
|
||||
`http://`, 443 for `https://`) — include it otherwise.
|
||||
|
||||
## Free in the pool
|
||||
|
||||
Addresses from the range above that aren't spoken for yet. Update this
|
||||
alongside the table above so it's obvious at a glance what's safe to hand
|
||||
to the next app.
|
||||
|
||||
- `EDIT ME`
|
||||
+123
@@ -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 `<NODE_IP>` 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://<NODE_IP>: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://<NODE_IP>: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 <NODE_IP>
|
||||
talosctl config node <NODE_IP>
|
||||
```
|
||||
|
||||
`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 <NODE_IP>
|
||||
```
|
||||
|
||||
Note the disk name you want Talos installed to (e.g. `/dev/sda`). Then:
|
||||
|
||||
```sh
|
||||
talosctl apply-config --insecure \
|
||||
--nodes <NODE_IP> \
|
||||
--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 <NODE_IP>
|
||||
```
|
||||
|
||||
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 <NODE_IP> .
|
||||
```
|
||||
|
||||
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.
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user