The Exit Is the Architecture: Managing Single Cloud Supplier Risk Without Running Two Clouds

The Exit Is the Architecture: Managing Single Cloud Supplier Risk Without Running Two Clouds

👁13views

You manage single cloud supplier risk by building the ability to leave rather than running a second cloud. Own the layers above the infrastructure, including provisioning, artefacts, data formats and runtime stack, so the provider supplies a replaceable commodity. Portability addresses commercial, contractual and structural risks that a second production cloud cannot.

Article Summary
  • 1.
    What it is
    Single cloud supplier risk is a commercial, contractual and structural problem, not a runtime one, and it is managed through exit capability rather than by running a second cloud. The article explains how a platform of Backstage, Odin (Terraform modules) and JFrog concentrates provider specific detail so moving stays feasible.
  • 2.
    Why it matters
    It argues that dual cloud estates buy the wrong mitigation, adding security, identity and operational complexity while failing to catch the runtime failures they claim to hedge. Regulators ask how single provider dependency is managed, and portability gives a better answer than counting providers.
  • 3.
    Key takeaway
    Rent the substrate, own the abstraction: the better risk question is not how many clouds you use but what it would take to rebuild a service somewhere else, and how you know.
4,426 words · ~23 min read
Listen to this article1 play

Ask a bank how it manages the risk of depending on a single cloud provider and you’ll usually hear “we’re adding a second one”. It sounds prudent and looks great on a risk register, but for the tightly coupled transactional workloads that actually matter, it’s often the most expensive way to buy the least protection. There is a cheaper answer for those workloads, you can measure it, and it doesn’t involve dragging a second identity model, network fabric and policy estate into your bank.

Earlier this year I wrote Why Multicloud Is Not a Cloud Resilience Strategy. The most common response, from regulators, risk committees and plenty of people in my own industry, was a fair question: if you won’t run two clouds, how are you managing the risk of depending on one? Our regulators are asking us exactly that right now, and they’re right to. This post is my answer, and it comes down to one idea: supplier independence is an engineering property you can measure and keep under review, not a procurement property you get by counting vendors.

1. Three Controls, Not One

Most arguments about cloud concentration go wrong before they start, because they lump three different controls together.

flowchart LR
    subgraph R["Runtime resilience"]
        R1["Multi AZ, multi region"]
        R2["Protects against: infrastructure failure"]
        R3["Measured by: recovery time and data loss"]
    end
    subgraph X["Exit architecture"]
        X1["Portable platform, open engines, open data"]
        X2["Protects against: lock in, commercial and structural risk"]
        X3["Measured by: estimated exit time and cost"]
    end
    subgraph T["Exit assurance"]
        T1["AI review plus targeted automated tests"]
        T2["Protects against: gaps nobody noticed"]
        T3["Measured by: open findings and how long they stay open"]
    end
    R --- X --- T

What this shows: resilience, portability and assurance are separate controls with separate measures, and strength in one doesn’t compensate for the absence of another.

If you have great runtime resilience and no exit architecture, you’re resilient and trapped. If you have a beautiful exit architecture that nobody keeps checking, it quietly rots as teams add dependencies nobody wrote down. For a regulator, the most useful thing you can do is separate two time measures that almost everyone collapses into one.

MeasureThe question it answersTypical driver
Recovery timeHow long to restore service after an infrastructure failure, on the same providerAZ or regional failure
Exit timeHow long to move a critical service to another provider, by choice and in an orderly wayCommercial dispute, price change, strategic decision

Recovery time is about infrastructure failing underneath you, and you solve it inside the provider with availability zones and regions. Exit time is about deciding to leave, for commercial or strategic reasons, and you solve it in the layers above the provider. Treating one as a proxy for the other is how banks end up with excellent disaster recovery and no negotiating leverage, or with a theoretical exit plan masquerading as resilience. This post is about the second one: a planned, orderly exit that you choose to make, which assumes AWS stays available long enough to move the data and stand up the replacement. A sudden, provider wide loss of AWS is a different risk with different answers, and I’m deliberately not covering it here. One more number matters alongside exit time, and that’s runway, which comes in two flavours. Contractual runway is the service and transition assistance the provider is obliged to give you after notice. Operational runway is how long you can realistically keep running normally while the exit is under way, which a dispute, a deteriorating relationship or a withdrawn service can shorten. In a friendly commercial exit the two are usually close; they aren’t guaranteed to be the same. A ninety day exit estimate means very little unless you know you have ninety days.

2. Two Different Problems That Get Treated as One

