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:

vars:
  CLUSTER_NAME:
    sh: echo "${CLUSTER_NAME:-current}"
  KUBE_CONTEXT:
    sh: echo "${KUBE_CONTEXT:-admin@${CLUSTER_NAME}}"

includes:
  core: .taskfiles/core
  talos: .taskfiles/talos
  flux: .taskfiles/flux
  k8s: .taskfiles/k8s
  vault: .taskfiles/vault
  ceph: .taskfiles/ceph
  backup: .taskfiles/backup
  tailscale: .taskfiles/tailscale
  cert-manager: .taskfiles/cert-manager
  matrix: .taskfiles/matrix

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

task talos:bootstrap        # Talos-Cluster hochziehen
task k8s:bootstrap          # Kubernetes formen
task flux:bootstrap         # Kontrolle an Flux übergeben
task vault:bootstrap        # Vault initialisieren
task core:cluster-create    # neuen Cluster aus Vorlage scaffolden
task backup:restore         # VolSync-Restore
task tailscale:gen-authkey  # getaggten Pre-Auth-Key minten
task 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":

show-drifts:
  desc: Show resources that drifted from desired state
  cmds:
    - kubectl get events -A --field-selector=reason=DriftDetected
        --sort-by='.lastTimestamp'
        -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:

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

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

force-release:
  desc: Forcefully perform a release install/upgrade
  cmds:
    - kubectl annotate --field-manager=flux-client-side-apply --overwrite
        "helmrelease/{{.RELEASE_NAME}}" -n "{{.NAMESPACE}}"
        "reconcile.fluxcd.io/requestedAt=$(date +%s)"
task 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:

recover-e2ee:
  desc: Recover Matrix E2EE after a crypto-store wipe (mints a NEW device_id)
  prompt: "Mint a NEW Matrix device + token for the bot and rewrite the secret?"
  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.