Verteilter Monolith: der Rollout-Radius macht Kopplung messbar

03.08.2026

Thema: alle Beiträge zu Kubernetes

Ob eine Änderung lokal bleibt, kannst du zählen statt diskutieren. Wie du mit helm template misst, wie viele Services eine einzelne Zeile Konfiguration neu startet.

Inhaltsverzeichnis

Eberhard Wolff und Falk Sippach beschreiben im iSAQB-Interview ein Muster, das viele kennen: Teams verteilen ihr System auf viele kleine Services, bevor die Domänengrenzen geklärt sind, und werden dadurch nicht flexibler, sondern langsamer. Ihre Prüffrage dafür ist angenehm unaufgeregt: Bleiben Änderungen lokal?

Das ist als Architekturfrage gemeint. Sie hat aber eine Ebene darunter eine harte, nachrechenbare Entsprechung, und die wird selten benutzt.

Die Antwort vorweg: Ob eine Änderung lokal bleibt, musst du nicht diskutieren, du kannst es zählen. Der Messwert ist der Rollout-Radius, also die Anzahl der Deployments, deren Pod-Template sich durch eine einzelne Änderung ändert. Wer eine Zeile Konfiguration anfasst, die genau einem Service gehört, und damit drei Deployments neu startet, hat an dieser Stelle nicht drei Services, sondern einen, der in drei Prozessen läuft. Diese Zahl steht vor jedem Deploy fest und braucht keinen Cluster.

Zwei Grenzen, und nur über eine wird geredet

Domain-driven Design zieht die Grenze im Modell und danach im Code. Das ist die Grenze, über die in Architekturrunden gesprochen wird, und sie ist die wichtigere von beiden.

Es gibt aber eine zweite: die Auslieferungsgrenze. Sie entsteht nicht im Entwurf, sondern in Helm-Charts, ApplicationSets und Pipelines, meistens nebenbei und ohne Entscheidung. Wenn beide Grenzen deckungsgleich sind, stimmt die Architektur mit dem Betrieb überein. Wenn nicht, gewinnt immer die Auslieferungsgrenze, denn sie bestimmt, was tatsächlich zusammen neu startet, zusammen ausfällt und zusammen abgestimmt werden muss.

Der unangenehme Fall ist nicht der offensichtliche. Der Code kann sauber getrennt sein, die Bounded Contexts können sitzen, und trotzdem zieht die Auslieferung die Grenze wieder ein.

Der Messwert und warum er belastbar ist

Ein Deployment rollt in Kubernetes genau dann neu aus, wenn sich spec.template ändert. Der Deployment-Controller berechnet daraus einen Hash, legt bei Abweichung ein neues ReplicaSet an und fährt die alten Pods herunter. Das ist keine Heuristik und keine Faustregel, sondern das Kriterium des Controllers.

Damit ist der Rollout-Radius sauber definiert:

Rollout-Radius einer Änderung = Anzahl der Deployments, deren gerendertes spec.template sich gegenüber dem vorherigen Stand unterscheidet.

Und er lässt sich vollständig lokal ausrechnen, weil das Rendern eine eigene Stufe ist. helm template erzeugt die Manifeste, ohne einen Cluster anzufassen.

Der Aufbau

Drei Services in einem Chart, so wie es in sehr vielen Repositories aussieht. Eine gemeinsame ConfigMap für die Konfiguration:

apiVersion: v1
kind: ConfigMap
metadata:
  name: shop-config
data:
  LOG_LEVEL: {{ .Values.config.logLevel | quote }}
  WAEHRUNG: {{ .Values.config.waehrung | quote }}
  FEATURE_FLAGS: {{ .Values.config.featureFlags | quote }}

Dazu je Service ein Deployment. Die Annotation darin ist kein Sonderweg, sondern das von Helm selbst dokumentierte Vorgehen, damit Änderungen an der Konfiguration überhaupt ankommen:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: katalog
spec:
  replicas: 2
  template:
    metadata:
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
    spec:
      containers:
        - name: katalog
          image: {{ .Values.katalog.image | quote }}
          envFrom:
            - configMapRef: { name: shop-config }

Warenkorb und Bezahlung sehen genauso aus. Bis hierhin hat niemand etwas falsch gemacht: drei getrennte Deployments, drei getrennte Images, drei getrennte Teams denkbar.

Die Messung

Der Radius braucht nur zwei Renderläufe und je Deployment einen Fingerabdruck des Pod-Templates:

