# Alarme, die nicht wissen, in welcher Cloud sie laufen

Jede Cloud verkauft Ihnen Alarme. CloudWatch Alarms, Azure-Monitor-Alarmregeln, Cloud-Monitoring-Alerting-Policies: jeder ist vorkonfiguriert, jeder ist ein paar Klicks entfernt, und jeder ist in einem Format definiert, das nur seine eigene Cloud versteht. Laufen Sie in mehr als einer, landen Sie bei drei Alarmsystemen, drei On-Call-Routings, drei Definitionen von „die API ist ungesund“ und früher oder später bei drei verschiedenen Schwellwerten für dieselbe Sache, weil jemand einen nachjustiert und die anderen vergessen hat.

Wir haben dargelegt, dass [Logs](/de/blog/your-logs-should-not-know-which-cloud) und [Traces](/de/blog/your-traces-should-not-know-which-cloud) von der Anwendung emittiert und dort gespeichert werden sollten, wo die Cloud sie nicht besitzt. Alarme sind das dritte Stück, und es ist das, was um 02:40 zählt, denn ein Alarm ist das, was entscheidet, ob Sie wach sind. So definieren wir sie einmal, aus Metriken, die die Anwendung emittiert, sodass „Fehlerrate über 1 %“ auf EKS, GKE und AKS dasselbe bedeutet und ein Dienst, der zwischen ihnen umzieht, nichts daran ändert, was Sie weckt.

## Auf das alarmieren, was der Nutzer sieht, nicht auf das, was die Cloud sieht

Die nativen Alarme der Cloud handeln von den Ressourcen der Cloud: CPU auf einer Instanz, 5xx-Zähler auf einem Load Balancer, Throttles auf einer Tabelle. Sie sind nützlich, und sie sind das Falsche zum Wecken, denn ein Nutzer erlebt keine CPU. Ein Nutzer erlebt einen langsamen oder fehlgeschlagenen Request. Also liegen die Alarme, die wecken, auf drei Zahlen pro Dienst, gemessen aus Sicht der Anwendung selbst:

- **Rate**: Requests pro Sekunde, damit ein Absturz auf null sichtbar ist.
- **Errors**: Anteil der fehlgeschlagenen Requests.
- **Duration**: Latenz im 95. oder 99. Perzentil.

Diese kommen aus derselben OpenTelemetry-Pipeline, die die Traces trägt. Der `spanmetrics`-Connector des Collectors macht aus jedem Server-Span ein Histogramm der Dauern und einen Zähler der Aufrufe, gelabelt mit `service.name`, der HTTP-Route, dem Statuscode und, weil `resourcedetection` zuerst lief, `cloud.provider`. Keine neue Instrumentierung in der App. Die Spans, die Sie bereits emittieren, werden zu den Metriken, auf die Sie alarmieren.

```yaml
connectors:
  spanmetrics:
    histogram:
      explicit: { buckets: [50ms, 100ms, 250ms, 500ms, 1s, 2s, 5s] }
    dimensions:
      - name: http.route
      - name: http.response.status_code
    exemplars: { enabled: true }
    resource_metrics_key_attributes: [service.name, cloud.provider, cloud.region]

service:
  pipelines:
    traces:  { receivers: [otlp], processors: [resourcedetection, batch], exporters: [otlphttp/tempo, spanmetrics] }
    metrics: { receivers: [spanmetrics, otlp], processors: [batch], exporters: [prometheusremotewrite/mimir] }
```

Die Metriken gehen nach Mimir (oder schlichtes Prometheus oder VictoriaMetrics; der Architektur ist es egal). Exemplare sind an, sodass ein Punkt im Latenzgraphen auf einen echten Trace verweist, der ihn erzeugt hat.

