Multi-Cloud DevOps Strategies: How to Avoid Vendor Lock-In Across AWS, Azure & GCP

Table of Contents

What We’ve Learned From Building Cloud Infrastructure Without Getting Trapped

We’ve built cloud infrastructure for clients across banking, healthcare, AI startups, and SaaS companies. Each engagement comes with its own mess of constraints. Budget is always tighter than the brief suggests. Timelines are always shorter than what’s reasonable. And somewhere in the middle of all that, we have to make sure the architecture doesn’t become a trap.

Nobody sets out to get locked into a single cloud provider. It’s never a conscious decision. It’s a hundred small ones. Someone picks RDS because it’s easy. Someone else builds the pipeline around CodePipeline because it’s already wired up. Someone writes a Lambda function, then another, then ten more. And then one day the CTO asks “could we move this to GCP?” and the room goes very quiet.

I’ve been in that room. More than once.

Here’s what we’ve actually learned about working across AWS, Azure, GCP, and hybrid setups – see also our AWS vs Azure vs GCP comparison – without painting clients into a corner.

Lock-In Isn’t Really a Tech Problem

I used to think of vendor lock-in as an architecture thing. It’s not. It’s a money thing.

When a client says “we want to move off AWS,” what they’re really asking is: how much is this going to cost, and how long is it going to hurt? If the answer is “a couple of weeks, mostly configuration,” the client isn’t locked in. If the answer is “six months, probably a rewrite,” they are.

The annoying part is that nobody thinks about this when things are working fine. They think about it when the pricing changes, or a region has issues, or some new compliance rule means the data can’t live where it’s been living. By then the client is already committed.

We’ve watched this happen up close. A fintech client wanted to expand into a new region. Their provider barely had presence there. What should’ve been a straightforward expansion turned into a six-figure headache because nothing was portable.

Another time – a healthcare project. Azure AD made sense for identity. That was the right call. But the team before us had also wired Azure-specific APIs deep into the application logic. Not the identity layer – the business logic. So moving anything meant rewriting everything.

And then there was an AI pilot that ran perfectly on cheap GPU instances. The business loved it and wanted to scale. Except the model-serving layer was tangled up with one provider’s ML toolkit and couldn’t be extracted without starting over.

None of these are exotic scenarios. This is just what happens when people don’t think about switching cost upfront.

How We Pick Infrastructure

There’s no framework pinned to a wall. But we run through the same questions every time, before opening Terraform, before sketching a diagram, before anything.

What’s the budget, and when does this need to work?

This one question kills most theoretical debates. If someone’s building an MVP on a tight budget, they don’t need multi-AZ Kubernetes. They need a VM, a database, Nginx, and a deploy script that doesn’t break.

On a Laravel/MySQL project, we laid out two paths for the client. Path A was single-AZ – one EC2 instance, RDS, Nginx, PHP-FPM. Straightforward. Path B was the full production setup – multi-AZ RDS, ALB, the whole thing. About 35–40% more expensive.

The client went with A. They haven’t had a minute of downtime since.

Multi-AZ feels safer, and we understand the instinct. But here’s the thing about MVPs – the traffic is low, the user base is small, and there’s no revenue stream yet that justifies paying for redundancy that won’t be touched for months. Multi-AZ failover, cross-AZ load balancing, redundant NAT gateways – those are patterns for systems where five minutes of downtime costs somebody real money. An MVP isn’t that. Not yet.

What an MVP actually needs is the ability to recover – not to never go down, that’s a different price point, but to come back quickly when something does go wrong. So we built a backup and restore process that gets the database back in under an hour. The Terraform code recreates the compute layer in a different AZ in minutes. The app is stateless, so there’s nothing to lose on that side.

I put this tradeoff in front of every MVP client the same way: you can spend 35–40% more for automatic failover you probably won’t need, or you can put that money into building the product and upgrade the infrastructure when your numbers justify it. Everyone picks the second option. Every single time.

The important part is documenting the upgrade path. The Terraform is structured so that going from single-AZ to multi-AZ is a config change. Adding an ALB and a second instance is less than a day’s work when the time comes. The architecture doesn’t fight growth – it just doesn’t pay for growth that hasn’t shown up yet.

Can the client’s team actually run this?

This is the question that gets skipped the most, and it causes more damage than any wrong architecture choice.

