What Migrating 60+ Microservices Between Clouds Taught Me
Lessons from moving 60+ production microservices from AWS to Oracle Cloud at a Canadian fintech: what actually took the time, what broke, and what I'd do again.
For the last couple of years, a big part of my job at a Canadian fintech was moving our platform from AWS to Oracle Cloud. Lending, investing and payments: 60+ microservices across five environments, all ending in one production cutover weekend.
The cutover worked. No rollback, and the first full trading week on the new cloud was clean. But most of what I learned had nothing to do with Kubernetes manifests. Here's what actually mattered.
1. The inventory is the project
A "service" turns out to be a fuzzy word. Once you count cron jobs, Lambda functions, queue consumers, small "temporary" scripts that have quietly run in production for years, and services that only exist in one environment, the list is always longer than anyone's first guess.
Every item you miss in the inventory shows up later as an outage. Before you plan anything, list every process that runs, what triggers it, what it reads, what it writes, and who notices when it stops.
2. Replace managed glue early, not at the end
The hardest part wasn't moving containers. It was the cloud-specific glue around them: SNS/SQS topics, S3 event triggers, Lambda functions fired by bucket uploads, IAM-scoped access.
We moved event workflows from SNS/SQS to RabbitMQ, converted Lambdas into normal containerized Node.js services, and refactored the storage layer of 18 services to use S3-compatible APIs so the code didn't care which cloud it was on. Doing this before the move meant we could test each change on the old cloud, one workflow at a time, with QA validating the real user journey (orders, watchlists, price alerts) rather than a big-bang switch.
If I did it again, I'd start here on day one.
3. Environments drift, and drift is silent
With five environments (sandbox, dev, QA, staging, prod), configuration drifts constantly: a secret that exists in prod but not staging, a feature flag missing from one database, a database migration that ran by hand in one place and never in another.
None of these fail loudly. They fail as a 500 on one endpoint, or a service quietly falling back to localhost. What helped:
- Secrets in one place (Vault), synced into the cluster, and populated for every environment from a single source of truth.
- Migrations that run on deploy, not by hand. If a human has to remember it, it will drift.
- Fail fast on mismatched config. One of my favourite small fixes was making an app refuse to start if its build environment and runtime environment didn't match.
4. Rehearse the cutover like a release, not a meeting
Our cutover date moved several times. That was the right call every time: the go/no-go meetings caught real gaps.
The most valuable thing we did was a full tabletop walkthrough of the cutover sequence: every step got an owner, a procedure, and a proof of completion. "Database migrated" isn't a step. "Row counts match on these five tables, checked by X" is.
And we set a hard rollback checkpoint: if testing wasn't done by a fixed time on Saturday morning, we'd roll back, no debate. Having that decided in advance takes the ego out of a 3 a.m. decision.
5. The migration isn't done at cutover
The first few weeks after cutover are where you find everything the test plan didn't cover: jobs that only run weekly, behaviour that only shows up under real market-open traffic, alerts that were wired to the old infrastructure and can never fire.
What worked for us was a daily scan of the last 24 hours of errors across every service, triaged into "fix now," "ticket," or "noise." Most of the real post-migration bugs I fixed came out of that scan, not out of user reports.
6. Make the boring things time-proof
A few small habits that paid off far more than their size:
- Every outbound HTTP call gets an explicit timeout. Library defaults are not a decision.
- Every retry loop alerts on its last attempt, not just on errors.
- Scheduled jobs run in the business's time zone (
America/Toronto), not UTC, so daylight saving doesn't move a market-open job by an hour. - Every consumer that buffers a whole file sets a prefetch limit.
What I'd tell a team starting one
A cloud migration is mostly an inventory problem, a configuration problem and a communication problem, with a bit of infrastructure on top. Get the inventory right, remove cloud-specific glue early, make drift impossible instead of rare, and treat the cutover as a rehearsed release with a rollback line you've agreed on in advance.
The Kubernetes part is the easy bit.