The case against dual cloud is a security, network and operational one, and I made it in full last time. Neither provider’s native identity model can express rules about the other’s workloads, so you bridge them with federation and external policy engines. That works, but each bridge is another control plane to secure, govern and keep consistent. DNS, routing and certificates become shared choke points above both clouds, policy drift becomes a permanent source of gaps, and incidents slow down at a boundary neither provider will own. For synchronous transaction processing, the latency between providers also rules out the active active designs that would make a second cloud useful at runtime.

Single supplier risk is different. It’s mostly commercial and structural: the provider changing prices or terms when your switching costs are high, withdrawing or deprecating a service you rely on, the relationship deteriorating, and the slow build up of proprietary dependencies that turns a theoretical exit into a multiyear programme nobody will fund.

A truly independent second provider can help with some of that, so my position is conditional. For tightly coupled, stateful transaction processing, a second production cloud usually costs more than it protects. For stateless channels, batch, analytics or an isolated recovery environment, the balance can be different, and a second provider may earn its place. Decide workload by workload. Where it doesn’t earn its place, you get most of the same protection more cheaply by making the organisation able to leave.

3. Rent the Substrate, Own the Abstraction

Everything we do here hangs off one principle: we rent infrastructure from AWS, but we try to own every layer above it that decides how hard it would be to leave. If we own those layers, AWS is a very good supplier of something we could buy elsewhere at some cost and inconvenience. If AWS owns them, it’s not a supplier, it’s a dependency with no way out.

flowchart TB
    subgraph Owned["Owned by us: the portable layers"]
        A["Developer portal and golden paths<br/>(Backstage)"]
        B["Provisioning and policy<br/>(Odin, Terraform underneath)"]
        C["Artefacts and supply chain<br/>(JFrog, CI/CD pipelines)"]
        D["Runtime stack<br/>(Linux, Java, containers, Kubernetes)"]
        E["Data engines and formats<br/>(PostgreSQL, MySQL, Kafka, open file formats)"]
    end
    subgraph Rented["Rented from the provider: the substrate"]
        F["Compute, block and object storage"]
        G["Networking, load balancing, DNS"]
        H["Managed engines and key management"]
    end
    A --> B --> C --> D --> E
    E --> F
    E --> G
    E --> H

What this shows: many of the most important controls on how hard it is to leave sit in layers we own; the provider supplies the substrate underneath.

Our engineers don’t deploy to AWS. They deploy through a platform. The platform itself still runs on AWS infrastructure, IAM, networking and service APIs, of course; the point isn’t that the dependency disappears, it’s that it’s concentrated behind a few well governed boundaries where you can see it and measure it. Spotify’s Backstage is the developer portal, with a catalogue of services, owners and dependencies and golden path templates for new services. Beneath it sits Odin, our customised provisioning layer, which is essentially Terraform with our own modules, guardrails and automated security controls. Artefacts are built through our CI/CD pipelines and stored in JFrog, the system of record for every binary and image that reaches production.

That arrangement does more for supplier risk than it first appears. A team asking for a PostgreSQL database asks the platform for a PostgreSQL database; that it resolves today to an RDS instance with a particular parameter group and backup policy is a detail of one module, not something baked into the application. Anyone who tells you Terraform will translate AWS resources into Azure resources for you hasn’t tried it, so retargeting is real work. But the module interface application teams consume stays the same, only the implementation behind it changes, and that’s bounded work owned by one team rather than an archaeology project across the estate. Backstage also gives risk teams something they rarely get: a live inventory of what exists, who owns it and what it depends on. You can’t plan an exit from a dependency you haven’t catalogued.

4. The Exit Toolchain, and Why Our Keys Stay in AWS

There’s a catch in all of that. A platform only helps you leave if the platform itself isn’t welded to the provider. If your pipelines are built on a provider’s own build and deployment services, your artefacts live only in its proprietary registry, and your runbooks assume its consoles, then the toolchain you’d use to move is one more thing that has to be rebuilt before you can move anything else. That’s why we chose Backstage, Terraform, JFrog and our own CI/CD rather than the provider’s native equivalents: they run on AWS today, and they would run on the target tomorrow, so the machinery of the exit comes with you. For each tool in the exit path, the question is simple: could this run on another provider without a rewrite, and has anyone checked?

Cryptography is different, and deliberately so. Our encryption keys, secrets and signing keys stay in AWS. The hardware boundary around a managed key service is itself a security control, and every copy of key material held outside it is another place a key can leak. Escrowing keys outside AWS to make an exit easier would trade a remote supplier risk for a permanent security risk, which is exactly the bargain this post argues against.

