Every client who asks for multi-cloud infrastructure starts with the same justification: they don't want to be dependent on one cloud provider. It's a reasonable concern. What follows is usually a conversation about what multi-cloud actually means, because the term covers two very different things.
Active multi-cloud vs. cloud portability
Active multi-cloud means you run workloads simultaneously on AWS and GCP, traffic is live on both, and you can shift load between them. This is genuinely hard. You need abstraction layers at compute, storage, networking, and identity. The operational overhead is substantial and the engineering investment is real. Most companies don't actually need this. Almost none build it.
Cloud portability means you design and provision infrastructure such that migrating to a different provider is weeks of work, not eighteen months. This is achievable. It doesn't require running on two clouds simultaneously. The Terraform setup we use is built for portability, not active multi-cloud.
The module structure
One Terraform module per provider, one provider per environment. Never mix providers inside a single module — the plan output becomes unreadable, state dependencies get tangled, and the mental overhead of tracking which resource belongs to which provider in a 400-line plan is miserable.
infra/
modules/
compute/ # provider-agnostic interface
database/
networking/
storage/
providers/
aws/ # AWS-specific implementations
gcp/
hetzner/
environments/
production/
staging/The key discipline: your application code shouldn't know which provider it's running on. Abstract compute, object storage, and managed databases behind names your application understands. The application uses `APP_BUCKET` and `DB_HOST`. Whether those point to an S3 bucket or a GCS bucket, an Aurora Postgres or a Cloud SQL instance — that's the Terraform module's concern, not the application's.
This means the module interface has to be stable. When you define a `compute` module, its inputs and outputs stay consistent across providers. The internal implementation differs. The calling code doesn't.
Where OVH and Hetzner fit in
For Indian startups with European or US customers, OVH and Hetzner are real production alternatives to AWS in a way that wasn't true five years ago. Hetzner's cloud compute costs roughly ₹800-1,200 per month for a 4-core/8GB instance. The AWS equivalent (t3.xlarge in eu-central-1) is ₹3,500-4,000. That's a 3-4x difference on the same specs.
OVH has managed Kubernetes and managed databases that are production-grade. Hetzner's managed Kubernetes is stable and the load balancer offering is reasonable. Neither has the breadth of services AWS has, but for compute-heavy workloads — batch jobs, ML training, CI runners, staging environments — the economics are hard to argue with.
The setup we use for cost-sensitive clients: AWS or GCP for primary production (the latency-sensitive, customer-facing tier), Hetzner for development, staging, and non-production workloads. The Terraform provider for Hetzner is maintained and stable. Switching staging from AWS to Hetzner cut the infra bill for one client's non-production environments by about 65%.
The three decisions that make portability real
Terraform is provisioning. Portability lives in the workloads Terraform provisions.
- Use managed services with data portability as a selection criterion. Postgres over Aurora-specific features. S3-compatible object storage APIs (MinIO, Backblaze B2, Cloudflare R2) over native S3 if you want to move off AWS. The moment you use Aurora Serverless v2's auto-pause or DynamoDB's change streams, you're building on provider-specific primitives.
- Build your CI/CD pipeline around Docker images, not provider-specific deployment mechanisms. CodeDeploy hooks you to AWS. GitHub Actions with a standard Docker deploy step works anywhere with a container runtime.
- Keep secrets in a provider-agnostic vault. HashiCorp Vault or 1Password Secrets Automation instead of AWS Secrets Manager. The migration from Secrets Manager to anything else is a tedious, manual process that often gets skipped, and then it's the reason your AWS dependency runs deeper than anyone thought.
The honest part
Multi-cloud adds complexity. State management across providers, different IAM models that don't map cleanly to each other, networking primitives that work differently — it's real overhead and you shouldn't pretend it isn't.
For a team of three engineers shipping product, that complexity cost is meaningful. The right time to invest in a multi-cloud or cloud-portable architecture is when you have a specific reason: a customer contract requiring it, a genuine cost optimization target, a regulatory data-residency requirement, or a provider SLA that doesn't meet your uptime needs.
If you're doing it because it seems like the responsible thing to do, you'll spend engineering time building infrastructure abstractions instead of building product. That's a trade-off that sometimes makes sense and often doesn't.