Von Makefiles zu task: Das Cluster über Anweisungen bedienen

Warum ich die operativen Anweisungen meines Labs von Makefiles auf go-task migriert habe — mit Namespaces, Preconditions, Prompts und einem Überblick, was sich damit alles steuern lässt
Table of contents

Ein Follow-up zum GitOps-Aufbau meines Labs: Wenn das Repository den Zustand beschreibt, brauche ich trotzdem eine Handvoll Anweisungen, um den Cluster zu bedienen — bootstrappen, Secrets rotieren, Backups zurückspielen, Zertifikate erneuern. Lange lebten diese Anweisungen in Makefiles. Heute steckt jede davon in einem go-task -Taskfile, und der Umstieg war eine der besseren kleinen Entscheidungen.

Warum nicht mehr Make#

make ist ein großartiges Werkzeug — für das, wofür es gebaut wurde: Dateien aus anderen Dateien zu erzeugen, anhand von Zeitstempeln. Für operative Anweisungen ist es das falsche Modell. Meine Targets bauen nichts; sie führen Befehle gegen einen Cluster aus. Und genau da reibt sich Make an allen Ecken: bedeutungstragende Tabs, .PHONY-Deklarationen für alles, fummeliges Quoting in Rezept-Zeilen, umständliche Parameterübergabe und eine Lesbarkeit, die mit jedem $(shell …) schlechter wird.

go-task dreht das um. Ein Taskfile ist deklaratives YAML, Parameter und Variablen sind erste Bürger, und es bringt von Haus aus mit, was ich im Cluster-Alltag wirklich brauche: Preconditions, interaktive Prompts vor gefährlichen Schritten, Includes zum Namespacing und sauberes Verschachteln von Tasks. Es ist kein Build-System, das ich zweckentfremde, sondern ein Task-Runner für genau diesen Zweck.

Ein Anweisungs-Verzeichnis mit Namespaces#

Das Root-Taskfile.yaml tut zwei Dinge: Es definiert die geteilten Variablen — Cluster-Name, Kontexte, Pfade zu KUBECONFIG und TALOSCONFIG — und bindet die thematischen Taskfiles als Namespaces ein:

 1vars:
 2  CLUSTER_NAME:
 3    sh: echo "${CLUSTER_NAME:-current}"
 4  KUBE_CONTEXT:
 5    sh: echo "${KUBE_CONTEXT:-admin@${CLUSTER_NAME}}"
 6
 7includes:
 8  core: .taskfiles/core
 9  talos: .taskfiles/talos
10  flux: .taskfiles/flux
11  k8s: .taskfiles/k8s
12  vault: .taskfiles/vault
13  ceph: .taskfiles/ceph
14  backup: .taskfiles/backup
15  tailscale: .taskfiles/tailscale
16  cert-manager: .taskfiles/cert-manager
17  matrix: .taskfiles/matrix

Damit wird task selbst zur Dokumentation — ein nacktes task listet alle Anweisungen mit Beschreibung, gruppiert nach Namespace:

1task talos:bootstrap        # Talos-Cluster hochziehen
2task k8s:bootstrap          # Kubernetes formen
3task flux:bootstrap         # Kontrolle an Flux übergeben
4task vault:bootstrap        # Vault initialisieren
5task core:cluster-create    # neuen Cluster aus Vorlage scaffolden
6task backup:restore         # VolSync-Restore
7task tailscale:gen-authkey  # getaggten Pre-Auth-Key minten
8task matrix:recover-e2ee    # Matrix-E2EE des Bots wiederherstellen

Der gesamte Onboarding-Aufwand für eine neue Maschine schrumpft damit auf direnv allow && task — der erste Befehl lädt die gepinnte Toolchain aus shell.nix, der zweite zeigt, was es zu tun gibt.

Was ein Task ausmacht#

Im einfachsten Fall ist ein Task nur ein benanntes Bündel Befehle mit Beschreibung — etwa „zeige mir, wo der Cluster vom Soll-Zustand abgewichen ist":

1show-drifts:
2  desc: Show resources that drifted from desired state
3  cmds:
4    - kubectl get events -A --field-selector=reason=DriftDetected
5        --sort-by='.lastTimestamp'
6        -o custom-columns=NAMESPACE:.metadata.namespace,NAME:.involvedObject.name

Spannender sind die Mechanismen, an denen Make scheiterte. Preconditions brechen früh und mit klarer Meldung ab, statt mitten im Lauf zu zerschellen:

1bootstrap:
2  desc: Bootstrap Flux
3  preconditions:
4    - flux check --pre
5    - sh: kubectl config get-contexts "{{.KUBE_CONTEXT}}"
6      msg: "Kubectl context {{.KUBE_CONTEXT}} not found"
7  cmds: [ … ]

Parameter kommen sauber als Variablen herein, ohne Quoting-Akrobatik — hier ein gezieltes Force-Release einer einzelnen Ressource:

1force-release:
2  desc: Forcefully perform a release install/upgrade
3  cmds:
4    - kubectl annotate --field-manager=flux-client-side-apply --overwrite
5        "helmrelease/{{.RELEASE_NAME}}" -n "{{.NAMESPACE}}"
6        "reconcile.fluxcd.io/requestedAt=$(date +%s)"
1task flux:force-release RELEASE_NAME=zot NAMESPACE=zot

Und für die wirklich gefährlichen Anweisungen — alles, was Daten überschreibt oder Identitäten rotiert — schiebt go-task einen Prompt davor, der ohne --yes interaktiv bestätigt werden will:

1recover-e2ee:
2  desc: Recover Matrix E2EE after a crypto-store wipe (mints a NEW device_id)
3  prompt: "Mint a NEW Matrix device + token for the bot and rewrite the secret?"
4  cmds: [ … ]

Genau dieser Prompt rettet einen davon, das Matrix-E2EE-Recovery versehentlich doppelt auszuführen.

Ein grober Abriss, was geht#

Über die Namespaces zieht sich der komplette Lebenszyklus des Labs:

NamespaceBeispieleZweck
talosbootstrap, cluster-gen-configsTalos-Knoten ausrollen, Configs erzeugen
k8s / fluxbootstrap, force-release, show-driftsKubernetes formen, Flux steuern
vaultbootstrap, renew-tokens, gen-certVault initialisieren, Tokens/Zertifikate
cert-managerrenew-all-certsTLS-Material erneuern
backuplist, snapshot, restoreVolSync-Restic -Backups
tailscalegen-authkey, prune-stale-gatewayHeadscale -Keys & Tailnet
matrixrecover-e2ee, prune-bot-devicesMatrix-Bot-Wartung
corecluster-create, update-gpg-keysCluster-Vorlage, SOPS-Recipients

Der schönste Nebeneffekt: Auch ZeroClaw führt aus seinem Pod heraus task in einem Repo-Klon aus. Mensch und Agent bedienen den Cluster damit über exakt dieselben Anweisungen — es gibt keine zweite, undokumentierte „so macht man das wirklich"-Ebene.

Fazit#

Der Wechsel von Make zu go-task hat keine einzige Fähigkeit gekostet, dafür aber Lesbarkeit, sichere Parameter, Preconditions und Prompts gebracht. Aus einer Sammlung kryptischer Targets wurde ein durchsuchbares Anweisungs-Verzeichnis, das sich selbst dokumentiert und das ich einem Agenten genauso in die Hand geben kann wie mir selbst. Für ein deklaratives Lab ist das genau die richtige Imperativ-Schicht: klein, explizit und jederzeit per task auffindbar.