<div class="article-figure">
<svg viewBox="0 0 900 220" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Pipeline von Spans zu einem Page. Anwendungen auf EKS, GKE und AKS emittieren Spans über OTLP. Der Collector führt resourcedetection aus, dann leitet der spanmetrics-Connector Request-Zähler und Dauer-Histogramme ab, gelabelt nach Dienst, Route, Status und Cloud-Provider. Metriken gehen nach Mimir. Grafana-Alarmregeln, einmal als Code definiert, werten PromQL über alle Clouds aus und routen an den On-Call-Pager. Native Cloud-Alarme für Infrastruktur speisen denselben Pager, wecken aber nie von allein.">
<defs><marker id="arrA" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#4fffb0"/></marker></defs>
<g font-family="Inter,system-ui,sans-serif" font-size="12">
<rect x="15" y="40" width="150" height="90" rx="10" fill="#151b2e" stroke="#ffd166" stroke-width="1.5"/><text x="90" y="62" text-anchor="middle" fill="#f1f3ff" font-weight="700">Apps</text><text x="90" y="82" text-anchor="middle" fill="#ffd166" font-size="11">EKS · GKE · AKS</text><text x="90" y="100" text-anchor="middle" fill="#9aa3c7" font-size="11">Spans über OTLP</text><text x="90" y="118" text-anchor="middle" fill="#9aa3c7" font-size="11">kein Metrik-Code</text>
<line x1="167" y1="85" x2="200" y2="85" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrA)"/>
<rect x="203" y="40" width="190" height="90" rx="10" fill="#151b2e" stroke="#4fffb0" stroke-width="1.5"/><text x="298" y="62" text-anchor="middle" fill="#f1f3ff" font-weight="700">Collector</text><text x="298" y="82" text-anchor="middle" fill="#9aa3c7" font-size="11">resourcedetection →</text><text x="298" y="100" text-anchor="middle" fill="#4fffb0" font-size="11">spanmetrics-Connector</text><text x="298" y="118" text-anchor="middle" fill="#9aa3c7" font-size="11">Rate · Errors · Duration</text>
<line x1="395" y1="85" x2="428" y2="85" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrA)"/>
<rect x="431" y="40" width="130" height="90" rx="10" fill="#151b2e" stroke="#7b8cff" stroke-width="1.5"/><text x="496" y="62" text-anchor="middle" fill="#f1f3ff" font-weight="700">Mimir</text><text x="496" y="82" text-anchor="middle" fill="#9aa3c7" font-size="11">Labels enthalten</text><text x="496" y="100" text-anchor="middle" fill="#7b8cff" font-size="11">cloud_provider</text><text x="496" y="118" text-anchor="middle" fill="#9aa3c7" font-size="11">Exemplare → Tempo</text>
<line x1="563" y1="85" x2="596" y2="85" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrA)"/>
<rect x="599" y="40" width="150" height="90" rx="10" fill="#151b2e" stroke="#7b8cff" stroke-width="1.5"/><text x="674" y="62" text-anchor="middle" fill="#f1f3ff" font-weight="700">Grafana Alerting</text><text x="674" y="82" text-anchor="middle" fill="#9aa3c7" font-size="11">Regeln als Code</text><text x="674" y="100" text-anchor="middle" fill="#9aa3c7" font-size="11">Burn Rate auf SLOs</text><text x="674" y="118" text-anchor="middle" fill="#9aa3c7" font-size="11">eine Regel, alle Clouds</text>
<line x1="751" y1="85" x2="784" y2="85" stroke="#ff6b8a" stroke-width="1.5" marker-end="url(#arrA)"/>
<rect x="787" y="40" width="98" height="90" rx="10" fill="#151b2e" stroke="#ff6b8a" stroke-width="1.5"/><text x="836" y="80" text-anchor="middle" fill="#ff6b8a" font-weight="700">Pager</text><text x="836" y="100" text-anchor="middle" fill="#9aa3c7" font-size="11">eine Rotation</text>
<rect x="203" y="160" width="358" height="44" rx="10" fill="#0d1120" stroke="#2a3150" stroke-width="1.5" stroke-dasharray="5,3"/><text x="382" y="178" text-anchor="middle" fill="#9aa3c7" font-size="11">native Alarme: RDS-CPU, NAT-Fehler, Aurora-ACU, Quota-Limits</text><text x="382" y="195" text-anchor="middle" fill="#9aa3c7" font-size="11">bleiben in jeder Cloud · an denselben Pager, niedrige Priorität</text>
<path d="M561,182 L836,182 L836,132" fill="none" stroke="#2a3150" stroke-width="1.5" stroke-dasharray="5,3" marker-end="url(#arrA)"/>
</g>
</svg>
</div>

## Die Regeln, einmal geschrieben

