I Migrated a Product from AWS to GCP — Here's What I'd Do Differently

Search for a command to run...

Love the practical examples. Are there any specific pitfalls or anti-patterns you'd warn beginners to avoid here?
S01E03 of FastAPI in Production

S01E02 of FastAPI in Production

S01E01 of FastAPI in Production

Day 7 of Building PulseCart: Event-Driven Architecture on GCP

Last year we moved a production platform off AWS and onto GCP. Full migration — compute, database, storage, CDN, CI/CD, the works. The product was live, clients were active, and we had no meaningful downtime budget.
It went well enough. But "well enough" means we made mistakes we didn't have to make. Here's what actually happened, in order, and what I'd do differently.
What we did: before touching GCP, we spent a week mapping every AWS service in use — ECS Fargate, RDS Postgres, S3, CloudFront, SES, Secrets Manager, Route 53 — and found the GCP equivalent for each.
This was the right call. The mapping exercise surfaced two services we'd forgotten were even running (a legacy SQS queue and a Lambda that ran once a week), and forced us to make explicit decisions about what to migrate vs. what to retire.
What I'd do the same: the audit. Non-negotiable. You will find things you forgot existed.
What I'd do differently: involve the whole team earlier. I did most of the audit myself, which meant others were context-free when implementation started. A shared doc with the mapping, built collaboratively, would have halved the onboarding time later.
What we did: we committed to 100% Infrastructure as Code from day one. No GCP Console clicks that weren't codified in Terraform. Every resource — Cloud Run services, Cloud SQL instance, Redis Memorystore, Pub/Sub topics, Cloud Tasks queues — was provisioned via Terraform with remote state in GCS.
This was the best decision we made. When we needed to spin up a staging environment mid-migration, it took 20 minutes, not two days.
What I'd do the same: everything here. IaC from day one, no exceptions.
What I'd do differently: set up the Terraform module structure earlier. We started with a flat main.tf and refactored into modules halfway through when it got unwieldy. Starting modular costs nothing and saves a painful refactor.
What we did: we deployed the application services to Cloud Run first, kept them pointed at the existing AWS RDS Postgres instance, and validated the full stack was working before touching the database.
In theory this was smart — decouple the compute migration from the data migration. In practice it meant our Cloud Run services in GCP were making cross-cloud calls to RDS in AWS for three weeks, adding ~40ms of latency on every database call. Clients noticed.
What I'd do differently: plan the database migration timeline before starting compute. Three weeks of cross-cloud latency was avoidable. A tighter migration window — compute and database moved within the same week — would have been uncomfortable but faster.
What we did: we used AWS DMS (Database Migration Service) to stream changes from RDS Postgres to Cloud SQL during a live migration window. The plan was to run both databases in sync, then do a cutover with a short maintenance window.
DMS had opinions about our schema we hadn't anticipated. Several constraints didn't replicate cleanly, and we spent two days debugging replication lag before the cutover. The actual maintenance window was 47 minutes — longer than the 15 we'd planned.
What I'd do differently: test the full DMS pipeline on a production-sized data copy at least two weeks before the actual cutover. We tested on a small subset, which didn't surface the schema issues. Test at scale, with real data volume.
Also: pg_dump / pg_restore for smaller databases (under ~50GB) is less exciting than DMS but dramatically less surprising. We overcomplicated it.
What we did: we had ~30 secrets in AWS Secrets Manager — API keys, DB credentials, service tokens. We manually recreated them in GCP Secret Manager. It took half a day and introduced two errors we caught in staging (wrong value copy-pasted, one secret missed entirely).
What I'd do differently: script it. Even a simple Python script that reads from AWS Secrets Manager and writes to GCP Secret Manager would have been faster, auditable, and error-free. Manual secret migration at any meaningful scale is asking for problems.
What we did: replaced GitLab CI pipelines pointing at ECS Fargate with GitHub Actions pipelines pointing at Cloud Run. The logic was near-identical — build Docker image, push to registry, deploy.
Switching to Workload Identity Federation instead of long-lived service account keys added a day of setup but was worth it. No credentials to rotate, no keys to accidentally commit.
What I'd do the same: Workload Identity Federation from the start. Not an afterthought.
Audit everything, including the things you think you know. You will find forgotten services.
IaC from day one, modular from day one. Refactoring Terraform mid-migration is pain you don't need.
Test the database migration at production scale, weeks early. Schema surprises at cutover time are expensive.
Script the boring parts. Secret migration, DNS cutover checklists, smoke test sequences — anything manual and repetitive should be a script.
Set a tighter timeline for cross-cloud transitional states. Three weeks of cross-cloud DB latency was a self-inflicted wound.
The migration took eight weeks end to end. With these changes, I think six was achievable — and the last two weeks would have been less stressful.