Case: CI/CD Build Platform
Shared CI Infrastructure für Enterprise.
Lernziele
Nach dieser Lektion kannst du eine geteilte CI/CD-Build-Plattform für 200 Repositories designen: Self-hosted Runner auf Kubernetes mit ephemeren Build-Umgebungen, Container-Builds ohne privilegierten Docker-Daemon und eine Cache-Strategie, die den Sicherheitsgewinn der Ephemeralität nicht wieder auffrisst. Du kannst das Spannungsfeld des Cases — Geschwindigkeit gegen Isolation — explizit machen und für beide Seiten konkrete Mechanismen nennen.
Das Szenario
„Designe eine zentrale CI/CD-Plattform für 200 Repositories und rund 500 Builds pro Tag. Builds sollen schnell sein, Caching nutzen und strikt voneinander isoliert laufen.” Der Satz enthält den Konflikt schon: Schnell heißt Zustand wiederverwenden, isoliert heißt Zustand wegwerfen. Caches, warme Runner und geteilte Daemons machen Builds schnell — und sind gleichzeitig die Wege, über die ein kompromittierter Build den nächsten infiziert. Der Interviewer will sehen, ob du diesen Zielkonflikt benennst und pro Mechanismus entscheidest. Und das Framing dazu: CI-Systeme führen per Definition fremden Code aus und tragen Build-Credentials — ein Lieblingsziel für Supply-Chain-Angriffe.
Anforderungen klären: Lastprofil, Bestand, Vertrauensgrenzen
Vier Fragen mit direkten Designkonsequenzen:
- „Wo liegt der Code, was ist gesetzt?” — Angenommen: GitHub mit GitHub Actions als Workflow-Engine. Damit lautet die echte Frage nicht „welches CI-Tool”, sondern „GitHub-hosted oder self-hosted Runner”.
- „Wie sieht das Lastprofil aus?” — 500 Builds/Tag, im Schnitt ~8 Minuten, ergibt rund 4.000 Build-Minuten täglich — aber nicht gleichverteilt: Builds ballen sich werktags tagsüber, mit Spitzen von vielleicht 30–50 parallelen Jobs nach dem Mittag-Merge-Fenster. Konsequenz: Die Plattform muss elastisch skalieren; statisch dimensionierte Runner sind nachts Verschwendung und mittags ein Engpass.
- „Was wird gebaut?” — Angenommen: überwiegend Container-Images (JVM, Node, Go), dazu Tests. Container-Builds sind der heikle Teil, weil sie traditionell einen Docker-Daemon wollen.
- „Brauchen Builds Zugriff auf interne Systeme?” — Angenommen: ja, interne Registry und Artefakt-Repos. Das ist ein Hauptargument für self-hosted Runner — GitHub-hosted Runner stehen außerhalb des Netzwerks.
Fasse zusammen: „Self-hosted Runner auf Kubernetes, elastisch skaliert, Container-Builds ohne privilegierten Daemon, Caching extern — einverstanden?”
Architektur: ephemere Runner, daemonlose Builds, Cache als externer Dienst
Runner-Schicht: GitHub Actions mit Actions Runner Controller (ARC) auf einem dedizierten Build-Cluster. Der entscheidende Modus: ephemere Runner — pro Job ein frischer Pod, der danach verworfen wird. Begründung laut aussprechen: Ein persistenter Runner akkumuliert Zustand, und jeder Build erbt, was der vorherige hinterlassen hat — von vollgelaufenen Disks bis zu absichtlich platzierten Hintertüren. Ephemeralität macht jede Job-Umgebung reproduzierbar und entwertet Persistenz-Angriffe. ARC skaliert nach Warteschlangenlänge, der Cluster-Autoscaler liefert Nodes nach; für den Großteil der Builds eignen sich Spot-Instances, weil ein abgebrochener Build einfach neu startet — Release-kritische Pipelines laufen auf On-Demand-Kapazität.
Build-Schicht: Container-Images ohne Docker-Socket. Docker-in-Docker braucht privilegierte Pods — ein privilegierter Container, der fremden Code ausführt, ist faktisch Root auf dem Node und damit das Ende jeder Isolationsgeschichte. Stattdessen daemonlose bzw. rootless Builder: Kaniko oder BuildKit im Rootless-Modus bauen Images aus dem Dockerfile und pushen direkt in die Registry (z. B. Harbor), ohne erhöhte Rechte. Ehrliche Einschränkung dazu: Workloads, die echte Container zur Laufzeit brauchen — Integrationstests mit Testcontainers etwa — passen nicht in dieses Schema. Dafür gibt es einen separaten, härter isolierten Runner-Pool (dedizierte Nodes, ggf. Sandbox-Runtime), als dokumentierte Ausnahme statt als Aufweichung des Standards.
Cache-Schicht: Hier holst du die Geschwindigkeit zurück, die Ephemeralität kostet. Dependency-Caches (Maven, npm, Go-Module) liegen in S3 und werden pro Job geladen; Docker-Layer-Caching läuft registry-basiert, indem BuildKit/Kaniko Cache-Layer in die Registry schreiben und beim nächsten Build importieren. Dazu ein Pull-Through-Cache für Basis-Images, damit nicht 500 Builds täglich dieselben Layer aus dem Internet ziehen. Wichtig ist die Sicherheitsregel dahinter: Caches werden nach Vertrauensgrenze getrennt — ein Build aus einem Fork-PR schreibt niemals in den Cache, den Main-Branch-Builds lesen. Cache-Poisoning ist der klassische Weg, Ephemeralität zu umgehen.
Credentials und Übergabe: Runner-Pods laufen in eigenen Namespaces mit default-deny-NetworkPolicies — erreichbar sind Registry, Artefakt-Repos und das Internet für Dependencies, nicht aber die Workload-Cluster. Cloud-Zugriffe laufen über OIDC-Federation statt langlebiger Keys: Der Job tauscht sein kurzlebiges Token gegen eine Cloud-Rolle. Und die wichtigste Grenze: CI deployt nicht. Die Pipeline baut, testet, pusht und aktualisiert den Image-Tag im GitOps-Repo — den Rest macht Argo CD. Damit braucht kein Runner Credentials für Produktionscluster, und der Blast Radius eines kompromittierten Builds endet an der Registry.
Einwände des Interviewers
„Ephemere Runner ohne lokalen Docker-Cache — werden die Builds nicht langsam?” Zugeben und beziffern: Ja, der erste Effekt ist Verlangsamung. Dagegen stehen drei Mechanismen — S3-Dependency-Cache, Registry-Layer-Cache, vorinstallierte Toolchains im Runner-Image (statt Setup-Schritten in jedem Job). Damit liegt der Overhead pro Job typischerweise bei Sekunden bis wenigen zehn Sekunden, nicht Minuten. Der Trade „etwas Latenz gegen reproduzierbare, nicht infizierbare Build-Umgebungen” ist für eine 200-Repo-Plattform richtig.
„Warum nicht einfach GitHub-hosted Runner — null Betrieb?” Kriterien nennen: Zugriff auf interne Systeme (hosted Runner stehen außen), Kostenkontrolle bei 4.000+ Build-Minuten täglich, eigene Maschinentypen und Caches. Aber ehrlich bleiben: Für ein Setup ohne interne Abhängigkeiten und mit kleinerem Volumen sind hosted Runner die rationale Wahl — der Betrieb eines Build-Clusters ist kein Selbstzweck.
„Ein Build wird kompromittiert — was kann der Angreifer?” Die Blast-Radius-Frage, am besten als Aufzählung der Grenzen: Pod ohne Privilegien (kein Node-Takeover über den Socket), NetworkPolicy (keine lateralen Bewegungen), OIDC-Token mit kurzer Laufzeit und eng gescopter Rolle (kein Dauer-Key zum Exfiltrieren), kein Schreibzugriff auf fremde Caches, keine Cluster-Credentials (Deployment macht GitOps). Bleibt: Er kann ein bösartiges Image pushen — dagegen stehen Image-Signierung und Admission-Kontrolle auf Cluster-Seite.
„Spot-Instances killen Builds mitten im Lauf.” Stimmt, und das ist eingepreist: Builds sind idempotent und werden neu eingereiht, der Cache macht den Wiederholungslauf billig. Pipelines, bei denen Wiederholung weh tut (Release, lange E2E-Suites), pinnt man per Label auf On-Demand-Nodes.
Praxis
Spiele den Case 40 Minuten laut mit Timer. Pflichtbestandteile:
- Das Lastprofil mit Peak-Parallelität und die Folgerung „elastisch statt statisch”.
- Der Konflikt „Cache vs. Isolation” als expliziter Satz, mit der Fork-PR-Regel als Beispiel.
- Der Lebenszyklus eines Builds als Erzählung: Webhook → ARC skaliert Pod → Checkout → Cache-Restore → Build mit BuildKit/Kaniko → Push → Tag-Update im GitOps-Repo → Pod stirbt.
- Die Blast-Radius-Antwort als Aufzählung der fünf Grenzen.
Wiederhole nach ein paar Tagen mit der Variante „Monorepo statt 200 Repos” und beobachte, was sich verschiebt: Affected-Detection und Remote-Build-Caching werden wichtiger als Runner-Skalierung.
Typische Stolperfallen
Docker-in-Docker mit privilegierten Pods vorschlagen. Der häufigste Patzer in diesem Case. Wer „privileged: true” für Builds braucht, hat die Isolationsanforderung aus der Aufgabenstellung verfehlt — daemonlose Builder sind die erwartete Antwort.
Caching ohne Vertrauensgrenzen. Ein globaler Cache, in den jeder PR schreiben darf, macht aus dem Beschleuniger einen Angriffsvektor. Lese-/Schreibrechte am Cache folgen der Vertrauensgrenze (Fork-PR liest höchstens, schreibt nie).
CI und CD vermischen. Runner mit kubeconfig für Produktion sind ein gigantischer Credential-Honeypot. Die Übergabe an GitOps ist nicht Stilfrage, sondern Blast-Radius-Begrenzung.
Die Plattform ohne Metriken designen. Queue-Wartezeit, Build-Dauer p50/p95, Cache-Hit-Rate, Kosten pro Build — ohne diese Zahlen kannst du weder Engpässe belegen noch den Wert der Plattform zeigen. Ein Satz dazu gehört in jede Antwort.
Interview-Vorbereitung
Deine Kernbotschaft: Ephemere Runner auf einem dedizierten, elastischen Build-Cluster; Container-Builds ohne Privilegien per BuildKit/Kaniko; Geschwindigkeit über externe Caches mit Vertrauensgrenzen statt über geteilten Zustand; kurzlebige OIDC-Credentials — und die Deployment-Grenze: CI endet an der Registry, ab da übernimmt GitOps.
Rechne mit diesen Follow-ups:
- „Ephemer oder persistent — und warum?” — Ephemer: reproduzierbar, kein Zustands-Erbe, keine Persistenz für Angreifer; der Geschwindigkeitsverlust wird über Caches und vorgebaute Runner-Images kompensiert.
- „Kaniko oder BuildKit?” — Beide bauen ohne privilegierten Daemon; BuildKit bietet ausgefeilteres Caching und parallele Build-Stages, Kaniko ist der ältere Standardweg für In-Cluster-Builds. Wichtiger als die Wahl: begründen können, warum kein Docker-Socket.
- „Wie hältst du 200 Pipelines konsistent?” — Wiederverwendbare Workflows und Templates der Plattform statt 200 kopierter YAML-Dateien; Teams konfigurieren Parameter, der Golden Path pflegt die Mechanik zentral.
- „Wo entsteht hier Supply-Chain-Sicherheit?” — Gepinnte Basis-Images über den Pull-Through-Cache, Image-Signierung beim Push, SBOM-Erzeugung im Build, Admission-Check auf Signatur im Zielcluster.
Zusammenfassung
Der CI-Plattform-Case lebt vom Zielkonflikt Geschwindigkeit gegen Isolation: Ephemere ARC-Runner und daemonlose Builds liefern die Isolation, externe Caches mit sauberen Vertrauensgrenzen holen die Geschwindigkeit zurück, OIDC und die Übergabe an GitOps begrenzen den Schaden eines kompromittierten Builds auf die Registry. Wer das Lastprofil schätzt, den Build-Lebenszyklus erzählen kann und die Blast-Radius-Frage als Aufzählung von Grenzen beantwortet, spielt den Case souverän. In der nächsten Lektion bekommt die Plattform ihr Regelwerk: eine Policy-as-Code-Plattform, die Guardrails über alle Cluster durchsetzt — von Admission Control bis Compliance-Reporting.