That’s a deliberate design choice built on non exportable keys, and it doesn’t stop you leaving, because in our design the keys never need to move. Secrets are just values, which you reissue on the target and rotate. Data is re-encrypted rather than the keys: replication and exports read data through the engine’s normal decryption path, send it over TLS, and the target encrypts it at rest under its own keys. Signing keys are the hard part. A key generated in KMS can’t be exported, so you issue new ones on the target and have verifiers trust both during the transition, but that means working through every certificate chain, token issuer, external party that verifies your signatures and any obligation to verify historical signatures after the old key is gone. That inventory belongs in the exit plan, not in the last week of the exit.

flowchart LR
    subgraph Portable["Portable toolchain, moves with us"]
        BS["Backstage"]
        TF["Odin / Terraform modules"]
        J2["JFrog artefacts"]
        CI2["CI/CD pipelines"]
    end
    subgraph Stay["Stays in AWS by design"]
        KMS["KMS encryption keys"]
        SEC["Secrets Manager secrets"]
        SIG["Signing keys"]
    end
    subgraph Target["Target provider, during the exit"]
        T1["Data re-encrypted under target keys"]
        T2["Fresh secrets issued, old ones rotated out"]
        T3["New signing keys, dual trust during transition"]
    end
    KMS -. "decrypt in AWS, re-encrypt on target" .-> T1
    SEC -. "read, reissue, rotate" .-> T2
    SIG -. "reissue" .-> T3
    classDef safe fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef keep fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    class BS,TF,J2,CI2,T1,T2,T3 safe
    class KMS,SEC,SIG keep

What this shows: the toolchain moves with us, but key material never leaves AWS; in an exit the data, secrets and trust relationships move and the keys stay put.

Decrypting your way out has a bill and, more importantly, a speed limit. Databases and managed streams such as MSK are mostly fine, because they use envelope encryption with data keys rather than a KMS call per row or message. Object storage is where it hurts: reading S3 objects encrypted with SSE-KMS without Bucket Keys typically costs a kms:Decrypt per object, and so do streams where your own code encrypts each record under KMS. At $0.03 per 10,000 requests, ten billion SSE-KMS objects cost around $30,000 in decrypts, which is survivable. The quota isn’t: KMS requests are throttled per account and region, shared with production, and at the time of writing the default outside the three largest US and European regions, Cape Town included, is 10,000 requests per second, a quota worth checking for your own region, operations and account rather than assuming. Ten billion decrypts at that rate is more than eleven days of the account’s entire KMS capacity. Get that wrong and your migration ends up throttling the bank. S3 Bucket Keys, which AWS says cut SSE-KMS request costs by up to 99%, data key caching in stream processors, fewer and larger files, a quota increase requested well in advance and a dedicated migration account all belong in the exit plan, long before the exit. On that last one, be precise: KMS throttles the account making the request, not the account that owns the key, so a separate account only protects production if the migration’s reads and decrypts actually originate from it.

5. Open Source Where the Data Lives

The second big lever is boring in the best way: the software holding our data and running our business logic is predominantly open source. We run Linux, we write most services in Java, our databases are overwhelmingly PostgreSQL and MySQL, and for streaming and caching we prefer Kafka and Valkey. Nobody will give us an award for those choices, but every major provider offers managed versions of them, and data is where most real lock in lives. PostgreSQL on RDS gives us well established logical export and replication paths to compatible PostgreSQL anywhere else, using tools the community maintains rather than the provider. Native managed snapshots don’t travel; logical exports and replication do.

Open source doesn’t buy you everything, though. The management layer of RDS isn’t portable, and Aurora replaces the storage and replication layer with something that has no equivalent outside AWS. The same engine isn’t automatically compatible across providers either: extensions, major versions, collations, authentication and each managed service’s limits all differ, and any one of them can stall a migration. So the platform owns a database compatibility profile of approved versions, extensions and collations known to exist on candidate targets, not just the choice of engine.

6. Data Gravity: Can It Move Fast Enough?

A portable engine doesn’t mean the data can move in a timeframe anyone will accept. Fifty terabytes at a sustained ten gigabits per second is a little over eleven hours of pure transfer, and real transfers rarely sustain line rate. Transfer is also the easy step.

flowchart LR
    P["Provision compatible target"] --> A["Export<br/>snapshot or logical dump"]
    A --> B["Transfer<br/>bounded by link throughput"]
    B --> C["Restore<br/>index rebuilds dominate"]
    C --> D["Reconcile<br/>balances, ledgers, counts"]
    D --> E["Validate<br/>application and business checks"]
    E --> F["Cutover"]
    R["Continuous logical replication<br/>(after initial sync has caught up)"] -.->|"shrinks the cutover window"| D
    classDef slow fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef fast fill:#dcfce7,stroke:#16a34a,color:#14532d
    class C,D slow
    class R fast

