homelab/CLAUDE.md
2026-09-16 23:30:38 +09:00

148 lines
7.3 KiB
Markdown

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this is
Infrastructure-as-code for a small K3s homelab. There is no application source
code to build or test here — this repo *is* the infrastructure: Ansible for
host bootstrap, Argo CD for GitOps reconciliation of the cluster, and raw
Kubernetes manifests / Helm values for service configuration. "Testing" a
change means applying it (via `ansible-playbook`, `kubectl`, or letting Argo
CD sync) and checking real cluster/service state — there are no unit tests.
Full operational detail (bootstrap sequence, secrets workflow, storage
layout, URL list, gotchas) lives in the root `README.md` — read it before
making non-trivial changes. Scoped `README.md` files in `ansible/`,
`argocd/`, and `manifests/` cover directory-specific workflows.
## Operating rules for Claude Code
- **There is no staging environment.** Argo CD watches `main` on the Gitea
remote directly, and most `Application`s run with `selfHeal: true` (see
Architecture below) — so `git push` to `main`, especially anything under
`manifests/`, `values/`, or `argocd/`, is a production deploy, not just a
code change, and Argo CD will keep re-asserting it even against manual
cluster edits. Never push to `main` without the user's explicit go-ahead
for that specific push — an earlier approval to commit is not approval to
push.
- **Never run a command that needs the user's own credentials** (API/personal
access tokens, passwords, anything destined for `.env` or an MCP `-e`
flag) directly, and don't hand it back as an inline `! command` either.
Write the exact command to a file (e.g. under the session scratch/tmp
directory — never committed) and ask the user to open it, review it, and
run it themselves. This keeps secrets out of the conversation transcript.
## Architecture
Three-layer flow: **Ansible** brings up hosts and anything that must run
outside Kubernetes → **Argo CD** (`manifests/argocd/app-of-apps.yaml`
`argocd/apps/*.yaml`) reconciles everything else from Git → each
`Application` points at either a raw manifest directory under `manifests/`
or a Helm chart configured by `values/*.yaml`.
Hosts (see `ansible/inventory.yaml`):
| Host | IP | Role |
| --- | --- | --- |
| `minisforum` | `10.10.40.53` | K3s server, Traefik entrypoint, primary app node |
| `debian` | `10.10.40.20` | K3s agent, NFS storage (`/mnt/storage`), secondary Pi-hole |
| `mac-mini` | `10.10.40.30` | Standalone Docker host — Watch Party, Ollama (not in the K3s cluster) |
| `gpu-node` | `10.10.40.12` | K3s agent with NVIDIA GPU, tainted `spot=true:NoSchedule`, labeled `node-role: gpu`; runs Ollama directly on the host, not as a pod |
K3s's bundled Traefik and ingress controller are disabled — Traefik is
installed and managed through Argo CD instead. Internal services live under
`home.arpa` (internal CA via cert-manager, no wildcard DNS — every hostname
must be added explicitly to **both** `values/pihole.yaml` and
`values/pihole-debian.yaml`). Public services live under `nik4nao.com`
(Let's Encrypt).
### Repo layout
| Path | Purpose |
| --- | --- |
| `ansible/` | Host bootstrap and non-Kubernetes services (inventory, roles, playbooks, `group_vars/`, `host_vars/`) |
| `argocd/apps/` | One file per Argo CD `Application` |
| `argocd/values/` | Helm values for installing/reconciling Argo CD itself |
| `manifests/` | Raw Kubernetes resources grouped by service area (see `manifests/README.md`) |
| `values/` | Helm values consumed by Argo CD `Application`s for third-party charts |
| `config/dashy/` | Dashy dashboard config, injected via a script (not the raw manifest) |
| `router/` | OpenWRT (UCI) config backup for the network router — not part of the Ansible/K3s flow |
### Ansible
Three inventory groups (`k3s_server`, `k3s_agents`, `mac_mini`) plus
`gpu_workstation` (see `ansible/inventory.yaml`). Per-host variables live in
`ansible/host_vars/<host>.yaml`; shared/secret vars in
`ansible/group_vars/all/vault.yaml`. Roles are composed per playbook, e.g.
`ansible/playbooks/setup-gpu-node.yaml` runs `common → docker → nvidia →
k3s-agent → ollama → glances` against `gpu_workstation`. Note there are two
`ansible.cfg` files (repo root and `ansible/`) — check which one is active
for the working directory a command is run from before assuming inventory
defaults.
The `ollama` role branches on `ansible_facts['system']` to support both the
Mac Mini (Homebrew + launchd) and Linux GPU nodes (install script +
systemd), so changes to it must keep both code paths working.
### Argo CD
Most `Application`s use automated sync with `selfHeal: true` and
`prune: false` — Argo CD corrects drift but does **not** delete cluster
resources removed from Git; matching manual cleanup is often required.
`targetRevision: main` is the default for repo-managed sources.
## Secrets
Never commit plaintext secrets. Two patterns coexist, both listed in
`manifests/README.md`:
- **Runtime scripts** (`manifests/**/*-secret.sh`) read `.env` (copy from
`.env.example`) and create Kubernetes `Secret`s directly against the live
cluster — nothing plaintext is committed.
- **Sealed Secrets** (`*-sealed.yaml`) are safe to commit; regenerate them
with the matching script and commit the resulting YAML. `kubeseal` must
point at the in-cluster `sealed-secrets-controller` (`kube-system`).
`ansible/group_vars/all/vault.yaml` holds Ansible-side secrets (e.g.
`vault_k3s_node_token`) referenced from `host_vars`.
## Validation commands
No cluster access is required for any of these — run them before reporting a
change done, and show the actual output, not just a claim it passed.
```bash
yamllint -c .yamllint.yml .
ansible-lint ansible/
for pb in ansible/playbooks/*.yaml; do
ansible-playbook --syntax-check -i ansible/inventory.yaml "$pb"
done
kubeconform -summary -ignore-missing-schemas -kubernetes-version 1.32.0 \
-schema-location default \
-schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' \
$(find manifests -name "*.yaml" -not -name "*-sealed.yaml") argocd/apps/*.yaml
```
`ansible-lint` needs the collections in `ansible/requirements.yml` installed
first (`ansible-galaxy collection install -r ansible/requirements.yml`),
otherwise it reports spurious `unknown-module` errors for `community.general`/
`community.docker` modules that are actually fine.
## Key gotchas (see README.md "Gotchas" for the full list)
- Pi-hole has no wildcard DNS — add every new `home.arpa` hostname to both
Pi-hole values files.
- Gitea uses a manual public `IngressRoute`; its Helm-chart ingress is
disabled in `values/gitea.yaml`. Changing `ROOT_URL` may require deleting
the generated inline config secret before Argo CD reconciles cleanly.
- Secondary Pi-hole's external IPs can be lost on chart upgrades — rerun
`manifests/network/pihole-debian-patch.sh`.
- Dashy's manifest ships an empty ConfigMap shell; real config only lands
after running `manifests/core/apply-dashy-config.sh`.
- A newly pushed Argo CD `Application` doesn't create its namespace
instantly — `app-of-apps` only picks it up on its next Git poll. A
namespace-scoped secret script run immediately after pushing can fail
with `namespaces "<ns>" not found` until then (or until a forced
refresh).