podtemplates() {
  helm template shop ./shop "$@" \
    | yq -r 'select(.kind == "Deployment") | [.metadata.name, (.spec.template | tojson)] | @tsv' \
    | while IFS=$'\t' read -r name tpl; do
        printf '%s %s\n' "$name" "$(printf '%s' "$tpl" | sha256sum | cut -c1-12)"
      done
}

Das yq hier ist die jq-kompatible Variante, also python-yq. Der Ausgangsstand:

bezahlung 96e6c5bc1db6
katalog   920c8e1f9167
warenkorb 7c36afc5600f

Erster Fall, ein neues Image für genau einen Service:

podtemplates --set katalog.image=registry.example.org/katalog:1.4.3

Genau ein Fingerabdruck ändert sich, katalog auf 942b29aeace8. Radius 1. Das Werkzeug ist also nicht kaputt, und der Normalfall funktioniert.

Zweiter Fall, LOG_LEVEL von info auf debug. Ein Wert, den nur der Katalog liest:

podtemplates --set config.logLevel=debug
bezahlung ae5a1ec34317
katalog   182a3c947761
warenkorb cb85534ef297

Alle drei. Radius 3.

Auf dem Cluster nachgestellt

Damit das nicht nur eine Aussage über Textdateien bleibt, dasselbe auf einem frischen k3d-Cluster. Vor dem Upgrade laufen sechs Pods aus drei ReplicaSets. Dann dieselbe eine Zeile:

helm upgrade shop ./shop --set config.logLevel=debug --wait
katalog:   2 generations, 2 beobachtet
warenkorb: 2 generations, 2 beobachtet
bezahlung: 2 generations, 2 beobachtet

bezahlung-856b569547   0
bezahlung-85fb9cb8d7   2
katalog-854f467fc5     2
katalog-bf644cdf5      0
warenkorb-669cdb4c9    0
warenkorb-68595c9bc9   2

Drei neue ReplicaSets, sechs neue Pods, drei alte ReplicaSets auf null. Die Bezahlung startet neu, weil der Katalog mehr loggen soll. Genau das ist der Punkt aus dem Interview, nur eben als Vorgang im Cluster statt als Diagramm: Die Änderung bleibt nicht lokal.

Nüchtern betrachtet ist damit jede Konfigurationsänderung ein Ereignis für alle. Sie braucht ein gemeinsames Wartungsfenster, sie bringt für alle Beteiligten Rollout-Risiko mit, und sie erzeugt genau den Abstimmungsaufwand, den die Verteilung eigentlich abschaffen sollte. Die Betriebskosten der Verteilung sind bezahlt, die Rollout-Semantik ist die des Monolithen geblieben.

Der Reflex, der es schlimmer macht

Wer das entdeckt, will typischerweise als Erstes die Annotation loswerden. Ohne sie ist der Radius tatsächlich 0. Nur ist das keine Entkopplung, sondern ein zweiter Fehler:

ConfigMap im Cluster:      debug
Pod-Env nach dem Upgrade:  info

Die Pods laufen unverändert weiter. Über envFrom eingebundene Werte werden beim Start des Containers gelesen, eine spätere Änderung der ConfigMap erreicht den Prozess nicht. Aus sichtbarer Kopplung wird damit eine stille Abweichung zwischen dem, was im Cluster steht, und dem, was tatsächlich läuft. Sie fällt beim nächsten unabhängigen Grund für einen Neustart auf, also irgendwann, bei irgendeinem Pod.

Die Annotation ist nicht der Fehler. Sie macht die Kopplung nur sichtbar, die vorher schon da war.

Der Umbau

Der eigentliche Fehler ist die geteilte ConfigMap. Sie ist ein gemeinsamer Zustand, an dem drei Services hängen, und sie ist im Architekturdiagramm nicht zu sehen. Die Auflösung ist unspektakulär: eine ConfigMap je Service, und die Prüfsumme nur über den Teil, der diesem Service gehört.

      annotations:
        checksum/config: {{ .Values.katalog.config | toYaml | sha256sum }}

Die Prüfsumme läuft hier bewusst über den Werte-Teilbaum und nicht mehr über die gerenderte Datei. Liegen ConfigMap und Deployment eines Services in derselben Vorlage, würde sich diese Datei sonst über die Annotation selbst einbinden.

Dieselbe Änderung, LOG_LEVEL des Katalogs auf debug:

Radius: 1
katalog b74fafa2dd78

Radius 1. Ohne neuen Architekturstil, ohne Migration, ohne dass ein einziger Service umgezogen ist. Die Auslieferungsgrenze liegt jetzt dort, wo die Domänengrenze ohnehin schon war.

Was die Zahl entscheidet, und was nicht

