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:
Maxwell Morris
2026-08-22 17:30:45 +01:00
co-authored by Claude Sonnet 5
commit 92c3ad3191
29 changed files with 2083 additions and 0 deletions
+27
View File
@@ -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>
+38
View File
@@ -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
+13
View File
@@ -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
+32
View File
@@ -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