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:
@@ -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: []
|
||||
Reference in New Issue
Block a user