What this shows: restore and reconciliation are the bottlenecks, not transfer, and replication only helps once a compatible target exists and has caught up.

Restoring a large PostgreSQL database from a logical dump means rebuilding every index, which can take far longer than the copy. Reconciliation, proving every balance and ledger entry matches, is the step almost every migration plan underestimates, and nobody explains a missing balance to a customer by saying the migration was on schedule. Continuous logical replication moves the bulk in the background and shrinks cutover to minutes or hours, but only after a compatible target is provisioned and initial sync has caught up, and only if you handle what it doesn’t carry: schema changes, sequence values, large objects, and tables without a primary key or replica identity. Matching row counts per database isn’t enough either. A single payment can span a ledger, an event stream, a fraud decision and a notification, so the exit has to preserve consistency across the whole business transaction, which means cutting over services in a deliberate order and reconciling at the business level, not just the table level. You already restore critical databases for disaster recovery, so use those restore and reconciliation timings in your exit estimates, because those numbers, not the network, set your exit time.

7. Where the Money Actually Goes

Most of our AWS spend runs through roughly a dozen products and spending categories, and we’re not special in that. Vantage, which aggregates anonymised spend from customers who connect their AWS accounts, ranks EC2 compute, S3 and RDS as the three largest AWS services by spend, and its quarterly reports have found compute, storage and databases in the top three on AWS, Azure and Google Cloud for several consecutive quarters. That data covers all industries, and I know of no public breakdown for banks, but an estate built on virtual machines, Kubernetes and relational databases tends to be at least as concentrated. The chart is an illustrative banking profile, not measured data; the script in the appendix produces the real one for your accounts. EKS worker nodes bill as EC2, so Kubernetes mostly hides in the compute line.

{
  "type": "bar",
  "data": {
    "labels": ["EC2 (incl. EKS nodes)", "RDS (PostgreSQL, MySQL)", "S3", "EBS", "Networking and data transfer", "ElastiCache", "MSK", "CloudWatch", "Direct Connect", "Elastic Load Balancing", "EKS control plane", "KMS", "Everything else"],
    "datasets": [
      {
        "type": "bar",
        "label": "Share of spend (%)",
        "data": [32, 17, 9, 7, 6, 4, 4, 3.5, 2.5, 2, 1, 1, 11],
        "backgroundColor": "rgba(37, 99, 235, 0.75)",
        "yAxisID": "y"
      },
      {
        "type": "line",
        "label": "Cumulative share (%)",
        "data": [32, 49, 58, 65, 71, 75, 79, 82.5, 85, 87, 88, 89, 100],
        "borderColor": "rgba(234, 88, 12, 1)",
        "backgroundColor": "rgba(234, 88, 12, 1)",
        "tension": 0.25,
        "yAxisID": "y1"
      }
    ]
  },
  "options": {
    "plugins": {
      "title": { "display": true, "text": "Illustrative AWS spend by category for a banking estate (not measured data)" },
      "legend": { "position": "bottom" }
    },
    "scales": {
      "y": { "beginAtZero": true, "max": 40, "title": { "display": true, "text": "Share of spend (%)" } },
      "y1": { "position": "right", "min": 0, "max": 100, "grid": { "drawOnChartArea": false }, "title": { "display": true, "text": "Cumulative (%)" } }
    }
  }
}

What this shows: in this illustrative profile roughly a dozen spending categories carry close to ninety percent of spend; it’s a category view rather than a strict product ranking, and your own curve is the one that matters.

When a dozen or so categories carry most of the spend, the commercial question stops being “can we leave AWS?” and becomes “can we replace these services?”, which is far more tractable because each has an identifiable replacement or migration path elsewhere.