We’ve seen proposals – good ones, technically – that recommended Kubernetes for a three-person team that had never touched a cluster. Suddenly they’re expected to manage Helm charts, ingress controllers, pod autoscaling, node pools, persistent volumes. Last month they were doing manual SSH deploys. That’s not DevOps. That’s setting people up to fail.

On the Laravel project, we didn’t containerize anything, because the client’s team didn’t use Docker locally. They knew PHP, Nginx, and how to SSH in and read logs. So we gave them EC2 with PHP-FPM, wrote the whole thing in Terraform, and hooked up CI/CD from Git. If they ever need to move to GCP, the Terraform modules show exactly what to rebuild – but they don’t need to learn containers to get there.

On another project – five GitLab repos, multiple services – Docker made complete sense. Developers were already running containers locally, and Docker Compose mirrored production. The consistency was real, not theoretical. We’ve written separately about deploying containerized apps like this in our guide to deploying Django apps on Kubernetes.

Match the tool to the team. Not the other way around.

Where does portability actually matter?

Honest answer: not everywhere.

App runtime? Yes, keep it portable. CI/CD? Should work on any cloud. Terraform? Always. Docker images? Don’t wire them to one provider’s APIs.

But some things are just better as managed services, and pretending otherwise wastes time.

We use RDS on nearly every project that needs PostgreSQL. Self-hosting a database on EC2 means owning backups, patches, failover, and monitoring – all of it. RDS handles that. Yes, it ties the client to AWS. But the data is standard PostgreSQL, and moving it later means pg_dump and restore. Not fun, but doable.

What we avoid is reaching for Aurora-specific features unless the workload genuinely demands them. Aurora Serverless v2 and Aurora Global are harder to walk away from. Standard RDS PostgreSQL is a deliberate choice – managed infrastructure with a reasonable exit.

A Banking Platform. VMs. No Kubernetes. No Drama.

This is one of the engagements we’re genuinely proud of.

Banking operations platform. Predictable traffic. Small team. Serious security requirements. The client needed something solid, not something flashy.

So we built it on VMs. Nginx. Application runtime on the instance. Managed PostgreSQL. Terraform for the whole thing. CI/CD. Security groups locked tight.

For DR, we set up daily RDS snapshots with cross-region copy (see AWS’s documentation on copying RDS snapshots across regions). If the primary AZ goes down: restore the snapshot to another AZ, spin up EC2 from Terraform, update DNS. Done. The client’s team rehearsed this – not read about it in a doc, actually did it. Recovery target is under an hour.

We also built a health check that watches the app endpoint, the database connection, and disk usage. If anything fails, the on-call person gets an alert with a link to the exact runbook for that failure – not just “something’s broken,” the actual steps.

People look at this setup and say it’s not modern. We look at it and see a system that’s been running for months without anyone losing sleep. The ops team knows every moving part. Cost is predictable. Backups work. Deploys take minutes.

Kubernetes would’ve added serious overhead here – cluster management, nodes, pods, Helm charts – for an app that doesn’t have 15 microservices. It’s a backend, a frontend, and a database. Sometimes the boring answer is the right answer. (We’ve gone deeper on when Kubernetes complexity is and isn’t worth it in our Kubernetes cost optimization guide.)

A Healthcare App Where We Said Yes to Lock-In

This project reinforced something we keep telling other architects: sometimes lock-in is the right call. The key is being honest about where the boundary is.

Healthcare. Compliance everywhere. The client’s org ran on Azure AD. Secrets had to go through Key Vault with RBAC. Auditability wasn’t negotiable.

We didn’t fight it. Azure was the right answer for identity and secrets. Trying to abstract that away would’ve added weeks of integration work for zero business value.

But we drew a line.

The Django backend doesn’t touch Azure APIs directly. Secrets come in through environment variables. Key Vault populates them at deploy time. The app reads standard config. If the client decides to move to AWS next year, Key Vault swaps for Secrets Manager and the injection layer gets updated – the app doesn’t notice.

Deployment was Docker, Terraform, CI/CD with approval gates. Dev, staging, prod – all separated. Azure Site Recovery handled VM replication to a paired region. The pipeline could target either region for failover.

In the proposal, we wrote it down plainly: “Azure dependency at identity and secrets. Application layer portable. Switching cost for identity: high. For the app itself: low. DR failover to paired region: tested.”

Leadership read that and knew exactly what they were buying. That’s the point.

