Rebuilding the Ship at Sea: How We Migrated Our Production Cloud from GCP to Microsoft Azure in 10 Days

This tool is a generic calculator and does not advertise or promote a specific financial product. It is not intended to be relied on for the purposes of making a decision in relation to a financial product. You should consider obtaining advice from a financial services licensee before making any financial decisions.

In ancient maritime lore, philosophers debated the paradox of the Ship of Theseus: if a wooden vessel embarks on an endless ocean voyage and every decaying plank, spar, and rib is replaced one by one while underway, is it still the same ship when it finally makes port?
To an academic in an armchair, it is an intellectual riddle. To a master shipwright standing on a pitching deck mid-Pacific, it is simply the relentless reality of survival.
You cannot haul a working trading galley into dry dock when you are a thousand nautical miles from shore. If an oak plank springs a leak or rot softens a timber, the carpenter must unseat the old wood, shape the new timber, caulk the seam with oakum and pitch, and bolt it into place—all while the hull flexes under heavy swell, without letting a single gallon of seawater breach the hold or slowing the voyage by half a knot.
When you manage production cloud infrastructure as an independent builder, a cloud migration feels exactly like rebuilding a ship at sea.
You don't have a five-person platform engineering team. You don't have an SRE pod monitoring Grafana dashboards at 3 AM. And you certainly cannot hang a "Closed for Maintenance" banner across your website while real buyers are evaluating properties and mobile apps are pinging live APIs. You have to rebuild the entire hull—compute, object storage, secrets, edge CDN, and DNS routing—underneath a running application, plank by plank, without dropping a single packet.
A few weeks ago, we completed the migration of OptiWealth AU's entire production footprint from Google Cloud Platform (GCP) to Microsoft Azure. We moved compute, secrets, object storage, continuous deployment, and telemetry across in under 10 working days—with zero seconds of unplanned downtime.
Here is the unvarnished story of why we did it, how we approached the transition, the five unexpected traps that almost broke production along the way, and what the final architecture looks like today.
1. Why We Moved: The Economics of Survival & Enterprise AI
Every major cloud provider loves to tell you that architecture should be governed purely by technical elegance. In the real world of bootstrapped product engineering, architecture is governed by survival and runway.
For months, OptiWealth ran smoothly on Google Cloud:
- GCP Cloud Run for serverless container workloads.
- Google Cloud Storage (GCS) for property caches and ephemeral SRE telemetry.
- Google Cloud Secret Manager for credential rotation.
- Cloud Armor & Cloud CDN for edge filtering and asset acceleration.
The system was fast and reliable. But as our analytical models expanded—running deterministic hedonic pricing, spatial planning overlay audits, and macroeconomic yield calibrations—our monthly cloud bills began ticking upwards. Every dollar spent on idle cloud compute or egress was a dollar drawn straight from our personal runway.
Then we were accepted into the Microsoft for Startups Founders Hub, unlocking an initial $5,000 USD grant scaling up to $150,000 USD in Azure sponsorship credits, alongside direct access to Azure OpenAI.
For an early-stage startup, cloud credits aren't abstract monopoly money. They are lifeblood.
[ The Economic Reality ] GCP Production Baseline ───► ~$320 - $550 AUD / month (Direct credit card cash burn) Azure Founders Hub Credit ───► $0 AUD / month (100% sponsored runway for up to 3 years)
By moving to Azure:
- We drove direct cloud cash burn to $0 AUD. That single move extended our operating runway by years without diluting equity or rushing product decisions.
- We secured local data sovereignty in Sydney (
australiaeast). Both our container compute and enterprise LLM inference (Azure OpenAI) now reside in the same physical geography as our Australian user base, cutting cross-Pacific round-trip network hops. - We enforced tri-cloud portability. Rather than replacing one proprietary lock-in with another, we treated this migration as a forcing function to decouple our application completely from any single vendor's SDKs.
2. The Approach: How to Move a Live Platform in 10 Days
The biggest trap for a solo developer is the "Big Bang" migration—believing you can rewrite your entire deployment over a long weekend and flip a single DNS switch on Sunday night. That is the quickest path to a 48-hour production outage.
Instead, we treated the migration like carving a joint in wood: measure twice, cut incrementally, and maintain backward compatibility at every layer.
We executed the migration across five distinct, sequenced phases over 10 working days:
flowchart LR P1["Phase 1 (Days 1-2)<br/>Terraform Foundation<br/>in australiaeast"] P2["Phase 2 (Day 3)<br/>In-Memory Secret<br/>Lift-and-Shift"] P3["Phase 3 (Days 4-5)<br/>Polymorphic Storage<br/>Abstraction"] P4["Phase 4 (Days 6-8)<br/>Container Apps<br/>Staging & SRE Probes"] P5["Phase 5 (Days 9-10)<br/>Blue/Green Cutover<br/>& GCP Trigger Retirement"] P1 --> P2 --> P3 --> P4 --> P5
Phase 1: Terraform Primitives in Sydney (Days 1–2)
Before writing a single line of application code, we codified the entire target environment in Terraform. We provisioned:
- Resource Group:
rg-optiwealth-staging-australiaeast(and production equivalents). - Azure Container Registry (ACR):
optiwealthacr.azurecr.iofor nativelinux/amd64Docker images. - Azure Key Vault:
kv-optiwealthwith Managed Identity access policies and soft-delete protection. - Storage Account:
stoptiwealthprodwith partitioned blob containers (static-assets,property-cache,sre-ephemeral, andsre-patterns). - Container Apps Environment (CAE): Serverless consumption workload profile paired with a dedicated Log Analytics Workspace (
law-optiwealth-prod).
Phase 2: In-Memory Secret Lift-and-Shift (Day 3)
We had 16 active production secrets in Google Cloud Secret Manager (Gemini keys, Supabase credentials, Slack webhooks, distance matrices).
Rather than manually copying and pasting sensitive credentials into the Azure portal—or worse, writing them to a temporary .env file on disk—we wrote an automated in-memory Node.js migration engine.
The script streamed values directly from GCP Secret Manager into Azure Key Vault strictly in-memory:
// In-Memory Secret Migration Engine (Excerpt) for (const secret of SECRETS_TO_MIGRATE) { // 1. Fetch live secret value directly from GCP Secret Manager via CLI stream const secretValue = execSync( `gcloud secrets versions access latest --secret="${secret}" --project="${GCP_PROJECT}"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] } ).trim(); // 2. Format name to comply with Azure Key Vault naming constraints const azureName = secret.replace(/_/g, '-').toLowerCase(); // 3. Inject into Azure Key Vault with zero disk persistence execSync( `az keyvault secret set --vault-name "${AZURE_KEY_VAULT}" --name "${azureName}" --value "${secretValue}" --tags env_var="${secret}"`, { stdio: 'ignore' } ); }
All 16 production secrets were migrated in under 90 seconds without a single byte of sensitive data touching local storage.
Phase 3: The Polymorphic Storage Abstraction (Days 4–5)
Our Next.js backend relies heavily on object storage for caching institutional property audits (with 35-day TTLs) and ephemeral SRE diagnostic dumps.
Instead of cluttering our business logic with @azure/storage-blob imports, we carved a clean abstraction layer:
┌────────────────────────┐ │ IStorageAdapter │ │ (Unified Contract) │ └───────────┬────────────┘ │ ┌───────────────────┼───────────────────┐ ▼ ▼ ▼ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ Azure Blob │ │ Google GCS │ │ Local Disk │ │ Adapter │ │ Adapter │ │ Adapter │ └──────────────┘ └──────────────┘ └──────────────┘
By introducing a polymorphic IStorageAdapter interface, switching from GCS to Azure Blob Storage required changing only one environment variable: STORAGE_PROVIDER=azure. The entire property risk engine didn't need to know or care which hyperscaler was holding the bytes.
Phase 4: Staging Deployment & Synthetic Probes (Days 6–8)
We built our Next.js standalone container via Azure Container Registry Cloud Build (az acr build) and deployed to Azure Container Apps (optiwealth-app-staging).
Before touching production DNS, we ran synthetic HTTP probes across our entire route surface:
GET /-> HTTP 200 OK (Full SSR landing page rendered in 113KB).GET /blog/-> HTTP 200 OK (Static site generation archive).GET /api/ad-config/-> HTTP 200 OK (Dynamic SRE telemetry returned in < 180ms).- Hedonic calculation test suites: 76/76 unit and integration tests passing.
Phase 5: Zero-Downtime Traffic Shift & GCP Retirement (Days 9–10)
With staging verified, we configured Azure Front Door with managed TLS certificates for optiwealth.au.
We implemented a blue/green rolling revision strategy on Azure Container Apps: the new revision was spun up, validated with health probes, allocated 100% of incoming traffic, and the previous revision was demoted to standby (0%).
Only after live traffic confirmed zero 5xx errors did we decommission the legacy GCP Cloud Run services.
3. Problems and Limitations: The 5 Hidden Traps & The Domain Incident
No cloud migration is ever as clean as the keynote slides promise. As a solo builder, the hardest part isn't the code you write; it's the invisible friction between how two different cloud providers think.
We hit five distinct roadblocks—including a terrifying domain routing incident that temporarily knocked out our production endpoints:
Trap 1: The Azure Key Vault Underscore Ban
In Unix and POSIX environments, environment variables use screaming snake case with underscores: NEXT_PUBLIC_SUPABASE_URL, OPENAI_API_KEY. GCP Secret Manager handles these natively.
Azure Key Vault, however, strictly prohibits underscores (_). Secret names can only contain alphanumeric characters and hyphens (-).
If you naively try to upload your existing environment keys, Azure rejects them with HTTP 400. If you rename them manually, your application runtime fails because Node.js looks for process.env.OPENAI_API_KEY, not process.env.OPENAI-API-KEY.
- The Fix: We built our lift-and-shift engine to automatically convert underscores to hyphens (
openai-api-key) when writing to Key Vault, but stamped each secret with anenv_var="OPENAI_API_KEY"metadata tag. When Azure Container Apps mounts these secrets into the container, it binds them back to standard POSIX environment variable names at process boot.
Trap 2: The optiwealth.au Domain Incident & The 30-Second Edge Timeout
Setting up our custom domain (optiwealth.au) on Azure Front Door triggered our most harrowing 3 AM production debugging session. It broke in two distinct ways:
A. The GoDaddy HTTPS Port 443 Black Hole:
Our registrar (GoDaddy) had apex forwarding (A @ -> 15.197.142.173, 3.33.152.147) set up to forward HTTP traffic from the root optiwealth.au to https://www.optiwealth.au.
The fatal catch? GoDaddy's forwarding servers only listen on HTTP port 80. They do not accept incoming HTTPS connections on port 443. Any visitor or mobile app connecting directly to https://optiwealth.au hung indefinitely until timing out. Compounding the chaos, local macOS DNS caches (dscacheutil) cached the stale forwarding IPs for 60 minutes, making it look as though our entire cloud had vanished.
- The Fix: We bypassed third-party forwarding entirely. We provisioned both
optiwealth.au(apex) andwww.optiwealth.auas first-class custom domains directly inside Azure Front Door with automated DigiCert TLS 1.2+ certificates and_dnsauthTXT validation tokens.
B. The 30-Second Edge Timeout & The Flutter HTML Crash:
Azure Front Door defaults to an origin response timeout of 30 seconds.
Our multi-pass property risk audits—which cross-reference statutory planning overlays, cadastral boundaries, and live market sales—take 35 to 45 seconds on cold, un-cached addresses. Exactly at second 30.1, the Front Door Melbourne edge POP (C1MEL) abruptly severed the connection and returned a generic HTML 504 Gateway Timeout error page (<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0...).
Our Flutter mobile client was expecting JSON. When it tried to parse the raw HTML document, the app crashed with an unhandled exception:
AI AUDIT ERROR: FormatException: Unexpected character (at character 1) <!DOCTYPE html PUBLIC...
- The Fix: We increased the Azure Front Door origin response timeout to 180 seconds (
originResponseTimeoutSeconds: 180), waited 20 minutes for global edge POP propagation, and wrapped the Flutter client in defensive error handling so HTML gateway responses are caught safely without crashing the UI.
Trap 3: HTTP/2 Stream Framing & Compression Mismatch
Shortly after DNS cutover, web assets occasionally threw cryptic ERR_HTTP2_PROTOCOL_ERROR glitches in Chrome.
Next.js inside our standalone container was dynamically compressing static chunks using Gzip/Brotli with chunked transfer encoding. When Azure Front Door attempted to reverse-proxy and re-compress these streams at the edge, it caused HTTP/2 framing mismatches.
- The Fix: We created an Azure Front Door Rule Set (
rsNextjsFix) strippingAccept-Encodingheaders before forwarding traffic to the Container Apps origin, disabled Next.js edge compression, and executed a global edge cache purge (/*).
Trap 4: The Ghost GCP Cloud Build Trigger
Two days after shifting our staging traffic to Azure, I pushed a hotfix to main.
To my shock, our Azure deployment didn't reflect the change immediately. When I checked our logs, I discovered that an old GCP Cloud Build trigger (OptiWealth in australia-southeast1) was still listening to pushes on ^main$.
Both pipelines were firing simultaneously: GitHub Actions was building and deploying to Azure Container Apps, while GCP Cloud Build was quietly spinning up legacy containers on GCP Cloud Run. We were in a dual-deployment collision.
- The Fix: We deleted the GCP trigger via
gcloud builds triggers delete OptiWealth --region=australia-southeast1, neutralized the legacy GCP build pipeline with an immediate exit guard, and codified the retirement into Terraform withdisabled = trueandbranch = "^legacy-disabled$"to prevent accidental resurrection.
Trap 5: The Silent Metadata Server Timeout
When our Next.js container booted on Azure Container Apps, our API routes initially suffered an inexplicable 5 to 10-second latency spike on cold requests.
The culprit was subtle: buried deep in our third-party telemetry and storage helper libraries were calls attempting to contact the Google Cloud Metadata Server (http://169.254.169.254) for Application Default Credentials (ADC).
On GCP, this call returns in 2ms. On Azure Container Apps, there is no Google metadata server. The HTTP client hung for its full 10-second connection timeout before throwing an unhandled exception and falling back.
- The Fix: We audited all credential resolution paths, guarded GCP-specific token fetches behind explicit environment checks (
if (process.env.STORAGE_PROVIDER === 'gcp')), and bound Azure Container Apps to a native System-Assigned Managed Identity (2d3ee2b7-415f-48ae-b2c9-4c4708761268) with direct Key Vaultgetandlistpolicies. Cold request latency immediately plummeted back under 200ms.
4. The Final Picture: Production Topology on Microsoft Azure
Today, OptiWealth AU runs on an entirely modern, serverless Microsoft Azure footprint located entirely within Australia.
Here is the production architecture:
flowchart TD subgraph ClientLayer["Client & Edge Ingress"] Client["Users (Web Browser & Flutter Mobile App)"] AFD["Azure Front Door (Global Anycast CDN)<br/>• Managed TLS for optiwealth.au & www<br/>• Edge WAF & 180s Origin Timeout"] Client -->|HTTPS / Port 443| AFD end subgraph ComputeLayer["Container Compute (australiaeast - Sydney)"] CAE["Azure Container Apps Environment (cae-optiwealth-prod)<br/>• Consumption Workload Profile<br/>• KEDA Auto-Scaling (1 to 3 Replicas)<br/>• Next.js 16 Standalone (Linux/amd64)"] AFD -->|Private Ingress / 8080| CAE end subgraph SecurityLayer["Identity & Secrets"] MI["Managed Identity (System-Assigned)"] AKV["Azure Key Vault (kv-optiwealth)<br/>• 16 Encrypted Production Secrets<br/>• Zero Plaintext on Disk"] CAE <-->|RBAC Get/List| MI MI <--> AKV end subgraph StorageLayer["Data & Cache Tier (stoptiwealthprod)"] SA_CDN["static-assets Container<br/>(Public CDN Assets)"] SA_CACHE["property-cache Container<br/>(35-Day Institutional AVM Cache)"] SA_SRE["sre-ephemeral Container<br/>(14-Day Auto-Prune Lifecycle Rule)"] CAE --> SA_CDN CAE --> SA_CACHE CAE --> SA_SRE end subgraph HybridAILayer["Hybrid Dual-Brain LLM Engine"] AZ_AI["Primary Analytical Engine: Azure OpenAI<br/>• GPT-6.1 Sol (Short Context)<br/>• $0 AUD Cash Burn via Startup Credits"] G_AI["Live Grounding Engine: Google Gemini API<br/>• Gemini 3.8 Flash + Native Google Search Grounding<br/>• Real-time Australian Listings & Auction Tape"] OAI["Secondary Fallback: OpenAI Direct<br/>• GPT-5.6 Sol Failover"] CAE -->|Math & Synthesis| AZ_AI CAE -->|Live Web Grounding| G_AI AZ_AI -.->|Outage Failover| OAI OAI -.->|Disaster Recovery| G_AI end subgraph ObservabilityLayer["Telemetry & SRE"] LAW["Log Analytics Workspace (law-optiwealth-prod)<br/>• OpenTelemetry Instrumentation<br/>• Automated SRE Error Circuit Breakers"] CAE --> LAW end
Why We Still Use Google Gemini: The Hybrid Dual-Brain Strategy
A common question other engineers ask is: "If you migrated all your compute and storage to Microsoft Azure, why didn't you completely shut down Google Cloud and drop the Gemini API?"
The answer comes down to our core engineering philosophy: use the best tool for the specific job, never let dogma override utility, and always maintain disaster recovery.
When we decommissioned our high-cost GCP infrastructure (load balancers, Cloud Run, Cloud Armor, and GCS buckets), we deliberately retained two Google services: Google Maps Platform and the Google Gemini API (generativelanguage.googleapis.com).
Here is why Gemini remains an indispensable pillar of our production stack:
1. Native Google Search Grounding for Live Australian Real Estate
Australian real estate is hyper-dynamic. Yesterday's auction clearance rates, newly gazetted heritage overlays in Melbourne's Boroondara council, or an unadvertised strata defect notice on a Gold Coast tower aren't captured in static LLM weights.
Azure OpenAI (GPT-6.1 Sol) is a formidable engine for deep financial reasoning, deterministic mathematical calculations, and structured JSON generation. But it does not possess Google's real-time search index.
By keeping Google Gemini in our architecture, we leverage native Google Search Grounding. When a user requests a property appraisal, Gemini queries Google Search in real-time, retrieving live auction clearance data, recent comparable street listings, and statutory notices without us needing to build, host, or maintain a fragile custom scraping pipeline.
2. The Hybrid Dual-Brain Workload Division
In our centralized model configuration, we codified an elegant division of labor:
┌──────────────────────────────────────┐ │ Incoming Intelligence Request │ └──────────────────┬───────────────────┘ │ Does the audit require live web search? │ ┌────────────────┴────────────────┐ ▼ ▼ [YES: useSearch] [NO: Analysis] │ │ ▼ ▼ ┌───────────────────┐ ┌───────────────────┐ │ Google Gemini │ │ Azure OpenAI │ │ (Gemini 3.8 Flash)│ │ (GPT-6.1 Sol) │ │ Google Grounding │ │ $0 Cash Burn via │ │ Live Market Truth │ │ Startup Credits │ └───────────────────┘ └───────────────────┘
- When
useSearch: true(Live Market Screening): We route first to Google Gemini (['gemini', 'azure', 'openai']) to anchor the audit in live, empirical market facts. - When
useSearch: false(Institutional Synthesis & Hedonic Modeling): We route directly to Azure OpenAI (['azure', 'openai', 'gemini']). This consumes our Microsoft for Startups credits, keeping our cash outflow at exactly $0 AUD while delivering lightning-fast inference.
3. Cross-Hyperscaler Fault Tolerance (Zero 3 AM Panic)
Every cloud provider experiences outages. Relying 100% on a single vendor for your AI inference is engineering negligence.
If Azure OpenAI experiences a regional outage or rate-limit spike in Sydney, our universal LLM orchestrator cascades automatically:
Because our Google Cloud project remains active with pre-warmed API quotas, we have guaranteed cross-hyperscaler disaster recovery. If Microsoft has a bad day, OptiWealth stays online. If OpenAI has a bad day, OptiWealth stays online. That peace of mind is worth every second of maintenance.
What Changed in Production: The Hard Numbers
We measure engineering success in concrete numbers, not vague impressions:
| Dimension | GCP (Before) | Microsoft Azure (After) | Impact |
|---|---|---|---|
| Monthly Infrastructure Cash Burn | ~550 AUD | $0 AUD | 100% offset by Microsoft for Startups sponsorship |
| API Cold Response Latency | 380ms – 650ms | 180ms – 240ms | Reduced network hops; local Sydney region collocation |
| Secret Management Storage | GCP Secret Manager (plain POSIX) | Azure Key Vault + Managed Identity | Zero long-lived service account JSON keys stored in repo |
| Storage Coupling | Hardcoded GCS SDK calls | Polymorphic IStorageAdapter | Runtime switchable across Azure, AWS, and GCP in < 60s |
| Mobile Research Failure Rate | ~6.4% on app backgrounding | 0.0% (HTTP 202 Job Ticket) | Complete resilience against mobile screen sleep & network drops |
| Unplanned Downtime During Cutover | — | 0 Seconds | Blue/green rolling traffic shift verified via Front Door |
Lessons from the Trenches: 3 Rules for Solo Builders
If you are contemplating a cloud migration or trying to keep your startup's infrastructure lean, here is what this journey taught us:
1. Abstractions Belong at the Boundaries, Not in Your Core Logic
You don't need a convoluted microservices framework to be cloud-agnostic. All you need is a clean TypeScript interface around external dependencies: object storage, secret retrieval, and database access.
Because we isolated our storage operations behind IStorageAdapter, switching from Google Cloud Storage to Azure Blob Storage required zero changes to our hedonic valuation models or property risk engines. Build walls at your edges, not in your living room.
2. Cloud Credits Are an Engineering Asset
Engineers often dismiss startup credit programs as "business school fluff". That is a mistake.
For a solo developer, securing $150,000 USD in cloud credits isn't just about saving money—it completely alters your engineering risk profile. It gives you the headroom to run institutional-grade KEDA auto-scaling, maintain dedicated staging replicas, and run comprehensive end-to-end integration test suites without constantly worrying about your personal bank balance. Treat cloud credits as high-octane engineering runway.
3. Respect the Unsexy Plumbing
It is tempting to spend all your time fine-tuning algorithmic weights and UI animations. But in production, users don't abandon your app because your button radius was 20px instead of 24px; they abandon it because their mobile socket dropped when they took a phone call, or because an unhandled metadata timeout added 10 seconds of blank screen.
The unsexy plumbing—HTTP 202 job queues, in-memory secret pipelines, resilient retry loops, and 14-day blob lifecycle rules—is what keeps a product alive when real users push it to its limits.
Final Thoughts
At the end of the day, migrating clouds isn't about loyalty to Google or Microsoft. Cloud providers are utilities, like electricity and water.
Like the shipwright replacing hull timbers mid-voyage, resilient engineering isn't about how many tools you can pile onto your workbench. It is about replacing fragile assumptions under full sail—eliminating cash burn, dismantling vendor lock-in, and securing your vessel—until only what is buoyant, resilient, and honest remains.
Have you navigated a cross-cloud migration or wrestled with serverless container cold starts in production? Let's discuss or reach out to our team at OptiWealth AU.
JeongSuk Lee
Transparent Data Science & EngineeringData Science, Software Architecture & Mathematical Risk Modeling Group
Authored by OptiWealth’s Quantitative & Data Engineering division. Our team specializes in full-stack software architecture, open-source data science, and spatial econometric property valuation (such as hedonic price regression, environmental zoning overlay synthesis, and cash-flow yield analytics). All underlying algorithms and calculation formulas are fully transparent, open to inspection, and designed purely for financial and property data visualization.
Expose Hidden Property Risks & Liabilities
OptiWealth replaces guessing games with algorithmic diagnostics. Use FairBid to audit council overlays, building defects, and calculate true net property yields.