flowchart LR
    subgraph Compute["Compute"]
        EC2["EC2<br/>Azure VMs · Compute Engine"]
        EKS["EKS<br/>AKS · GKE"]
    end
    subgraph Storage["Storage"]
        S3["S3<br/>Blob Storage · Cloud Storage"]
        EBS["EBS<br/>Managed Disks · Persistent Disk"]
    end
    subgraph Data["Data"]
        RDS["RDS PostgreSQL / MySQL<br/>Azure Database · Cloud SQL"]
        EC["ElastiCache Valkey<br/>Azure Cache · Memorystore"]
        MSK["MSK Kafka<br/>Kafka on AKS · GCP Managed Kafka"]
    end
    subgraph Network["Network"]
        ELB["ELB<br/>Load Balancer, App Gateway · Cloud LB"]
        VPC["VPC, Transit Gateway<br/>VNet, Virtual WAN · VPC, NCC"]
        DX["Direct Connect<br/>ExpressRoute · Interconnect"]
        R53["Route 53<br/>Azure DNS · Cloud DNS"]
    end
    subgraph Control["Security and operations"]
        KMS["KMS<br/>Key Vault · Cloud KMS"]
        CW["CloudWatch<br/>Azure Monitor · Cloud Monitoring"]
        IAM["IAM<br/>Entra ID, RBAC · Cloud IAM"]
    end
    subgraph Proprietary["Deliberate exceptions"]
        DDB["DynamoDB<br/>no like for like equivalent"]
        SFN["Step Functions<br/>Logic Apps · Workflows, different model"]
        LAM["Lambda<br/>Functions · Cloud Run, portable if thin"]
    end

    classDef open fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef concept fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef locked fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    class EC2,EKS,RDS,EC,MSK open
    class S3,EBS,ELB,DX,R53,CW,LAM concept
    class VPC,KMS,IAM,DDB,SFN locked

What this shows: green marks a portable workload or engine, amber a provider specific integration, and red something needing significant redesign or replacement. Even green items need their managed configuration, networking and identity rebuilt; it’s the workload that travels, not the setup around it.

The red items in the core columns aren’t avoidable; you can’t run on any cloud without its IAM, networking and key management. They’re where effort concentrates, which is why the platform contains them in a few Terraform modules rather than letting them spread through application code. One caveat matters: spend and lock in are different measures. An application whose bill is almost entirely EC2 can still depend on IAM, Route 53, KMS and CloudWatch, and a small, cheap proprietary dependency can make an otherwise portable application hard to move.

8. Using Proprietary Services Deliberately

I’m not arguing for refusing everything AWS specific. Some proprietary services really are better, and refusing them on principle is a permanent productivity tax paid to insure against an exit that may never happen. The discipline is to make the choice consciously, write it down and contain its blast radius.

flowchart TD
    Q1{"Is there an open source<br/>or open standard engine?"}
    Q1 -->|Yes| T1["Tier 1: Commodity<br/>Use freely through the platform"]
    Q1 -->|No| Q2{"Is the business logic separable<br/>from the service?"}
    Q2 -->|Yes| T2["Tier 2: Proprietary with escape hatch<br/>Thin adapter, logic in portable code"]
    Q2 -->|No| Q3{"Is the capability differentiating<br/>and worth the exit cost?"}
    Q3 -->|Yes| T3["Tier 3: Accepted lock in<br/>Architecture decision record, costed exit"]
    Q3 -->|No| T4["Tier 4: Decline<br/>Find a Tier 1 or Tier 2 alternative"]

What this shows: three questions turn every proprietary dependency into a recorded decision with a known exit cost.

A Lambda function whose handler is a thin adapter over a portable Java library is Tier two; one entangled with event formats and IAM assumptions is not. Aurora is usually Tier two for applications that only use the standard PostgreSQL or MySQL interface, but lean on its storage, replication or failover characteristics and it drifts towards Tier three. Tier three needs an architecture decision record with an owner, an exit estimate and a review date. Nobody ever wakes up and decides to lock their bank into a cloud provider. It happens through thousands of small, unrecorded decisions, each perfectly reasonable on the day, and this framing makes them visible one at a time.

The objective isn’t zero lock in. That would be an expensive fiction, because while you run on AWS you will always depend on its identity, networking, cryptography and billing. The objective is bounded lock in: every dependency understood, every proprietary choice deliberate, and the effort to replace it estimated and regularly challenged. A bank doesn’t need to be independent of every service it consumes. It needs to avoid becoming dependent on services it can’t afford to replace.

9. Using AI to Find the Gaps

The textbook answer to “how do you know your exit plan works?” is to prove it by rebuilding workloads on another provider. At banking scale that means a second identity estate, networking, security controls, tooling and people, maintained to validate an event that may never happen, and a successful rebuild today tells you very little about the dependency an engineer adds tomorrow. I’m not going to spend that kind of money.

What I’d do instead is use AI to look for the risks and gaps continuously. The things that make an exit hard are written down somewhere: in Terraform state, in application code, in IAM policies, in CloudTrail, in the Backstage catalogue and in architecture decision records. Nobody can read all of it, but a model can work through far more of it, far more systematically, than any review team, provided the inventory is complete and the model has the access and context it needs, and we already use Claude to diagnose production incidents in real time, so pointing it at exit risk is a small step rather than a new programme.

