Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Recipe reference

Every recipe in this repository, what it does, and which ones need a credential. Use it to find the command; the pages linked from each section explain the procedure around it.

Everything an operator runs goes through just. The root justfile is only a set of mod declarations. The real definitions are one file per namespace under .just/.

just help          # every recipe, namespace by namespace
just ks help       # one namespace's recipes

Arguments are positional: just ks logs media sonarr. A recipe documented as <x> requires that argument, and [<x>] takes an optional one.

ops, the operator machine

RecipeDoes
ops setupEverything below, plus tf init. Run this once on a new workstation
ops depsInstall the toolchain. Needs dnf and uv
ops collectionsansible-galaxy collection install -r requirements.yml
ops hookspre-commit install
ops labelsCreate or update the GitHub labels Renovate applies to its PRs. Needs gh
ops age-keyGenerate the SOPS cluster age keypair. Run once, at cold bootstrap
ops sops [<file>]Edit an encrypted file, seeding it from its .example if absent. No argument lists what is still missing
ops rekeyRe-seal every *.sops.* file to the recipients .sops.yaml names. Run after a key rotation
ops pass-sessionCheck for a Proton Pass session, and explain how to get one
ops meshCheck this machine is on the NetBird mesh, and explain how to join if not

just itself is the one thing ops deps cannot install for you. It has to be there to run the recipe. sudo dnf install just first.

Every tool that is only a binary on PATH is pinned in mise.toml and installed with mise install; the dnf packages, their daemons and the uv tools are the rest of the recipe. The binaries land in ~/.local/share/mise/shims, which Cold bootstrap covers putting on PATH.

The tf init inside ops setup skips any module with a backend.tf while config/sops/ops.sops.yaml does not exist yet, and says so. That is the cold-bootstrap case: setup runs at step 3, the encrypted files land at step 5. Run just tf init <module> for it afterwards.

ops mesh only reports: netbird up is an interactive SSO login, and the admin group it has to land in is filled from the dashboard, not from tofu/netbird. ops deps installs the client either way.

ops age-key is not part of ops setup: it mints key material, so it is deliberately explicit. It prints the public recipient for .sops.yaml and leaves the private key in a temporary file for you to store in Proton Pass and shred.

ops sops takes either name, foo.sops.yaml or foo.sops.yaml.example, and always edits the real file. If it does not exist yet, the template is copied, opened, and encrypted on save. It fails closed: an aborted edit or a failed encrypt deletes the plaintext rather than leaving it at a *.sops.* path.

just ops sops                                 # what is still missing
just ops sops config/sops/ops.sops.yaml           # create it, or edit it

ops rekey re-seals every *.sops.* file to the recipients .sops.yaml currently names, and prints the resulting diff. Run it after editing .sops.yaml, since a rotation that changes the recipients there leaves the files themselves sealed to the old ones.

ans, hosts

RecipeDoes
ans setup [<host>] [<args>]First contact and hardening. Re-runnable
ans k8sConverge the cluster and bootstrap Flux
ans render-secretsResolve the crown jewels into ansible/.generated/secrets.yml
ans pingansible all -m ping
ans lintansible-lint

setup and k8s both depend on render-secrets, so you rarely run it by hand.

setup is variadic after the hostname, so playbooks/setup.yml’s tags are reachable. Pass '' as the hostname to mean all of them:

just ans setup brokkr --tags podman        # one node's container plane
just ans setup '' --skip-tags podman       # the fleet's base layers only
just ans setup kenaz --tags mesh           # re-converge one node's mesh join

fx, Flux

RecipeDoes
fx getKustomizations and their sync status
fx sourcesGitRepository sources
fx hrHelmReleases
fx failingOnly Kustomizations and HelmReleases that aren’t Ready
fx reconcile [<name>]Force-reconcile one, or all of them, --with-source
fx redeploy <name>Force a HelmRelease to reinstall even if its chart is unchanged
fx logs [<controller>]Tail a controller, default kustomize-controller

ks, the cluster

RecipeDoes
ks statusOne screen: nodes, unhealthy pods, Flux sync state
ks nodeskubectl get nodes -o wide
ks pods [<ns>]List pods
ks failingOnly pods not Running with every container ready. Completed Jobs excluded
ks events [<ns>]Recent events, oldest first
ks warnings [<ns>]Warning events only
ks logs <ns> <name>Follow logs. <name> may be deploy/x, job/x, or a pod
ks previous <ns> <pod>A crashed pod’s logs from before its last restart
ks describe <ns> <pod>Describe a pod, with its events
ks restart <ns> <deploy>Roll a Deployment and wait for it
ks jobs [<ns>]Jobs and CronJobs
ks jobs-clean [<ns>]Delete finished Jobs. Running ones are left alone
ks topReal CPU/memory per node, and the 15 hungriest pods
ks ingressIngresses across all namespaces
ks certsCertificates and pending CertificateRequests
ks cli-proxy-login [<provider>]Link a subscription account into cli-proxy-api. Interactive, holds the terminal
ks kctl <args>kubectl passthrough, using the generated kubeconfig

Every ks, fx and bak recipe points KUBECONFIG at ansible/.generated/kubeconfig itself, so you do not need it in your environment. Re-converging the cluster is just ans k8s.

bak, backups

RecipeDoes
bak schedulesEvery Schedule and the jobs it most recently produced
bak snapshotsEvery restic snapshot, which volume it holds and its size
bak ls <id> [<depth>]List a snapshot’s contents and sizes, without restoring it
bak jobsBackup, check and prune jobs, newest last
bak logs <job> <ns>A job’s log, including restic’s summary of what it copied
bak now <ns>Back up a namespace immediately
bak restore <ns>Wipes the namespace’s local-path PVCs and restores them
bak pg-dump [<id>]Write the PostgreSQL dump to a local file. Reads only
bak pg-restoreOverwrites every database in the instance from that dump

A size of 0 in bak snapshots is a run that copied nothing, which is otherwise indistinguishable from one that worked. bak ls shows what such a snapshot does and does not hold; bak logs is where the file and byte counts for a single run are.

bak restore deletes data. It prints which PVCs it will destroy and which it will leave alone, and requires you to type the namespace back before it proceeds. Only local-path PVCs are ever wiped, so the rclone-backed classes are never touched. It is deliberately not reachable from any other recipe, and restores the newest snapshot of each volume. Backup and recovery covers what it does behind that prompt, and why a hand-written Restore is not equivalent.

bak restore postgres does nothing useful: that namespace’s data is a pg_dumpall object rather than a volume snapshot, and K8up cannot restore anything it took from stdin. The two pg-* recipes are its equivalent, going through restic dump and split so the reading half is safe to run on its own. bak pg-restore prompts the same way, then suspends the tenants’ Flux Kustomizations, scales them to zero, replays the dump, and resumes them.

tf, the cloud plane

RecipeDoes
tf init [<module>]All modules if no argument. No secrets, unless the module has a backend
tf plan <module>Plan, through sops --extract and pass-cli run, see below
tf apply <module>Apply
tf adopt <module> <address> <id>tofu import, taking over a resource that exists at the provider
tf validate <module>tofu validate

plan, apply and adopt need a Proton Pass session and the GPG smartcard plugged in, and so does init for a module that ships a backend.tf, because initialising a remote backend authenticates against it. There is no editing secrets from here. Secret values live in Proton Pass, and each module’s tofu.<module> section of config/sops/ops.sops.yaml holds only identifying values and the pass:// references that point at them. Edit it with just ops sops.

docs, this book

RecipeDoes
docs buildBuild into docs/book/, which is gitignored
docs serveServe with live reload and open a browser