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