flowchart LR
    subgraph Sources["What the estate already records"]
        S1["Terraform and Odin modules"]
        S2["Application code and SDK calls"]
        S3["IAM policies and CloudTrail"]
        S4["Backstage catalogue"]
        S5["Architecture decision records"]
        S6["Cost Explorer"]
    end
    AI["AI review<br/>on every change and on a schedule"]
    subgraph Out["What comes out"]
        F1["Undeclared proprietary dependencies"]
        F2["Drift from golden paths"]
        F3["Exit time and cost estimates"]
    end
    H["Engineers triage, fix or record an ADR"]
    Sources --> AI --> Out --> H
    H -. "next review" .-> AI

What this shows: exit assurance becomes a continuous loop over data the estate already produces, with people deciding what each finding means.

The questions are mundane, which is the point. Which services call AWS specific APIs directly instead of going through the platform? Which databases use Aurora only features or extensions outside the compatibility profile? Which Lambda functions have business logic tangled up with event formats? Which Tier three dependencies have no architecture decision record, or one past its review date? Where have teams drifted from the golden paths with hardcoded ARNs or hand built resources? Each answer becomes a ticket with an owner, and the trend over time tells a board more than any one off exercise would.

AI doesn’t replace judgement here. It produces candidates, some of which will be false positives, and engineers still decide whether a finding matters, fix it or record the decision. It also has a hard limit: reading code isn’t running it. AI can tell you a database uses an extension that doesn’t exist on the target; it can’t tell you the restore will finish in time. So the few assumptions that only execution can settle get cheap, targeted checks rather than a full rebuild: the platform’s database compatibility profile tested automatically in the pipeline, logical exports restored into a plain PostgreSQL container in CI, and the restore and reconciliation timings we already collect from disaster recovery. Be clear about what these prove, though. They prove component portability, that the data and engines will move, not end to end exit feasibility: a container restore says nothing about standing up the target’s networking, IAM, key management and security controls, and restore timings inside AWS are a guide to, not a prediction of, timings elsewhere. For the handful of workloads critical enough to need more, a short lived, isolated test on alternative infrastructure fills that gap, and it’s a very different thing from running a second production estate. Put together, you get a review on every change, small automated tests for the bits that need execution, and gaps found while they’re still small.

Then there’s the money, which engineers (me included) tend to forget. If leaving takes ninety days but costs more than you’d ever save, you’ve reduced technical dependency without reducing commercial dependency. So exit capability needs two measures, time and cost, including the unglamorous items such as egress and KMS request volume, both compared against the cost of staying. Migration will never be cheap. It needs to be bounded, understood and proportionate, because that’s what turns architecture into negotiating leverage. Put together, a board gets something far more useful than a provider count. AI can help here too, turning the dependency inventory into a first draft of the effort and cost to move each service, which architects then challenge. The values below are illustrative only.

MetricIllustrative valueEvidence
Exit time, critical estate90 daysEstimated, using measured restore and reconciliation timings
Contractual runway12 monthsNotice and transition terms in the agreement; operational runway assessed separately
Estimated cost to exitR45mModelled from actual volumes, egress and KMS request rates
Proprietary dependencies found by AI review214Automatically detected, engineer reviewed
Of those, covered by an architecture decision record88%Measured against the dependency inventory
High severity exit findings open longer than 90 days6Measured from the engineering workflow
Database compatibility checks passing97%Automated tests in CI

What this shows: the Evidence column separates what has been observed from what has been estimated, which is the distinction a risk committee cares about most.

10. What This Means for the Regulatory Conversation

Regulators are right to worry about cloud concentration; when much of a banking system runs on a few hyperscalers, the terms on which those providers serve it are a systemic question. But look at what the frameworks actually ask for. In South Africa, the Prudential Authority’s Directive 3 of 2018 and Guidance Note 5 of 2018 require board approved data strategies and risk based governance for material cloud arrangements, Joint Standard 1 of 2023 commenced on 15 November 2024 and puts accountability for IT risk on the governing body, and Joint Communication 2 of 2025 says a dedicated cloud Joint Standard is being developed. In the EU, DORA Article 28(8) requires tested exit strategies for ICT services supporting critical or important functions, and Article 29 requires an assessment of concentration risk before contracting. Australia’s CPS 230, in force since July 2025, requires management of material service providers alongside tested business continuity. As far as I can tell, none of them requires two production cloud providers. What they want is governance, awareness of concentration, continuity and a credible exit plan. DORA does expect exit strategies to be tested, but that doesn’t mean running a second production bank. Continuous AI assisted analysis, automated compatibility testing, measured restore timings and, where a workload’s criticality justifies it, occasional short lived validation in an isolated environment on alternative infrastructure can provide real evidence without permanently operating two clouds. Testing should be proportionate to how critical the workload is and to the risk the exit plan is meant to address.

