Die anderen Anleitungen erklären Schritt für Schritt. Diese hier zeigt das
Zusammenspiel – achtunddreißig Zeichnungen, kein einziger Befehl. Zuerst die Klammer:
dieselbe Anwendung, zwei Ziele, und was zwischen den beiden Welten
tatsächlich fließt. Danach die Hetzner-Landschaft, danach der CRC-Cluster mit der
Bibliothek darauf, dann die Bauteile von innen, zuletzt vier zum Nachschlagen. Jede Zeichnung beantwortet genau eine Frage; was darunter steht,
ist die Antwort in einem Satz.
Die Farben sind kein Schmuck – derselbe Ton bedeutet in allen Zeichnungen dasselbe:
Netz, Transport, Verbindung
Daten und Speicher
Ereignis, asynchron
Identität und Token
der Weg, der trägt
die Grenze nach aussen – und wo es bricht
Stand: 2. September 202638 Diagramme, 0 BefehleWerte aus den Anleitungen und dem Projekt-Repository
01 Das Zusammenspiel
Bereit: CRC, GitHub, ghcr.io, Tailnet und Hetzner
Die einzige Frage, die diese Seite überhaupt zusammenhält:
Was verbindet den Laptop-Cluster mit den Servern in Finnland?
In diesem Betriebsmodell haben die beiden Welten zwei technische Haupt-Beruehrungspunkte: den Build auf dem Laptop, der
dasselbe Image in zwei getrennte Registries schiebt – ghcr.io für Hetzner, die
clusterinterne Registry für CRC, die den Laptop nie verlässt – und das Tailnet, durch das der
Laptop den Server bedient. Alles andere laeuft getrennt. Deshalb kann ein Bauteil ueberhaupt umziehen, ohne dass der Rest davon erfaehrt –
und deshalb entscheidet allein die Latenztoleranz, welches.
Teil A – die Hetzner-Landschaft
02 Netz & Ports
Bereit: Firewall, Tailnet und offene Dienste
Wer darf womit reden – und warum die Firewall vor dem Server sitzt und nicht darauf.
Die Firewall filtert im Netz, bevor der Verkehr den Server erreicht – sie kostet
deshalb keine Rechenleistung und wirkt weiter, wenn auf dem Server etwas kaputt ist. Sie ist eine reine
Erlaubnisliste ohne Reihenfolge. Der gruene Weg ist der interessante: das Tailnet baut die Verbindung von
innen nach aussen auf, also braucht es dafuer gar keine eingehende Regel.
03 Ein Host, mehrere Stacks
Bereit: Reverse Proxy und getrennte Compose-Netze
Warum keiner der Dienste einen Port auf dem Wirt veroeffentlicht – und was das gemeinsame Netz damit zu tun hat.
Nur der Proxy veroeffentlicht Ports. Alle anderen Container sind ausschliesslich ueber
ihren Dienstnamen im gemeinsamen Netz erreichbar – Docker haelt dafür einen eigenen DNS-Resolver bereit,
keine externe Konfiguration nötig. Die MariaDB
hängt zusätzlich in einem zweiten Netz, das der Proxy nicht kennt: damit ist sie für ihren
Stack da und für sonst niemanden, auch nicht für die anderen Stacks. Vaultwarden braucht das
nicht – seine Datenbank ist kein Dienst, sondern eine Datei.
04 Der Deploy-Weg
Vom git push bis zum laufenden Container – und an welcher Stelle das Geheimnis liegt.
Bereit: git push auf main
Der Server zieht das Image, er baut es nicht. Damit braucht er weder Quellcode noch
Build-Werkzeuge noch Zugangsdaten zum Repository. Der Tag ist der Commit-Hash – das ist die einzige
Stelle, an der der Bezug zwischen laufendem Container und Quellstand entsteht; latest waere
bequemer und wuerde genau diese Zuordnung zerstoeren.
05 Server-Lebenszyklus
Bereit: Snapshot prüfen, erst dann löschen
Der Zyklus, mit dem ein Lernserver nur dann Geld kostet, wenn er laeuft.
Die Raute traegt den ganzen Zyklus. Erst wenn der Snapshot nachweislich gelungen ist,
wird der Server geloescht – scheitert er, bleibt alles stehen. Ohne diese Pruefung waere das Skript
eine Maschine zum Datenverlust. Die reservierte IP ist der zweite Grund, warum der Zyklus im Alltag
funktioniert: DNS, fremde Firewall-Regeln und known_hosts stimmen nach dem Wiederaufbau weiter.
06 Backup-Wege
Bereit: Dumps und Dateien in restic
Warum eine laufende Datenbank nie als Datei gesichert wird – und wo der Dump entlanglaeuft.
Datenbanken werden gedumpt, nie als Datei kopiert. Eine laufende Datenbank hat Dateien
in einem Zustand, aus dem sich nichts zuverlaessig wiederherstellen laesst. Der Dump geht direkt in die
Pipe nach restic, also ohne Zwischendatei – kein Platzbedarf und nichts, was jemand liegen laesst.
07 DNS & Zertifikate
Bereit: direkt zum Server oder über Cloudflare
Der Unterschied zwischen grauer und oranger Wolke – und wann die orange schadet.
Der Proxy ist eine Eigenschaft des einzelnen Records, nicht der Domain. Fuer
blog und www ist er sinnvoll, fuer einen Record, hinter dem SSH oder Mail
liegt, macht er den Dienst unerreichbar – und der Fehler sieht dann nach einem Serverproblem aus.
08 Zugangswege
Bereit: SSH, Tailnet und Hetzner-Konsole
Drei Wege hinein – und welcher noch funktioniert, wenn die anderen versagen.
Wer SSH auf die eigene Adresse begrenzt, sperrt sich irgendwann selbst aus. Das ist
kein Argument gegen die Regel – sondern eines dafuer, den dritten Weg zu kennen, bevor man ihn braucht.
Die Hetzner-Konsole haengt am virtuellen Bildschirm der Maschine und ist von Firewall, Netz und
SSH-Konfiguration voellig unabhaengig.
Teil B – der CRC-Cluster und die Bibliothek darauf
09 Netz & Ports der CRC-VM
Bereit: Windows, WSL2, CRC-VM und Ports
Drei Maschinen auf einem Laptop, die sich gegenseitig fuer entfernte Rechner halten.
Drei Netzwelten auf einem Geraet. Die CRC-VM, der Windows-Host und die WSL-Maschine
sind fuereinander entfernte Rechner – localhost bedeutet in jeder etwas anderes. Der
gruene Weg funktioniert nur, weil CRC den Port 443 nicht auf Loopback beschraenkt.
10 Was wo laeuft
Bereit: Routen, Services und Infrastruktur
Zwoelf Pods in einem Namespace, dazu das Frontend hinter seiner Route – und warum sie nicht gleichzeitig starten duerfen.
Nur drei Adressen zeigen nach draussen. Die sechs Fachdienste und der config-server sind
ausschliesslich clusterintern erreichbar, ueber ihren Servicenamen; von den acht Spring-Diensten hat allein
das api-gateway eine Route. Der gruen markierte config-server ist die einzige echte Startreihenfolge im
ganzen Stack – alles andere darf sich sortieren, wie es mag.
11 Eine Ausleihe von vorn bis hinten
Fuenf Dienste, sieben Aufrufe, ein Kompensationsschritt – und die Reihenfolge ist nicht beliebig.
Bereit: POST /loans mit Token
Zwei Dienste werden gefragt, bevor irgendetwas geschrieben wird. Der einzige Schritt,
der einen fremden Dienst veraendert, ist markCopyLoaned – und weil danach noch das lokale
Speichern scheitern kann, gibt es dafuer eine Ruecknahme. Diese eine Zeile ist der Grund, warum die
Ausleihe ohne verteilte Transaktion auskommt.
12 Zustaende: Exemplar und Ausleihe
Bereit: AVAILABLE, LOANED, LOST, ACTIVE und RETURNED
Zwei kleine Automaten in zwei verschiedenen Diensten – die zusammenpassen muessen, ohne sich zu kennen.
Die beiden Automaten kennen einander nicht. Dass ein Exemplar LOANED ist
und genau eine Ausleihe darauf ACTIVE, ist keine Datenbankbedingung, sondern eine Zusage
zwischen zwei Diensten. Wo eine solche Zusage nur in Anwendungscode steht, kann sie brechen – die
naechste Zeichnung zeigt, wie. Nebenbei sichtbar: RESERVED und IN_REPAIR sind
im Enum vorgesehen, aber kein Pfad setzt sie – und markCopyLost prueft den
Ausgangszustand nicht.
13 Das Rennen – vorher und nachher
Die Zeichnung, um die es auf dieser Seite eigentlich geht.
Bereit: A und B lesen AVAILABLE
Links passt zwischen Lesen und Schreiben eine zweite Anfrage, rechts nicht. Das ist der
gesamte Unterschied zwischen den beiden Fassungen – eine Bedingung, die von der Anwendung in die
UPDATE-Anweisung wandert. Die Datenbank kann zwei Anweisungen serialisieren; zwei getrennte Zugriffe
kann sie nicht zusammenbinden.
14 Anmeldung – und die Falle im Token
Bereit: Keycloak-Token und issuer-Claim
Warum ein einziges fehlendes „s“ jede Anfrage mit 401 beendet.
Zwei Adressen fuer dieselbe Keycloak, und beide muessen unterschiedlich sein. Der
iss-Claim entsteht aus dem Host-Header der Anmeldung, also mit https. Steht in
der Konfiguration http, schlaegt der Vergleich fehl – und die Fehlermeldung spricht von
einem ungueltigen Token, nicht von einem Tippfehler.
15 Circuit Breaker – und was er faelschlich gemessen hat
Bereit: CLOSED, OPEN, HALF_OPEN und 422→503
Ein Schutzmechanismus, der eine fachliche Ablehnung fuer einen Ausfall hielt.
Ein Circuit Breaker misst, was man ihm als Fehler hinstellt. Hier war das eine
fachliche Ablehnung – und die Rueckmeldung an den Nutzer wurde dadurch nicht nur falsch, sondern
irrefuehrend: sie zeigte auf die Infrastruktur, waehrend der Fehler im Anwendungscode sass.
16 Datenhoheit
Bereit: sieben Datenbanken, Kafka und HTTP
Sieben Datenbanken in einer Instanz – und die einzige erlaubte Querverbindung.
Eine Instanz, sieben Eigentuemer. Die gemeinsame Postgres spart Speicher und Startzeit;
die Trennung nach Datenbanken haelt trotzdem durch, dass jeder Dienst nur seine eigenen Tabellen kennt.
Informationen fliessen ueber Kafka oder HTTP – und nur der rote Kasten ist der Weg, auf dem ein Dienst
einen anderen mit herunterreissen kann.
Teil C – die Bauteile von innen
In den Zeichnungen davor sind Container, Kafka, Keycloak, DNS
und TLS einzelne Kästen. Hier wird jeder davon aufgeklappt – erst die Grundbegriffe, auf
denen alles andere steht, dann die Bauteile. Nicht als Lehrbuchkapitel, sondern genau so weit, wie
es nötig ist, um die Fehlermeldungen zu verstehen, die dieser Stack tatsächlich produziert.
17 VM, Container, WSL – was wirklich getrennt ist
Der Unterschied, aus dem sich fast alles Weitere ergibt: wie viele Kerne laufen hier eigentlich?
Bereit: VM, Container und WSL2
Ein Container ist keine kleine VM. Er ist ein Prozess auf dem Kernel des
Wirts, dem man eine eigene Sicht auf Dateisystem, Netz und Prozessliste gegeben hat. Das macht
ihn schnell und klein – und erklärt zugleich, warum ein Zugriff auf den
Container-Socket gleichbedeutend mit Root auf dem Wirt ist.
18 Image und Container
Warum Daten im Container verschwinden, was ein Tag wirklich ist, und wieso ein Rebuild oft Sekunden dauert.
Bereit: Dockerfile → Image → Container
Das Image ist unveränderlich, der Container ist vergänglich. Alles,
was ein Container schreibt, landet in einer dünnen Schicht obendrauf, die mit ihm
verschwindet. Wer das verinnerlicht hat, versteht ohne weitere Erklärung, warum es Volumes
gibt, warum ein Tag sich ändern muss und warum Container-Images so klein sein können.
19 Registries: wo die Images liegen
Was ghcr.io ist, warum drei verschiedene Registries im Spiel sind – und warum eine davon zwei Adressen hat.
Bereit: Name, Registry und Pull
Ein Lager, kein Dienst. Die Registry speichert Schichten unter ihrer
Prüfsumme und Manifeste unter einem Namen – mehr nicht. Welche man nimmt, ist eine
Frage von Anmeldung, Erreichbarkeit und Abrufgrenzen: ghcr.io für die eigenen
Images, docker.io für fremde, die Cluster-Registry für alles, was den
Laptop nicht verlassen soll.
Bereit: Docker-Daemon gegen rootless Podman
20 Docker und Podman
Derselbe Befehlssatz, ein grundlegend anderer Aufbau – und warum das für den Jenkins-Server wichtig war.
Derselbe Befehlssatz, ein anderer Aufbau.alias docker=podman
funktioniert für den Alltag – die Unterschiede zeigen sich erst dort, wo es um
Rechte, Neustarts und Werkzeuge geht, die einen Daemon erwarten. Genau diese drei Stellen sind
es, an denen die Anleitungen dieser Sammlung nachhelfen müssen.
Bereit: Compose-Dienst gegen Kubernetes-Objekte
21 Compose und Kubernetes
Dieselbe Idee in zwei Größen – und die eine Zeile, für die es in Kubernetes bewusst keine Entsprechung gibt.
Compose beschreibt einen Rechner, Kubernetes einen Sollzustand. Fast jede
Zeile hat eine Entsprechung – nur wird sie auf mehrere Objekte verteilt, die man einzeln
ändern kann. Die Ausnahme ist depends_on: was dort eine Zeile ist, wird hier
zur Anforderung an den Dienst selbst.
22 Kubernetes von innen
Keine Befehle, die ausgeführt werden – nur Schleifen, die vergleichen. Das ist der ganze Trick.
Bereit: Sollzustand an API-Server geben
Kubernetes führt nichts aus – es gleicht ab. Du hinterlegst einen
Sollzustand, und mehrere unabhängige Schleifen vergleichen ihn unablässig mit dem Ist.
Wer das einmal verinnerlicht hat, wundert sich nicht mehr, warum ein gelöschter Pod
zurückkommt, warum ein Befehl sofort zurückkehrt, obwohl nichts fertig ist, und
warum Handarbeit am Objekt nicht hält.
23 Wie eine Anfrage im Cluster ihr Ziel findet
Vom Hostnamen bis zum Prozess – und die Stelle, an der die Readiness-Probe darüber entscheidet, ob es klappt.
Bereit: Route → Service → nur bereite Endpoints
Der Service ist ein Name mit einer Mitgliederliste. Was in dieser Liste steht,
entscheidet allein die Readiness-Probe – nicht ob der Pod läuft. Diese eine Trennung
erklärt sowohl das Ausrollen ohne Ausfall als auch die häufigste Verwirrung im
Betrieb: ein Pod, der läuft, und trotzdem antwortet niemand.
24 Ingress und Route
Zwei Wege, dasselbe zu erreichen – und in diesem Projekt laufen beide: Routes auf CRC, Ingress auf k3s.
Bereit: zwei Eingänge, derselbe Service-Weg
Der Unterschied betrifft nur die Tür, nicht das Haus. Route und Ingress
beschreiben dasselbe – welcher Hostname zu welchem Service gehört – und ab dem
Service ist der Weg identisch. Die Fallen liegen beide darin, dass ein Objekt gültig sein
kann und trotzdem niemanden hat, der es ausführt.
25 ConfigMap und Secret
Derselbe Aufbau, ein Unterschied – und drei Wege hinein, von denen nur einer Aktualisierungen mitbekommt.
Bereit: ConfigMap, Secret und drei Pod-Zugriffe
Ein Secret ist eine ConfigMap mit anderen Rechten. Base64 versteckt nichts;
der Gewinn liegt allein darin, dass sich der Zugriff getrennt vergeben lässt. Wichtiger als
der Unterschied zwischen beiden ist der Unterschied zwischen den drei Wegen hinein – denn
nur der eingehängte bekommt mit, wenn sich etwas ändert. Für vertrauliche Daten braucht es
zusätzlich Verschlüsselung-at-Rest in etcd und restriktives RBAC.
Bereit: Templates, Werte, set und Release-Secret
26 Helm von innen
Eine Schablone, mehrere Wertedateien, ein Ergebnis – und die Stelle, an der ein Tippfehler wortlos verschluckt wird.
Helm läuft auf deinem Rechner, nicht im Cluster. Es setzt Schablonen und
Werte zu gewöhnlichem YAML zusammen und schickt nur das Ergebnis – plus eine Kopie
davon als Secret, damit es später weiß, was es zuletzt getan hat. Alles, was Helm
„kann“, ist Textersetzung und Buchführung.
Bereit: YAML, Trigger, Socket und Build
27 Jenkins von innen
Wo die Konfiguration herkommt, wo der Build wirklich läuft – und warum Klicks in der Oberfläche nicht halten.
Jenkins ist hier bewusst austauschbar gehalten. Pipeline und Konfiguration
liegen als Dateien im Repository, der Build läuft auf der Engine des Wirts, der Zustand
beschränkt sich auf ein Verzeichnis. Was das kostet, steht rot rechts: der
hineingereichte Socket ist gleichbedeutend mit Root auf dem Server – tragbar, solange
dort nur eigene Pipelines laufen.
Bereit: Commit, Parent, Branch und Fast-Forward
28 Git von innen
Ein Commit ist kein Unterschied, sondern ein vollständiger Stand – und ein Branch ist nur ein Zettel mit einer Nummer darauf.
Commits sind Stände, keine Änderungen. Jeder enthält den
vollständigen Dateibaum und zeigt rückwärts auf seinen Vorgänger; ein Branch
ist nur ein Zeiger darauf. Sobald man das so sieht, ist Fast-Forward keine Zauberei mehr,
sondern das Naheliegende: der Zeiger wandert, weil es einen geraden Weg gibt.
Bereit: Partition, Offset, Producer und Consumer-Gruppe
29 Kafka von innen
Was „asynchron“ konkret bedeutet – und warum ein gelesenes Ereignis nicht verschwindet.
Kafka ist ein Protokoll, keine Warteschlange. Ereignisse werden hinten
angehängt und bleiben liegen; jede Lesergruppe merkt sich nur ihre eigene Position. Daraus
folgt beides: der Sender wird nie ausgebremst – und ein Fehler beim Empfänger
fällt niemandem auf, solange keiner die Leseposition beobachtet.
Bereit: Browser, Code, Verifier und Tokens
30 Keycloak: der Anmeldefluss, Schritt für Schritt
Neun Schritte, zwei Umleitungen – und die eine Stelle, an der die
redirectUris aus Zeichnung 14 geprüft werden.
Das Passwort erreicht die Anwendung nie. Es geht ausschliesslich an Keycloak;
zurück kommt erst ein kurzlebiger Code und dafür dann ein Token. Die beiden Umleitungen
sind der Grund, warum redirectUris und webOrigins stimmen müssen
– Schritt 3 prüft die redirect_uri zeichengenau, noch vor der
Anmeldemaske. Passt sie nicht, erscheint Invalid parameter: redirect_uri statt des Logins,
und es kommt gar kein Code zurück.
Bereit: Header, Payload, Signatur und JWKS
31 Das Token von innen
Drei Teile, ein Punkt dazwischen – und warum jeder es lesen kann, aber niemand es fälschen.
Ein Token ist eine unterschriebene Behauptung, kein Geheimnis. Jeder kann
lesen, was drinsteht – niemand kann es ändern, ohne die Unterschrift zu zerstören.
Deshalb kommt in die Nutzlast nichts hinein, was niemanden angeht, und deshalb kommen die
Fachdienste ohne Rückfrage bei Keycloak aus.
32 DNS: wie ein Name zur Adresse wird
Drei Fragen, bis der Browser weiss, wohin – und warum eine Änderung nicht sofort wirkt.
Bereit: blog.seb4u.com? an UDP 53
Niemand kennt die ganze Antwort – jeder kennt den Nächsten. Das macht
DNS robust und zugleich träge: zwischen Änderung und Wirkung liegt die TTL, und in
dieser Zeit sehen verschiedene Leute verschiedene Wahrheiten. Wer das einkalkuliert, senkt die
TTL vorher – wer nicht, sucht den Fehler im Server.
33 TLS: Handshake und Zertifikatskette
Was in den ersten Millisekunden passiert – und warum der Browser dem Zertifikat überhaupt glaubt.
Bereit: ClientHello mit SNI
Verschlüsselung ist der leichte Teil – die Frage ist, wem man glaubt.
Die Kette endet immer bei einem Zertifikat, das schon im Gerät liegt. Wo TLS endet, ist
keine Formalie: bei edge spricht der Pod danach unverschlüsselt, und alles,
was er über die ursprüngliche Anfrage wissen soll, muss ihm in Kopfzeilen mitgegeben
werden.
34 Let's Encrypt: wie ein Zertifikat entsteht
Warum Port 80 offen sein muss, obwohl alles auf 443 umgeleitet wird.
Bereit: Zertifikat für blog.seb4u.com anfordern
Der Beweis ist die ganze Zeremonie. Ein Zertifikat bekommt nur, wer zeigen
kann, dass ihm der Name gehört – und der einfachste Beweis ist eine Datei, die unter
genau diesem Namen abrufbar ist. Deshalb hängt an Port 80 mehr als eine Umleitung.
Teil D – Zahlen und Nachschlagen unter Druck
Die Zeichnungen davor zeigen Aufbau. Diese vier zeigen
Zeit, Haltbarkeit, Symptome und Leckstellen – und sie
arbeiten mit den Werten, die auf diesem Laptop tatsächlich gemessen wurden, nicht mit
Beispielzahlen.
35 Wo die Zeit hingeht
Derselbe Stack, zweimal hochgefahren – einmal alle auf einmal, einmal
nacheinander. Alle Zahlen aus crc-monitor-*.csv und rollout-*.csv.
Bereit: gleichzeitiger Start gegen gestaffelten Rollout
Dieselbe Arbeit, ein Fünftel der Zeit. Der gestaffelte Rollout ist nicht
vorsichtiger, er ist schneller – 32 Minuten gegen zweieinhalb Stunden ohne
vollständiges Ergebnis, also rund 21 %. Und er zeigt zugleich, wo man ansetzen müsste:
zwei der neun Dienste verbrauchen knapp zwei Drittel der Gesamtzeit (1190 von 1912 s, 62 %).
36 Was überlebt was
Die Frage, die in sechs anderen Zeichnungen nebenbei vorkommt – hier einmal vollständig.
Bereit: Was überlebt einen Container-Neustart?
Fünf Ereignisse, sieben Aufbewahrungsorte – und drei überstehen
alles. Der rot umrandete Kasten ist der Fall, der in dieser Sammlung tatsächlich
eingetreten ist: emptyDir gegen „Pod neu erstellt“. Kein Fehler im
Betrieb, sondern eine bewusst gesetzte Einstellung, deren Reichweite unterschaetzt wurde.
37 Fehlerbild → wo suchen
Nicht erfunden: jede Meldung hier ist in dieser Sammlung mindestens einmal
tatsächlich aufgetreten.
Vom Symptom zur Stelle, an der man nachsieht. Die drei Bänder sind nach
dem sortiert, was man zuerst sieht: eine HTTP-Antwort, ein Pod-Zustand, eine Meldung beim
Bauen. Der wichtigste Eintrag ist der doppelte 503 – dieselbe Meldung, zwei
völlig verschiedene Ursachen, und die zweite kostet Stunden, wenn man sie nicht kennt.
38 Wo ein Passwort überall liegt
Ein Wert, acht Stationen – sechs sichtbare und zwei, an die man nicht denkt – und an jeder eine andere Art, ihn zu verlieren.
Bereit: vom Rechner bis in den Prozess
Acht Stationen, und nur eine davon ist wirklich sicher. Interessanter als
der Weg selbst sind die beiden Stationen unten: Image-Schicht und Helm-Verlauf halten einen
Wert fest, nachdem man ihn geändert hat. Beide sieht man nicht, wenn man nur den Weg
von links nach rechts betrachtet.