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.
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.healthone step, a provider param"] Q2 --> A2["
servicenow.recent_changesor 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.
| Cloud | Where the service identity hides |
|---|---|
| AWS | In the alarm's Dimensions — ServiceName, FunctionName, LoadBalancer… |
| Azure | At the end of the resource ID path, with the resource group and provider alongside it. |
| GCP | In resource.labels.service_name — or cluster_name, or instance_id. |
| OCI | In 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.
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.