A well evidenced exit plan shows mitigation; it isn’t proof a supervisor must accept every residual risk, and if I were in their seat I’d keep pushing too. But the response to that pressure should be evidence about specific services, not a second cloud whose integration risks land on everything. AWS has argued to APRA that single provider concentration often leads to lower net risk than a multi provider strategy; as someone who used to work there I’d discount that accordingly, but the engineering argument stands regardless of who makes it. So the question I’d love to have with supervisors isn’t “how many providers do you use?” but “can you show you could leave, how long would it take, what would it cost, and what are you doing to make each of those smaller?”

11. The Takeaway

Single supplier risk is real, and anyone telling you otherwise isn’t taking it seriously. But it’s mostly commercial and structural, best managed through exit capability built in the layers you own: a platform that provisions everything, a toolchain that moves with you, keys that stay inside the hardware boundary they were born in, open source engines holding the data, and every proprietary dependency a recorded decision. None of it is free, and it only counts if you keep looking for the gaps and put a price on leaving. Give two banks the same engineering maturity and budget, and the one that concentrates it on running one provider well, with exit times and costs it actually measures, will usually carry less net risk than the one spreading it across two providers for workloads that can’t benefit. The goal was never to be on two clouds. The goal was to never be trapped on one.

Appendix A: Measuring Your Own Exposure

This script pulls the last three full months of spend by service, splits RDS by database engine so Aurora is classified separately, and reports spend by portability tier and the concentration in your top twelve services. It needs Python 3, boto3 and Cost Explorer read access, and uses amortised cost with credits and refunds excluded, so Savings Plans and reserved instances are attributed to the services that consumed them. Spend is an exposure indicator, not a measure of switching difficulty, so pair it with the per service dependency findings from the AI review in section 9.

cat > cloud_portability_exposure.py << 'EOF'
#!/usr/bin/env python3
"""Report AWS spend by portability tier for the last three full months.

Requires boto3 and AWS credentials with Cost Explorer read access
(ce:GetCostAndUsage), ideally from the management account.
"""
import datetime
from collections import defaultdict

import boto3

RDS_SERVICE = "Amazon Relational Database Service"

TIER_MAP = {
    "Amazon Elastic Compute Cloud - Compute": "1 commodity",
    "EC2 - Other": "1 commodity",
    "Amazon Elastic Container Service for Kubernetes": "1 commodity",
    "Amazon Elastic Kubernetes Service": "1 commodity",
    "Amazon Simple Storage Service": "1 commodity",
    "Amazon ElastiCache": "1 commodity",
    "Amazon Managed Streaming for Apache Kafka": "1 commodity",
    "Amazon Elastic Load Balancing": "1 commodity",
    "Elastic Load Balancing": "1 commodity",
    "Amazon Virtual Private Cloud": "1 commodity",
    "AWS Direct Connect": "1 commodity",
    "Amazon Route 53": "1 commodity",
    "AWS Key Management Service": "1 commodity",
    "AmazonCloudWatch": "1 commodity",
    "Amazon CloudWatch": "1 commodity",
    "AWS Lambda": "2 escape hatch",
    "Amazon Simple Queue Service": "2 escape hatch",
    "Amazon Simple Notification Service": "2 escape hatch",
    "AWS Step Functions": "3 accepted lock in",
    "Amazon DynamoDB": "3 accepted lock in",
    "Amazon Kinesis": "3 accepted lock in",
    "Amazon Bedrock": "3 accepted lock in",
}


def rds_engine_tier(engine):
    """Aurora's engine is portable but its storage layer is not."""
    if "aurora" in engine.lower():
        return "2 escape hatch"
    return "1 commodity"


def month_start(d, months_back):
    y, m = d.year, d.month - months_back
    while m <= 0:
        m += 12
        y -= 1
    return datetime.date(y, m, 1)


