Almost nobody is on one cloud. There is the cloud you chose, the one an acquisition brought, the one the data team picked for a managed service, and something still running in a colo that everyone has agreed not to discuss. The on-call rota has to cover all of it, and the engineer holding the pager at 3am should not have to remember which console they need.

What genuinely differs, and what only looks like it does

Having built connectors for all four, the useful finding is how little of the difference is real. Three of the four questions an incident asks are the same everywhere. Only one is genuinely per-cloud.

graph LR subgraph Same["Same question, same shape"] Q1["Is the provider
having a problem?"] Q2["What changed
recently?"] Q3["Is the service
serving traffic?"] end subgraph Diff["Genuinely different"] Q4["Query the metrics
and the logs"] end Q1 --> A1["cloud.health
one step, a provider param"] Q2 --> A2["servicenow.recent_changes
or deploy history"] Q3 --> A3["load balancer or
request-rate metric"] Q4 --> A4["PromQL · CloudWatch · KQL
MQL · Cloud Monitoring filters
five languages, no abstraction"] style A4 fill:#fff4e2,stroke:#f4a03a,stroke-width:2px style A1 fill:#e8f8f4,stroke:#22c1a4

So the design splits along that line. One step for the questions that are the same; provider-specific steps for the one that is not.

Why cloud.health is one step and not four

"Is it us or the provider?" is the same question on every cloud, and the answer has the same shape: a list of open events, or nothing. Making an author learn four step types to ask one question is a tax with no benefit, so cloud.health takes a provider parameter and returns one shape.

Underneath, the four implementations have almost nothing in common. AWS needs the Health API and a Business support plan. Azure queries Resource Health on a subscription. GCP reads a public JSON feed. OCI reads a per-region, per-service document listing all ninety regions. None of that belongs in a runbook.

Where we did not abstract, and why

We deliberately did not build a cloud.metric that takes a provider and a metric name. The query languages are not cosmetic differences — a KQL join has no PromQL equivalent, MQL's windowing is its own thing, and a lowest-common- denominator wrapper would be worse than all four. An author writing an Azure runbook should write KQL. The abstraction earns its place for health because the question is genuinely identical; it does not for queries because they are genuinely not.

One incident shape, whatever raised it

The thing that makes a single rota possible is that an alert from any of twelve sources becomes the same object: source, alert name, service, severity, labels, external id, resolved-or-not. An engineer reads the same incident page whether the alert came from CloudWatch, Azure Monitor or a Prometheus in a colo.

CloudWhere the service identity hides
AWSIn the alarm's Dimensions — ServiceName, FunctionName, LoadBalancer…
AzureAt the end of the resource ID path, with the resource group and provider alongside it.
GCPIn resource.labels.service_name — or cluster_name, or instance_id.
OCIIn the alarm's dimensions, as resourceDisplayName.

Four different places, one service field afterwards. That normalisation is the whole reason a match rule can say service ~ orders-api and catch the alert wherever it came from.

The bug this uncovered, which applies to all of them

Every one of these vendors sends a thinner payload to close an alert than to open it. Azure's resolve drops the target IDs; GCP's drops the resource labels; Opsgenie's Close carries the alert but not the entity or tags. Match on a fingerprint built from labels and every close produces a different fingerprint from its open — so the close is discarded and the incident stays open for ever. We checked all five: five differing fingerprints, five matching ids. Matching the source's own id first is load-bearing, not an optimisation.

Where ServiceNow sits

Underneath all four, and it is the connector we built first for a reason that has nothing to do with ITSM process.

The most common classification an agent reaches is bad deploy, and on every cloud the causal claim is the same: errors and a release in the same window. That is correlation, so it is marked assumed and the confidence caps at 0.75. A change record naming the same configuration item inside the window turns it into something established — and the change calendar is cloud-agnostic, so one connector lifts the ceiling on RCAs across the whole estate.

graph TB A["AWS incident"] --> S["servicenow.recent_changes
ci = {{service_name}}, window 120m"] B["Azure incident"] --> S C["GCP incident"] --> S D["OCI incident"] --> S S --> R{"A change on this CI
inside the window?"} R -->|"yes"| E["Established
CHG0031482, implemented 01:34"] R -->|"no"| F["Still assumed — and worth saying:
‘no tracked change touched this’
is itself evidence"] style E fill:#e8f8f4,stroke:#22c1a4,stroke-width:2px

The negative answer matters too, and it is easy to overlook. "No change record touched orders-api in the last two hours" rules out a whole branch of investigation at 3am, which is worth as much as confirming one.

Structuring a multi-cloud runbook library

Three patterns, in increasing order of how much we would recommend them.

  • One runbook per cloud per alert class. Simple, obvious, and it duplicates the reasoning steps four times. Fine for two clouds; painful at four, because a fix to the verification logic has to be made in twelve places.
  • One runbook per alert class, branching on source. Conditions on {{incident.source}} pick the right query step. Fewer runbooks, but the conditions get hard to read and the dry run has more paths than any one incident uses.
  • One runbook per cloud for gathering; shared conventions for reasoning. What we would suggest. The query steps differ because the query languages differ, but every runbook saves into the same variable names — error_rate, recent_deploys, dep_health — so the classify and verify steps are copy-identical and the incidents are comparable across clouds.

That third pattern has a side benefit worth the discipline: because the saved names match, the Analytics page can compare runbook effectiveness across clouds, and you can see that your Azure estate escalates twice as often as your AWS one — which is a real finding and impossible to see if every runbook names things differently.

Next

That completes the stacks. The last series is about adopting it, starting with build versus buy — including the cases where building your own is the right answer.

Keep reading