Warum „Rolling Update" nicht genügt
Die Standard-Deployment-Strategie in Kubernetes ist RollingUpdate, und die meisten Teams hören dort auf. Sie ist besser als ein vollständiger Ausfall, aber als Produktions-Default trotzdem schlecht. Sie hat keinen automatisierten Health Check jenseits der Readiness Probe, kein Traffic Shaping, kein automatisches Rollback und keine Möglichkeit, die neue Version unter realer Last zu validieren, bevor alle Nutzer darauf umgelenkt werden. Wenn der neue Pod so kaputt ist, dass es sich erst bei 10 % Traffic zeigt, merken Sie es bei 100 %.
Echte Zero-Downtime-Deployments brauchen zwei Dinge, die zusammenspielen: eine Traffic-Strategie (Blue-Green oder Canary) und eine Health-Strategie (automatisierte Analyse, die einen Rollout abbrechen kann). Dieser Beitrag zeigt, wie Sie beides sauber mit Argo Rollouts, Kubernetes und einem Service Mesh oder Ingress aufbauen, das Sie vermutlich bereits betreiben.
Blue-Green: Der Umschalter
Blue-Green betreibt zwei vollständige Umgebungen nebeneinander. Blue bedient den Produktionsverkehr, während Green die neue Version erhält. Sie deployen, rauchen gegen einen internen Service-Namen, dann flippen Sie ein einzelnes Label oder eine Router-Regel, und Green wird Produktion. Das alte Blue bleibt warm für sofortiges Rollback.
Wann Blue-Green gewinnt
- Stateful-Tests werden vor dem Go-Live benötigt. Sie können vollständige Smoke-/E2E-Suites gegen Green auf echter Infrastruktur laufen lassen.
- Sofortiges Rollback ist harte Anforderung. Ein Label-Flip ist schneller als jeder Canary-Rewind.
- Die Kosten eines kleinen Bad-Rollout-Anteils sind inakzeptabel. Payment-Flows, Auth, kritische interne APIs.
Wann es wehtut
- Sie können sich 2x Kapazität für die Dauer des Umschaltens nicht leisten. Blue-Green verdoppelt Ihre Pod-Anzahl während der Transition.
- Sie haben Datenbank-Migrationen, die nicht rückwärtskompatibel sind. Blue und Green teilen dieselbe DB. Wenn Green ein Schema-Change benötigt, kann Blue brechen.
- Ihr Service ist groß. Ein vollständiges Green zu provisionieren kann Minuten an Scheduling kosten.
Blue-Green mit Argo Rollouts
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: payments
spec:
replicas: 10
strategy:
blueGreen:
activeService: payments-active
previewService: payments-preview
autoPromotionEnabled: false
scaleDownDelaySeconds: 600
prePromotionAnalysis:
templates:
- templateName: smoke-tests
args:
- name: service-name
value: payments-preview
postPromotionAnalysis:
templates:
- templateName: error-rate
selector:
matchLabels:
app: payments
template:
metadata:
labels:
app: payments
spec:
containers:
- name: api
image: ghcr.io/example/payments:1.14.0
ports:
- containerPort: 8080
Zwei wichtige Eigenschaften. autoPromotionEnabled: false erfordert eine manuelle Promotion (oder einen erfolgreichen AnalysisRun), bevor umgeschaltet wird. scaleDownDelaySeconds: 600 hält die alte ReplicaSet nach der Promotion 10 Minuten am Leben – das ist Ihr Rollback-Fenster.
Canary: Der Gradient
Canary-Releases leiten einen kleinen Anteil des Traffics auf die neue Version, beobachten die Metriken und weiten schrittweise aus. Richtig gemacht ist das in den meisten Produktionsumgebungen die sicherste Strategie. Falsch gemacht (keine Metriken, kein automatisches Abort, manuelle Weight Bumps) ist es schlechter als ein Rolling Update, weil der babysittende Engineer bei jedem Schritt einfach „Promote" klickt.
Wann Canary gewinnt
- Hoch frequentierte Services. 10 % von einer Million Requests pro Minute sind reichlich Signal, um Regressionen zu erkennen.
- Kostensensibilität. Sie fahren 110 % Kapazität während des Rollouts, nicht 200 %.
- Graduelle Risikoexposition. Ideal für nutzerseitige Änderungen, bei denen „intern sieht es gut aus" nicht genügt.
Wann es wehtut
- Niedrig frequentierte Services. 10 % von 50 rpm sind 5 rpm. Das ist in fünf Minuten nicht genug Signal, um etwas zu erkennen. Dafür sind Blue-Green oder plain Rolling Update in Ordnung.
- Schwer isolierbare Metriken. Wenn Sie die Error-Rate pro Version nicht berechnen können, ist Canary-Analyse eine Lüge.
Canary mit Argo Rollouts und Istio
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: api
spec:
replicas: 8
strategy:
canary:
canaryService: api-canary
stableService: api-stable
trafficRouting:
istio:
virtualService:
name: api
routes: [primary]
steps:
- setWeight: 5
- pause: { duration: 2m }
- analysis:
templates:
- templateName: success-rate
- templateName: latency-p99
args:
- name: service-name
value: api-canary
- setWeight: 25
- pause: { duration: 5m }
- analysis:
templates:
- templateName: success-rate
- templateName: latency-p99
args:
- name: service-name
value: api-canary
- setWeight: 50
- pause: { duration: 5m }
- setWeight: 100
Beachten Sie, wie die Pausen anfangs kurz und später länger sind. Die ersten Minuten eines Canary sind am informativsten – wenn die neue Version crasht oder 500er wirft, wollen Sie es sofort sehen.
Automatisierte Analyse: Der Teil, der wirklich zählt
Keine Strategie ist sicher ohne automatisiertes, metrikgetriebenes Abort. Argo Rollouts nutzt AnalysisTemplate, um Prometheus (oder Datadog, New Relic, CloudWatch usw.) abzufragen und den Rollout bei schlechtem Signal fehlschlagen zu lassen.
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
spec:
args:
- name: service-name
metrics:
- name: success-rate
interval: 30s
count: 6
successCondition: result[0] >= 0.99
failureLimit: 2
provider:
prometheus:
address: http://prometheus.monitoring:9090
query: |
sum(rate(http_requests_total{service="{{args.service-name}}",code!~"5.."}[1m]))
/
sum(rate(http_requests_total{service="{{args.service-name}}"}[1m]))
Und ein Latenz-Template nach demselben Muster:
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: latency-p99
spec:
args:
- name: service-name
metrics:
- name: latency-p99
interval: 30s
count: 6
successCondition: result[0] < 0.4
failureLimit: 2
provider:
prometheus:
address: http://prometheus.monitoring:9090
query: |
histogram_quantile(
0.99,
sum by (le) (
rate(http_request_duration_seconds_bucket{service="{{args.service-name}}"}[1m])
)
)
Schlägt eines davon fehl, bricht der Rollout ab und stellt den stabilen Traffic ohne Mensch im Loop wieder her. Genau darum geht es.
Datenbank-Migrationen: Der stille Killer
Der mit Abstand häufigste Grund, warum „Zero Downtime" zu „Ausfall" wird, ist eine Datenbank-Migration, die angenommen hat, der alte Code sei bereits verschwunden. Sowohl Blue-Green als auch Canary servieren alten und neuen Application Code gleichzeitig. Ihr Schema muss für beide gleichzeitig funktionieren.
Das Expand-and-Contract-Muster
Shippen Sie niemals eine breaking Migration in einem einzigen Release. Teilen Sie sie auf in:
- Expand. Die neue Spalte/Tabelle/den neuen Index hinzufügen. Alter Code ignoriert sie. Neuer Code schreibt in alt und neu.
- Backfill. Daten von alt nach neu kopieren. Läuft als Background Job.
- Reads migrieren. Neuer Code liest aus der neuen Spalte. Alter Code liest noch aus der alten.
- Contract. Die alte Spalte in einem späteren Release entfernen, sobald kein laufender Pod sie mehr nutzt.
Ja, das sind vier Releases statt einem. Es ist auch der Unterschied zwischen einer geplanten Änderung und einem Incident um zwei Uhr morgens.
-- Release N: expand
ALTER TABLE users ADD COLUMN email_normalized TEXT;
CREATE INDEX CONCURRENTLY idx_users_email_norm ON users(email_normalized);
-- Release N (app code): dual-write
INSERT INTO users (email, email_normalized) VALUES ($1, LOWER($1));
-- Release N+1: backfill
UPDATE users SET email_normalized = LOWER(email) WHERE email_normalized IS NULL;
-- Release N+2: read from new column
SELECT id FROM users WHERE email_normalized = $1;
-- Release N+3: contract
ALTER TABLE users DROP COLUMN email;
Connection Draining und Graceful Shutdown
Selbst bei perfektem Traffic Shifting reißt ein Pod, der mitten im Request stirbt, diesen Request mit. Kubernetes sendet SIGTERM, wartet terminationGracePeriodSeconds, dann sendet es SIGKILL. Ihre App muss:
- Ihre Readiness Probe sofort fehlschlagen lassen (damit sie aus den Endpoints entfernt wird).
- In-flight Requests weiter bedienen.
- Den HTTP-Listener schließen und Verbindungen drainen.
- Sauber beenden, bevor die Grace Period endet.
Minimaler Node.js-Shutdown-Handler:
import http from "node:http";
import express from "express";
const app = express();
let ready = true;
app.get("/ready", (_, res) => (ready ? res.sendStatus(200) : res.sendStatus(503)));
app.get("/live", (_, res) => res.sendStatus(200));
const server = http.createServer(app);
server.listen(8080);
const shutdown = async () => {
console.log("SIGTERM received, draining");
ready = false;
// Give load balancers time to observe unreadiness
await new Promise((r) => setTimeout(r, 5000));
server.close((err) => {
if (err) {
console.error("Shutdown error", err);
process.exit(1);
}
process.exit(0);
});
// Hard timeout
setTimeout(() => process.exit(1), 25000).unref();
};
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);
Und die passende Pod-Spec:
spec:
terminationGracePeriodSeconds: 30
containers:
- name: api
lifecycle:
preStop:
exec:
command: ["sleep", "5"]
readinessProbe:
httpGet: { path: /ready, port: 8080 }
periodSeconds: 2
failureThreshold: 2
Der preStop-Hook gibt dem Service Mesh einen Moment, das Entfernen zu propagieren, bevor der Prozess mit dem Herunterfahren beginnt. Kleinigkeit, enormer Unterschied in Produktion.
Die Wahl zwischen beiden
| Kriterium | Blue-Green | Canary |
|---|---|---|
| Traffic-Modell | Alles-oder-nichts | Graduelle Prozentsätze |
| Rollback-Geschwindigkeit | Sofort (Label-Flip) | Sekunden (Traffic-Reset) |
| Zusätzliche Kapazität | 2x während Umschalten | 1,1x während Rollout |
| Echte Nutzervalidierung | Erst nach Umschalten | Während Rollout |
| Nutzbarer Mindest-Traffic | Beliebig | ~100 rpm+ |
| DB-Migrations-Toleranz | Schwieriger (geteilte DB) | Gleich |
| Komplexität | Geringer | Höher (Metriken erforderlich) |
In der Praxis empfehlen wir Canary als Default für jeden Service mit genug Traffic, damit die Analyse aussagekräftig ist, und Blue-Green für Low-Traffic-Kritik-Services (Auth, Payments, Webhooks), wo vollständige Pre-Promotion-Smoke-Tests wertvoller sind als graduelle Exposition.
Nächste Schritte
Furchtlos shippen erfordert drei Dinge: eine Traffic-Strategie, automatisiertes metrikgetriebenes Abort und eine Datenbank-Disziplin, die davon ausgeht, dass zwei Code-Versionen gleichzeitig laufen. Das YAML ist einfach. Die Disziplin ist der harte Teil. Beginnen Sie damit, Expand-and-Contract auf Ihre nächste Schema-Änderung anzuwenden, ergänzen Sie ein AnalysisTemplate, das Ihr bestehendes Prometheus abfragt, und fahren Sie Ihren ersten Canary auf einem unkritischen Service. Wenn Sie Unterstützung beim Aufbau einer Progressive-Delivery-Plattform über mehrere Services hinweg wünschen, nehmen Sie Kontakt auf.