An AI Pilot Where GCP Was the Only Sensible Choice

This one reinforced that the right cloud for a pilot and the right cloud for production might be completely different – and that’s fine.

The client needed LLM inference – vLLM, Ollama, GPU compute. The kind of workload where a cloud bill can get ugly fast if nobody’s paying attention.

AWS would’ve been our default; our engineers live in AWS day to day. But on-demand GPU instances there were significantly more expensive than what GCP was offering.

GCP’s preemptible T4 instances came in at about 70% less. They can get interrupted – but for a pilot running inference where the occasional restart is fine, that’s not a problem, that’s a feature.

So we built it on GCP. FastAPI wrapper around the model. Docker. GCS for model artifacts – but through an abstraction layer, not direct GCS calls. Terraform. Monitoring for latency and GPU utilization.

The key decision was what happens when the pilot works, because pilots that work create pressure to just keep running them on whatever they’re already running on. We didn’t want that, so the application layer was built to not care which cloud it’s on. If the business wanted to scale on AWS Inferentia, stick with GCP on reserved instances, or go Azure NC-series because of an enterprise agreement – all possible. Only the Terraform changes.

The client proved the concept without committing to long-term GPU spend. That’s how pilots should work.

A SaaS Platform That Actually Needed Kubernetes

We spend a lot of time talking clients out of Kubernetes – so it’s worth covering a project where it was exactly right.

Growing SaaS product. Multiple microservices. Frequent releases. Different services needed different scaling. The client’s team was deploying several times a week, and rollbacks on VMs meant manually redeploying the previous version.

Kubernetes fixed real problems here. Each service runs in its own pods with resource limits, so one service spiking doesn’t starve the rest. Rolling deployments mean new versions go out gradually, and if something breaks, it rolls back on its own. The payments team deploys on Tuesday, notifications on Thursday – nobody waits for anybody else. (Our GitOps guide to managing Kubernetes at scale covers how teams keep this kind of multi-service environment consistent and auditable.)

Terraform for the cluster, Helm for deployments, ingress controller, centralized secrets, full observability – metrics, logs, alerts.

More complex than VMs? Of course. But the complexity had a reason: the business needed faster releases, isolation between services, and scaling that VMs couldn’t give without a pile of custom scripts.

The difference here was the client’s team. They’d worked with containers before. They understood pods and deployments – they weren’t learning Kubernetes while also trying to deliver features. That gap – between a team that’s ready and a team that isn’t – is where Kubernetes projects either succeed or turn into nightmares.

Disaster Recovery Without the Theater

DR is the part everyone agrees matters and nobody wants to pay for.

I’ve sat in meetings where someone says “we need disaster recovery” and what they really mean is “I need to stop worrying.” Those are different things. One costs planning. The other costs money. Usually a lot of it.

Figure out what actually matters

Not everything is equally important. A marketing site can be down for an hour and nobody notices. A payment system can’t. An internal dashboard can show stale data for a few minutes. A patient record system needs point-in-time recovery.

We sort services into three tiers:

Tier 1 – back within an hour, near-zero data loss. Gets automated failover, cross-region backups, tested restore.

Tier 2 – back within four hours, can lose up to an hour of data. Daily backups with cross-region copy, documented recovery steps.

Tier 3 – can handle a day of downtime. Daily backups, Terraform template to recreate from scratch.

Most clients’ infrastructure lands in Tier 2 or 3 once it’s actually sorted this way – a realization that alone saves a significant chunk of the budget.

Build failover that people can actually use

We’ve seen 40-page DR plans sitting in SharePoint folders that nobody’s opened since the day they were uploaded. That’s not disaster recovery – that’s a box-checking exercise.

What we build instead depends on the stack.

On AWS, we set up RDS snapshots with cross-region replication. If the primary region fails: restore snapshot in secondary region, run Terraform for compute and networking, update Route 53. The client’s team practices this quarterly – not reads about it, does it.

On the healthcare project with Azure, it was different. Azure Site Recovery for VM replication. Key Vault secrets replicate automatically. The CI/CD pipeline can target either region – but there’s a manual approval step before traffic switches. Healthcare, patient data – you want a human in that loop.

On the multi-service AWS platform, Docker images sit in ECR with cross-region replication. Terraform state is in S3 with versioning. If the primary region goes away, Terraform points at another region, Docker pulls the images, the app starts, DNS updates. Thirty to forty-five minutes.