def cost_by(ce, start, end, key, service_filter=None):
    totals = defaultdict(float)
    token = None
    while True:
        kwargs = dict(
            TimePeriod={"Start": start.isoformat(), "End": end.isoformat()},
            Granularity="MONTHLY",
            Metrics=["AmortizedCost"],
            GroupBy=[{"Type": "DIMENSION", "Key": key}],
        )
        # Amortised cost spreads Savings Plans and reserved instances across
        # the services that used them; credits and refunds are excluded so
        # they don't distort the tier shares.
        not_credits = {
            "Not": {
                "Dimensions": {
                    "Key": "RECORD_TYPE",
                    "Values": ["Credit", "Refund"],
                }
            }
        }
        if service_filter:
            kwargs["Filter"] = {
                "And": [
                    not_credits,
                    {"Dimensions": {"Key": "SERVICE", "Values": [service_filter]}},
                ]
            }
        else:
            kwargs["Filter"] = not_credits
        if token:
            kwargs["NextPageToken"] = token
        resp = ce.get_cost_and_usage(**kwargs)
        for period in resp["ResultsByTime"]:
            for group in period["Groups"]:
                amount = float(group["Metrics"]["AmortizedCost"]["Amount"])
                totals[group["Keys"][0] or "unknown"] += amount
        token = resp.get("NextPageToken")
        if not token:
            return totals


def main():
    ce = boto3.client("ce", region_name="us-east-1")
    end = datetime.date.today().replace(day=1)
    start = month_start(end, 3)

    by_service = cost_by(ce, start, end, "SERVICE")
    rds_by_engine = cost_by(ce, start, end, "DATABASE_ENGINE", RDS_SERVICE)

    total = sum(by_service.values()) or 1.0
    by_tier = defaultdict(float)
    for service, amount in by_service.items():
        if service == RDS_SERVICE:
            continue
        by_tier[TIER_MAP.get(service, "unclassified")] += amount

    rds_engine_total = sum(rds_by_engine.values())
    for engine, amount in rds_by_engine.items():
        by_tier[rds_engine_tier(engine)] += amount
    # RDS costs with no engine attribution (for example some storage or
    # snapshot charges) are counted as commodity.
    by_tier["1 commodity"] += max(by_service.get(RDS_SERVICE, 0.0) - rds_engine_total, 0.0)

    print(f"Spend from {start} to {end} (exclusive): {total:,.2f} USD\n")
    print("Spend by portability tier")
    for tier, amount in sorted(by_tier.items()):
        print(f"  {tier:<22} {amount:>14,.2f}  {amount / total:6.1%}")

    if rds_by_engine:
        print("\nRDS spend by engine")
        for engine, amount in sorted(rds_by_engine.items(), key=lambda kv: -kv[1]):
            print(f"  {engine:<30} {amount / total:6.1%}  {rds_engine_tier(engine)}")

    ranked = sorted(by_service.items(), key=lambda kv: kv[1], reverse=True)
    top = ranked[:12]
    top_share = sum(a for _, a in top) / total
    print(f"\nTop 12 services account for {top_share:.1%} of spend")
    for service, amount in top:
        tier = "split by engine" if service == RDS_SERVICE else TIER_MAP.get(service, "unclassified")
        print(f"  {service:<50} {amount / total:6.1%}  {tier}")

    unclassified = [
        s for s in by_service
        if s not in TIER_MAP and s != RDS_SERVICE and by_service[s] > 0
    ]
    if unclassified:
        print("\nUnclassified services (add these to TIER_MAP):")
        for s in sorted(unclassified):
            print(f"  {s}")


if __name__ == "__main__":
    main()
EOF
chmod +x cloud_portability_exposure.py
pip install --quiet boto3
python3 cloud_portability_exposure.py

Appendix B: Service Mapping Reference

AWS serviceAzure nearest equivalentGoogle Cloud nearest equivalent
EC2 and EBSVirtual Machines, Managed DisksCompute Engine, Persistent Disk
EKSAKSGKE
S3Blob StorageCloud Storage
RDS for PostgreSQL and MySQLAzure Database for PostgreSQL and MySQLCloud SQL
ElastiCache (Valkey)Azure Cache for RedisMemorystore
MSKKafka on AKS, or a managed Kafka partner (Event Hubs is protocol compatible only)Managed Service for Apache Kafka
Elastic Load BalancingLoad Balancer, Application GatewayCloud Load Balancing
VPC and Transit GatewayVirtual Network, Virtual WANVPC, Network Connectivity Center
Direct ConnectExpressRouteCloud Interconnect
Route 53Azure DNSCloud DNS
KMSKey VaultCloud KMS
CloudWatchAzure MonitorCloud Monitoring and Logging
IAMEntra ID, Azure RBACCloud IAM

Equivalent doesn’t mean identical. Event Hubs accepts Kafka clients but isn’t a Kafka cluster, Kubernetes manifests port well but the networking and identity underneath don’t, IAM is usually the largest single translation cost, and instrumenting with OpenTelemetry rather than a provider’s metrics API is one of the cheapest portability investments you can make.

Leave a comment

Your email address will not be published. Your first comment is held for approval.