Grafana-Alarmregeln leben in einer YAML-Datei im Repository und werden bei jedem Deploy provisioniert, genau wie die Dashboards. Eine Regel pro SLO, nicht eine pro Dienst pro Cloud. Die Abfrage erledigt die Auffächerung:

```yaml
# alerting/rules.yaml
groups:
  - name: api-slo
    interval: 1m
    rules:
      - alert: ApiErrorBudgetBurn
        for: 2m
        labels: { severity: page, team: platform }
        annotations:
          summary: 'api error budget burning {{ $labels.cloud_provider }} · {{ $value | humanizePercentage }}'
          runbook: https://runbooks.internal/api-errors
        expr: |
          (
            sum by (cloud_provider) (rate(calls_total{service_name="api", http_response_status_code=~"5.."}[5m]))
            /
            sum by (cloud_provider) (rate(calls_total{service_name="api"}[5m]))
          ) > (14.4 * 0.001)
          and
          (
            sum by (cloud_provider) (rate(calls_total{service_name="api", http_response_status_code=~"5.."}[1h]))
            /
            sum by (cloud_provider) (rate(calls_total{service_name="api"}[1h]))
          ) > (14.4 * 0.001)
```

Das ist ein Multi-Window-Burn-Rate-Alarm für ein Verfügbarkeits-SLO von 99,9 %: wecken, wenn die Fehlerrate der letzten fünf Minuten *und* der letzten Stunde beide das 14,4-Fache des Fehlerbudgets überschreiten, was dem Verbrauch des gesamten 30-Tage-Budgets in etwa zwei Tagen entspricht. Das `sum by (cloud_provider)` ist die gesamte Multi-Cloud-Geschichte. Eine Regel, ein Ausdruck, und sie feuert getrennt für `aws`, `gcp` und `azure`, mit der Cloud im Alarmtitel, wenn nur eine davon brennt. Zieht die API von EKS nach GKE um, ändert sich, welcher Label-Wert feuert. Die Regel ändert sich nicht.

Eine zweite Regel mit längerem Fenster (6 Stunden und 3 Tage, Schwelle 1×) fängt langsames Abbrennen als Ticket statt als Page. Latenz bekommt dieselbe Behandlung auf `histogram_quantile(0.99, sum by (le, cloud_provider) (rate(duration_bucket{...}[5m])))`.

<div class="article-figure">
<svg viewBox="0 0 900 210" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Diagramm der Fehlerrate über 24 Stunden für den api-Dienst, eine Linie pro Cloud. AWS und Azure bleiben deutlich unter dem Ziel von 0,1 Prozent. GCP springt um 02:40 für zwanzig Minuten auf 2 Prozent; die 5-Minuten- und 1-Stunden-Burn-Rate-Fenster überschreiten beide die 14,4-fache Schwelle und ein Page feuert nur für cloud_provider gcp. Ein langsames Plateau von 0,3 Prozent auf Azure am Nachmittag überschreitet nur die Langfenster-Regel und erzeugt ein Ticket.">
<g font-family="Inter,system-ui,sans-serif" font-size="12">
<text x="20" y="22" fill="#f1f3ff" font-size="14" font-weight="700">api-Fehlerrate nach Cloud · eine Regel, drei Label-Werte</text>
<line x1="60" y1="170" x2="880" y2="170" stroke="#9aa3c7"/><line x1="60" y1="40" x2="60" y2="170" stroke="#9aa3c7"/>
<line x1="60" y1="164" x2="880" y2="164" stroke="#2a3150" stroke-dasharray="3,3"/><text x="884" y="167" fill="#9aa3c7" font-size="10">0,1 % SLO</text>
<line x1="60" y1="120" x2="880" y2="120" stroke="#ff6b8a" stroke-dasharray="4,3"/><text x="884" y="123" fill="#ff6b8a" font-size="10">14,4× · Page</text>
<polyline points="60,167 200,166 400,167 600,166 880,167" fill="none" stroke="#ffd166" stroke-width="2"/><text x="70" y="52" fill="#ffd166" font-size="10">aws</text>
<polyline points="60,167 140,167 150,60 175,58 190,166 400,167 880,167" fill="none" stroke="#4fffb0" stroke-width="2"/><text x="70" y="66" fill="#4fffb0" font-size="10">gcp</text>
<polyline points="60,167 500,167 520,150 700,150 720,167 880,167" fill="none" stroke="#7b8cff" stroke-width="2"/><text x="70" y="80" fill="#7b8cff" font-size="10">azure</text>
<rect x="148" y="40" width="44" height="130" fill="#ff6b8a" opacity="0.12"/><text x="170" y="192" text-anchor="middle" fill="#ff6b8a" font-size="10">02:40 · Page: gcp</text>
<rect x="515" y="40" width="210" height="130" fill="#7b8cff" opacity="0.08"/><text x="620" y="192" text-anchor="middle" fill="#7b8cff" font-size="10">14:00–19:00 · langsames Abbrennen: Ticket, azure</text>
<text x="60" y="206" fill="#9aa3c7" font-size="10">00:00</text><text x="880" y="206" text-anchor="end" fill="#9aa3c7" font-size="10">24:00</text>
</g>
</svg>
</div>