On the GCP AI pipeline, DR is simpler because the workload is built to handle interruption. Preemptible instances get reclaimed all the time – the system restarts automatically. Model artifacts are in multi-region GCS. Zone fails, orchestrator spins up new instances elsewhere. No human needed.

Test it. For real.

We run DR drills on every project where the client allows it – not tabletop exercises where everyone nods and agrees the plan looks fine, actual recovery.

We kill the primary database. Watch the snapshot restore. Time it. Check data integrity. Make sure the app reconnects. Write down everything that went wrong.

And something always goes wrong: a connection string hardcoded in some config file nobody remembers, a cron job that doesn’t restart, an SSL cert that’s region-specific, a monitoring alert still pointing at the old endpoint while the new one is perfectly healthy and nobody knows.

Those are the things that turn a 30-minute recovery into a 4-hour scramble. You don’t find them in documentation reviews. You find them by breaking things on purpose.

Keep the cost proportional

For the banking platform, we didn’t build a hot standby – that would’ve nearly doubled the cost for a system that’s never gone down. Instead: warm recovery. Cross-region snapshot, Terraform ready to go, DNS TTL low enough for fast cutover. Under an hour to recover. Monthly cost is basically just snapshot storage.

For the SaaS Kubernetes platform, it was a different story. The business needed near-zero downtime. The cluster spans multiple AZs. Pod disruption budgets mean that if an AZ drops, pods reschedule to surviving nodes. The database is multi-AZ with automated failover. Recovery in minutes.

DR should match what the business actually needs. Most apps don’t need active-active across regions. Most do need a tested plan, even if that plan is “run this command and wait 45 minutes.”

Cloudflare in Front of Everything

One pattern we keep coming back to that doesn’t get talked about enough: Cloudflare as the edge layer, regardless of what’s behind it.

On AWS projects with EC2 and Nginx, we put Cloudflare in front. TLS termination, DDoS protection, caching, HSTS – Cloudflare handles it. AWS handles compute.

Why care? Because the edge becomes cloud-independent. If the backend moves from AWS to GCP next quarter, Cloudflare stays put. DNS changes; everything else stays the same. Smaller migration surface.

It’s also cheaper for bandwidth-heavy workloads. Cloudflare’s free tier handles more than most expect, while CloudFront bills per GB. On budget-sensitive projects – which is most of them – that difference matters.

Nobody writes conference talks about “we use Cloudflare and AWS.” But as a practical lock-in reduction strategy, it’s one of the best moves we’ve found.

Docker – When It Earns Its Place

Simple rule: if the client’s developers are already running containers locally, containerize production. If they’re not, think hard before adding that complexity.

Docker fixes real things – environment consistency, the disappearance of “works on my machine” once everyone’s running the same image, CI that builds what production runs, and new developers who don’t spend two days installing dependencies.

But it also adds work. Dockerfiles need maintaining. Base images need patching. PHP-FPM in a container doesn’t behave like PHP-FPM on a VM. If the team doesn’t understand containers, production debugging gets worse, not better.

On the multi-service platform – five repos, developers already in containers – Docker was a no-brainer, and Compose mirrored prod. On the Laravel project – small team, stable deploy, zero container experience – Docker would’ve been a mistake. We would have spent more time teaching Docker than building infrastructure.

It’s not about whether Docker is good. It’s about whether it solves a problem the team currently has.

Terraform Is the One Non-Negotiable

If we could only pick one tool to reduce lock-in, it’d be Terraform. Not because it makes things magically portable – it doesn’t. A Terraform file can’t be copied from AWS to GCP and expected to work.

But it does something more valuable: it makes every infrastructure decision visible. In code. Reviewable. Searchable.

When everything’s in Terraform, I can look at a project and immediately see what exists, how it’s connected, what the security rules are, roughly what it costs. When it’s configured through a console, that knowledge lives in someone’s head. When they leave, it leaves with them.

Terraform also makes DR real instead of theoretical. Region goes down? There’s no guessing what to rebuild – the blueprint exists. Change the provider, adjust resource names, apply. Not instant, but hours, not weeks.

Every project covered here – the Laravel app, the healthcare system, the multi-service platform, the AI pipeline, the SaaS product – has Terraform as the common thread. Even when provider-specific services are in play, the infrastructure is documented as code.