Der Rollout-Radius sagt nicht, ob ein Modulith oder Microservices richtig sind. Er sagt, ob der gewählte Stil im Betrieb tatsächlich existiert.

Ein gut geschnittener Modulith hat konstruktionsbedingt Radius 1, weil er eine einzige Auslieferungseinheit ist. Das ist ehrlich: Er verspricht keine unabhängige Auslieferung und liefert sie auch nicht. Ein verteiltes System mit Radius 3 verspricht sie, kassiert die Betriebskosten der Verteilung und liefert die Rollout-Semantik des Monolithen. Von beiden Optionen ist die zweite die teurere.

Deshalb passt die Messung so gut zu dem Rat aus dem Interview, mit einem gut strukturierten Modulith zu beginnen und später Teile herauszulösen. Beim Herauslösen ist der Radius die Abnahmebedingung: Der herausgelöste Service ist fertig herausgelöst, wenn eine Änderung an ihm niemanden sonst neu startet. Vorher steht die Grenze auf dem Papier.

Ehrlich dazugesagt: Der Rollout-Radius misst eine Kopplung, nicht alle. Eine geteilte Datenbank, synchrone Aufrufketten, eine gemeinsame Bibliothek im Basis-Image und Readiness-Probes, die auf Nachbarn zeigen, sieht er nicht. Er ist nur die mit Abstand billigste Messung: zwei Renderläufe, kein Cluster, unter einer Minute. Genau deshalb gehört er in die CI, als Regel, die einen Merge-Request markiert, sobald eine service-lokale Änderung mehr als ein Deployment bewegt.

Warum diese Messung überhaupt möglich ist

Ein Detail daran ist keine Kleinigkeit. Der Radius ist nur deshalb bestimmbar, weil das Rendern eine offene, eigene Stufe ist. helm template gibt dir vollständig heraus, was ausgerollt würde, bevor es jemand ausrollt. Kubernetes verrät dir, nach welchem Kriterium neu gestartet wird.

Bei einer geschlossenen Plattform, die aus deinem Repository heraus deployt und dir das Ergebnis nicht als Manifest zeigt, hast du diese Zahl schlicht nicht. Du kannst dann behaupten, deine Services seien unabhängig, aber du kannst es nicht nachweisen.

Das ist der Punkt, an dem die beiden Säulen ineinandergreifen: Verstehen setzt voraus, dass man nachsehen darf. Nachsehen dürfen ist eine Frage der Werkzeuge und ihrer Lizenz.

Fazit

  1. Die Frage aus dem iSAQB-Interview, ob Änderungen lokal bleiben, hat im Cluster eine exakte Entsprechung: den Rollout-Radius.
  2. Kubernetes rollt genau dann neu aus, wenn sich spec.template ändert. Damit ist der Radius vor jedem Deploy berechenbar, ohne Cluster, mit zwei helm template-Läufen.
  3. Im gemessenen Beispiel kostet ein neues Image Radius 1, eine Zeile geteilter Konfiguration dagegen Radius 3. Auf dem Cluster sind das sechs neue Pods für eine Zeile.
  4. Die checksum/config-Annotation zu entfernen senkt den Radius auf 0 und ersetzt sichtbare Kopplung durch stille Abweichung. Die ConfigMap sagt dann debug, der Prozess info.
  5. Die Ursache ist geteilter Zustand in der Auslieferung. Eine ConfigMap je Service und eine Prüfsumme je Service bringen den Radius auf 1 zurück, ohne Architekturwechsel.
  6. Die Zahl entscheidet nicht zwischen Modulith und Microservices. Sie entscheidet, ob der gewählte Stil im Betrieb existiert, und taugt als Abnahmebedingung beim Herauslösen.

Die Domänengrenze zieht die Architektur. Ob sie im Betrieb ankommt, entscheidet die Plattform. Und das ist der Teil, den man nicht diskutieren muss, sondern nachrechnen kann.

© 2026 Trutz Software Consulting GmbH. CC BY 4.0: Dieser Text darf mit Namensnennung weiterverwendet und bearbeitet werden, auch kommerziell. Code-Beispiele darin stehen unter Apache 2.0. Ausgenommen sind Marke und Logo sowie gekennzeichnete Zitate Dritter. Was das genau heißt.

Newsletter

Erhalte praxisnahe Tutorials, Best Practices und echte Lösungen für die häufigsten DevOps-Probleme direkt in Dein Postfach.

Datenschutz: Deine E-Mail-Adresse wird DSGVO-konform in Deutschland gespeichert. Keine Spam, nur relevante Inhalte. Eine E-Mail alle 1 bis 2 Wochen.