What I Learned Deploying 8 Services Across GCP, AWS, Azure, and Cloudflare
When I started building Upblit and Codily, I thought the hard part would be writing the code.
I was wrong.
The code was only half the challenge. The other half was figuring out how to make multiple services communicate reliably across different cloud providers while keeping deployments reproducible, secrets secure, and monthly costs under control.
As a solo developer, I ended up managing deployments across Google Cloud Platform (GCP), Amazon Web Services (AWS), Microsoft Azure, and Cloudflare, with services spread across multiple environments and regions.
This post isn't a guide to the perfect architecture. It's a reflection on what I learned by building, breaking, and rebuilding these systems.
The Architecture Grew Faster Than Expected
Initially, I had a single backend.
A few months later, the architecture looked something like this:
Cloudflare
│
▼
Public Endpoints
│
┌──────────────┴──────────────┐
▼ ▼
Upblit Codily
Frontend Frontend
Backend API
Pinger Worker
Ingestor
Eight independently deployed services meant eight deployment pipelines, multiple environments, separate configurations, and a growing list of operational concerns.
Networking Between Clouds Is Never "Free"
One of the biggest surprises was that deploying services in different clouds is technically easy—but operating them together is not.
Services need to communicate securely and predictably:
AWS Service
│
▼
Public Internet
│
▼
GCP Backend
│
▼
Azure Worker
Even when connectivity works, latency, DNS propagation, firewall rules, TLS certificates, and regional outages can complicate seemingly straightforward requests.
I quickly learned to avoid assuming that "reachable" means "reliable."
Every Network Hop Adds Complexity
Cross-provider communication introduces additional points of failure.
For example:
- transient DNS resolution issues,
- provider-specific firewall defaults,
- timeout mismatches,
- egress restrictions,
- certificate renewal problems,
- and inconsistent IPv4/IPv6 behavior.
The more services communicate over public networks, the more important retries, health checks, and observability become.
Secrets Become Harder Than Configuration
Environment variables are convenient when you have one service.
With eight services across multiple providers, secrets management becomes an operational challenge.
Each deployment may require:
- database credentials,
- API tokens,
- OAuth client secrets,
- webhook signing keys,
- encryption keys,
- and third-party integration credentials.
Managing them manually across environments quickly becomes error-prone.
Keeping Environments Consistent
Development, staging, and production rarely stay perfectly synchronized.
At one point I realized that fixing a configuration issue in one environment didn't automatically fix it elsewhere.
I started treating infrastructure configuration with the same discipline as application code:
- explicit environment variables,
- documented defaults,
- reproducible deployment scripts,
- and minimal manual intervention.
Reducing configuration drift saved far more time than I expected.
Costs Matter More Than CPU
Early in my journey I focused on technical elegance.
Eventually I started focusing on invoices.
A service consuming almost no CPU can still generate costs through:
- always-on virtual machines,
- managed databases,
- storage,
- outbound bandwidth,
- monitoring,
- load balancers,
- and idle resources.
For solo developers and early-stage products, operational simplicity often has a larger impact than shaving a few milliseconds off request latency.
Managed Services Are Convenient—but Add Up
It's tempting to adopt managed offerings for every component.
However, each managed service introduces another recurring cost and another piece of operational surface area.
I found myself regularly asking:
"Does this component genuinely need to be managed, or am I paying for convenience I don't currently require?"
That question helped prioritize spending where it created meaningful value.
Logging Is Worth Investing In Early
When services span providers, debugging by SSH quickly becomes unsustainable.
Without centralized logs, investigating failures often turns into piecing together partial information from multiple dashboards and terminals.
Consistent structured logging, request identifiers, and health endpoints dramatically reduced troubleshooting time and made incidents much easier to reason about.
Deployment Automation Pays Off
Manual deployments work until they don't.
With several independently evolving services, repeating deployment steps by hand eventually led to mistakes:
- forgotten environment variables,
- inconsistent container versions,
- stale configurations,
- and accidental rollbacks.
Automating builds and deployments reduced these risks while making releases less stressful.
Failures Should Be Expected
Distributed systems fail in surprising ways.
A provider API may experience elevated latency.
A worker process may restart unexpectedly.
A background service may temporarily lose connectivity.
Rather than assuming perfect availability, I began designing components with retries, health checks, graceful degradation, and restart tolerance as default behaviors.
This mindset changed how I approached reliability.
Monitoring Is More Valuable Than More Features
One lesson I repeatedly relearned is that observability often delivers more value than adding another feature.
Metrics such as:
- request latency,
- queue depth,
- worker health,
- deployment status,
- and error rates
help identify issues before they become user-facing problems.
A small investment in monitoring often prevents hours of debugging later.
Simplicity Wins
Some of my earliest designs were more complicated than necessary.
Over time I found myself removing abstractions instead of adding them.
A straightforward architecture with clearly defined responsibilities is easier to deploy, understand, document, and recover during incidents than an overly sophisticated one.
As a solo developer, maintainability became more important than theoretical elegance.
What I Would Do Differently
Looking back, there are several things I would prioritize from day one:
- Standardize configuration management earlier.
- Invest in deployment automation before scaling the number of services.
- Centralize logs and metrics from the beginning.
- Keep cross-cloud communication paths as simple as possible.
- Introduce infrastructure abstractions only when they solve real operational problems.
- Design for retries and recovery rather than assuming perfect execution.
Most importantly, I would spend more time reducing operational complexity instead of prematurely optimizing architecture.
Advice for Other Solo Builders
If you're deploying multiple services across cloud providers by yourself, my biggest recommendation is this:
Optimize for operability, not novelty.
A reliable deployment pipeline, understandable logging, disciplined secrets management, and predictable infrastructure will contribute more to long-term success than an impressive architecture diagram.
You don't need dozens of technologies to build a resilient platform. You need a system that you can confidently understand, debug, and maintain when something inevitably goes wrong.
Final Thoughts
Deploying Upblit and Codily across GCP, AWS, Azure, and Cloudflare taught me that software engineering extends well beyond writing application code. Networking, configuration, security, monitoring, and cost management all become first-class engineering problems as systems grow.
As a solo developer, I learned that the most valuable architectural decisions are often the ones that reduce operational burden. Building reliable software isn't just about choosing the right cloud provider or framework—it's about creating systems that remain understandable, recoverable, and sustainable long after the initial deployment succeeds.