What Goes in Every Proposal

Every infrastructure proposal we write includes switching costs – not in an appendix, but right next to the monthly estimate, where leadership will actually see it.

For a recent Django/React/Celery project, it looked like this:

RDS PostgreSQL – managed, automated backups. Switching cost: medium. Needs schema export and data migration. A few days of planned work.

EC2 with ALB – right-sized instances. Switching cost: low. Stateless. Terraform rebuilds it anywhere.

Amazon MQ for RabbitMQ – switching cost: low. RabbitMQ is open source. Self-host it or use any other managed broker.

CloudFront/S3 for static content – switching cost: low. Any CDN plus object storage works.

The goal isn’t to scare clients away from managed services – it’s to make sure leadership knows what they’re buying. A monthly bill with documented switching costs is useful. The same bill where nobody knows what happens if the client needs to move is a liability.

Takes about 30 minutes to add this to a proposal. Saves weeks of confusion later.

Mistakes Along the Way

We’re candid that we haven’t gotten everything right.

Early on, I over-built something badly. Multi-AZ everything. Managed services stacked on managed services. Monitoring dashboards for metrics nobody was looking at. It was “best practice” and also twice as expensive as it needed to be. Six months later the client scaled down and most of that infrastructure sat there burning money.

Another time, I told a client a migration would take “a couple of days.” Took two weeks. Edge cases in data formatting. Connection pooling behaving differently. Config assumptions that were true on one service and false on the other.

Those experiences changed how I work. I pad my estimates. I document assumptions so that when something breaks – not if – at least we know which assumption was wrong. The best infrastructure decision isn’t the most impressive one. It’s the one that still looks right six months later.

What We Tell CTOs

If a CTO asks how to avoid vendor lock-in, we push back on the question.

You don’t avoid it. You manage it.

Pick one primary cloud. For most of our clients, that’s AWS – the tooling is mature, the talent pool is big, and the documentation is better than the alternatives. Use it. Commit. Build real expertise.

But do three things:

Keep CI/CD and IaC portable. GitLab CI or GitHub Actions, not CodePipeline. Terraform, not CloudFormation – unless the client is absolutely certain they’ll never leave AWS. Nobody’s ever certain.

Keep cloud logic out of the application. Environment variables. Abstraction layers. Standard interfaces. The app shouldn’t know or care which cloud it’s running on.

Document switching costs. Not because a move is planned, but because someday the business might need one – and “we can move the database in three days and the compute in one” is a much better answer than “we’d have to figure that out.”

That’s not a multi-cloud strategy. That’s just good engineering.

One Last Thing

Multi-cloud DevOps isn’t about using every provider. It’s not about avoiding managed services. It’s not about Kubernetes everywhere, Docker everywhere, or any one tool everywhere.

It’s about making decisions with eyes open.

Sometimes a VM is the right answer. Sometimes Docker is enough. Sometimes Kubernetes is necessary. Sometimes a managed service is worth the dependency. Sometimes a project needs GCP for GPU pricing, AWS for the rest, and Azure because the client’s identity system lives there.

The skill isn’t in knowing best practices. It’s in reading the situation – the business need, the team’s capability, the budget, the timeline – and picking what fits all of those. Not just the thing that looks good on a conference slide. I don’t always get it right. But I always make sure the decision is written down, the tradeoffs are visible, and the business knows what it chose.

Everything else is implementation detail.

Facing a similar lock-in question, or planning a cloud migration of your own? Capital Numbers works across AWS, Azure, and GCP, and can walk through the switching-cost tradeoffs for your specific setup – see our DevOps consulting services and cloud engineering services.

Bibek Mukherjee, Manager – IT & DevOps

Bibek is a seasoned DevOps lead with over a decade of experience in optimizing and automating mission-critical deployments in both on-premises and cloud environments. He specializes in CI/CD pipeline creation, infrastructure management, and DevSecOps practices. His expertise spans various cloud platforms, including AWS and Azure, and he is proficient in programming languages like Python, Bash, and Go. Skilled in tools such as Jenkins, Docker, and Kubernetes, Bibek excels at enhancing software quality, streamlining operations, and accelerating release cycles.

Share

Recent Awards & Certifications

  • Employer Branding Awards
  • Times Business Award
  • Times Brand 2024
  • ISO
  • Promissing Brand
[class^="wpforms-"]
[class^="wpforms-"]