Multi-cloud usually isn’t a strategy. It’s what’s left after years of acquisitions, customer requirements and “just spin one up in whatever account is handy.” By the time I arrived, infrastructure spanned GCP, AWS and Azure, and much of it had been built by hand in each provider’s console.
The symptoms were familiar:
- Drift. Staging and production were “the same” until they weren’t.
- Unreviewable change. A console click leaves no pull request and no history.
- Slow provisioning. Every new customer environment repeated the same manual steps, with the same chance of missing one.
Principle: if it’s not in code, it doesn’t exist
The rule we worked toward was strict: all infrastructure is defined in Terraform, and every change goes through a pull request. Console access is for looking, not changing.
Getting there was mostly unglamorous work: importing existing resources into state, writing modules that matched what was really running, and then gradually replacing hand-built pieces with module-built ones.
Module design
The core idea is write once, compose many times. Modules capture how we build a network, a cluster or a database. Environments are thin compositions of those modules.
A few design choices made this hold up over time:
- Pinned module versions. Environments reference a tagged version, so a module change never silently rolls into production.
- Separate state per environment, with remote state and locking, so a staging mistake can’t touch production.
- Provider-specific modules behind a consistent interface. A “cluster” means the same thing to the people using it, whether it’s GKE, EKS or AKS underneath.
Pipelines, not laptops
Infrastructure changes run through CI/CD, never from someone’s machine:
- Open a pull request and the pipeline runs
terraform plan. - The plan output is posted to the PR, so reviewers see the actual change, not just the code diff.
- A peer approves the change.
- Merging triggers
terraform applyfrom the pipeline, using pipeline-scoped cloud credentials.
That gives every change a history (who, what, when and why), a reviewer and a way to roll back.
Multi-tenant Kubernetes
On top of this foundation, Kubernetes was the common runtime for application workloads, and Helm was the packaging layer. Serving multiple customers on shared foundations means isolation has to be designed in:
- A namespace per tenant, with resource quotas and limit ranges so one customer can’t starve another.
- Network policies that deny cross-namespace traffic by default.
- Helm values per tenant, so customer-specific configuration lives in code next to everything else.
- One standard pattern for build, deploy and monitoring, so every service ships and is observed the same way, whichever cloud it runs on.
Consolidating the core
The same discipline extended to the systems underneath engineering. Identity and core business systems were consolidated onto platforms built for a much larger organization, so growth didn’t mean adding yet another identity source or admin console.
What I’d tell another team
- Import before you rebuild. Get what’s running into state first, then improve it.
- Pin your module versions. Unpinned modules are an outage you haven’t had yet.
- Show the plan in the pull request. Review the change, not just the code.
- Design tenant isolation in from day one. Retrofitting it is much harder.