## Was nativ bleibt, und wie es dazukommt

Die eigenen Alarme der Cloud verschwinden nicht; sie wechseln den Job. RDS-CPU, Aurora-ACU-Auslastung, Port-Allokationsfehler des NAT Gateways, DynamoDB-Throttles, Service-Quota-Limits: das sind Eigenschaften von Infrastruktur, die die Cloud besitzt, und das Monitoring der Cloud sieht sie zuerst und am besten. Diese Alarme halten wir nativ, definiert im selben CDK, das die Ressource definiert, und routen sie mit *niedriger Priorität* an denselben Pager. Sie informieren; sie wecken niemanden. Beginnt ein NAT, Pakete zu verwerfen, weckt der API-Fehlerraten-Alarm ohnehin innerhalb von zwei Minuten, und der NAT-Alarm sitzt im Incident-Kanal und erklärt, warum.

Diese Umkehrung ist der ganze Punkt. Symptome wecken. Ursachen annotieren. Die Symptom-Alarme sind Cloud-neutral, weil die Anwendung die Daten emittiert hat; die Ursachen-Alarme sind Cloud-spezifisch, weil die Cloud die Daten emittiert hat. Keiner gibt vor, der andere zu sein.

## Drei Dinge, die uns länger gekostet haben, als sie sollten

**Kardinalität.** `spanmetrics` mit `http.route` als Dimension ist in Ordnung. Mit `http.target` (dem rohen Pfad, IDs eingeschlossen) erzeugt es eine Zeitreihe pro Bestellung und Mimir kippt um. Das Routen-Template verwenden, nie den Pfad.

**Ruhe während Deploys.** Ein Rolling Deploy erzeugt für ein paar Sekunden einen Schwall Connection Resets. Das `for: 2m` in der Page-Regel fängt ihn ab. Ohne es hat in der ersten Woche jeder Deploy jemanden geweckt.

**Der Pager muss eine Sache sein.** Wir hatten CloudWatch an ein On-Call-Tool und Grafana an ein anderes angeschlossen, weil sie zu verschiedenen Zeiten eingerichtet worden waren. Zwei Rotationen, zwei Apps auf dem Telefon und eine Nacht, in der die Person, die für das eine Bereitschaft hatte, für das andere keine hatte. Alles routet jetzt über Grafanas Contact Points in eine Rotation. Die nativen Alarme kommen über ein SNS-Topic → Webhook dorthin, was eine Stunde Einrichtung gekostet hat und Tag eins hätte sein sollen.

## Die Kurzfassung

Auf die drei Zahlen alarmieren, die der Nutzer spürt, abgeleitet aus Spans, die Sie ohnehin emittieren, gespeichert in einem Metrik-Backend, das Ihnen gehört, ausgewertet von Regeln, die in Git liegen und ein `sum by (cloud_provider)` enthalten. Die Alarme der Cloud für die Dinge der Cloud behalten, mit niedriger Priorität, in denselben Pager. Wechselt ein Dienst die Cloud, ändert sich ein Label-Wert und niemand bearbeitet einen Alarm.

Möchten Sie die Regeldatei für Ihre eigenen Dienste, abgestimmt auf echte SLOs statt auf runde Zahlen? [Sprechen Sie mit uns](/contact).
