Case study · Cloud infrastructure

Taming a multi-cloud Terraform estate: modules, state, secrets and a paved road

Years of acquisitions and team autonomy had left infrastructure across three clouds, described by hundreds of Terraform folders with copied modules, giant state files and secrets in plaintext. Changes were slow and frightening. The goal was an estate that teams could change quickly and safely, without a central bottleneck.

Case studyTechnology · Multi-cloud enterpriseSumanta Mukhopadhyay9 min read
#cloud#terraform#platform#security
CASE STUDY · CASE STUDY · CLOUD INFRASTRUCTURE28 Oct 2026 · 18:30 GST
At a glance
SectorTechnology · Multi-cloud enterprise
My roleFractional CTO and cloud architect
EngagementAssessment, refactor and enablement
ScopeModule library, state layout, secrets, policy, pipelines, tooling choice
What changed
  • A small library of composed, versioned modules per cloud
  • State split by blast radius, with locking and restricted access
  • Secrets moved out of state using ephemeral values and write-only arguments
  • Every change planned, policy-checked and applied through pipelines
Terraform or OpenTofuAWSAzureGCPModule registryRemote state with lockingPolicy as codeCI pipelines

Anonymised composite. Client names, identifying details and confidential figures are withheld; public facts are cited in the references.

The situation

A company had grown partly through acquisitions, and each acquired team had brought its own cloud and its own way of using Terraform. The result was infrastructure across AWS, Azure and GCP, described by hundreds of folders. Modules had been copied rather than shared, so fixes landed in one place and not the others. Some state files described entire environments, so a careless plan could touch hundreds of resources. Database passwords and keys sat in state, readable by anyone with access to the bucket.

Engineers had learned to avoid infrastructure changes. That was the real cost: features waited on a handful of people brave enough to run apply.

What had to be true

  • Small, composable modules per cloud, versioned and shared, rather than copies.
  • Small blast radius: no single plan able to damage unrelated systems.
  • No plaintext secrets in state, and narrow access to state that remains.
  • Every change through a pipeline, with plan review and policy checks; no laptop applies to production.
  • Teams self-serve common infrastructure through a paved road, with the platform team owning the road, not every change.

Architecture

Consumers
Product teamsservice stacksData teamspipelines and storesPlatform teamfoundations
Paved road
Service templatescompose modulesModule registryversioned releasesDocs and examplesper pattern
Modules
AWS building blocksnetwork, compute, dataAzure building blocksnetwork, compute, dataGCP building blocksnetwork, compute, dataShared interfacesnaming, tags, outputs
Delivery
CI plan and policyevery changeApproved applypipeline identity onlyDrift detectionscheduled plans
State and secrets
State per blast radiuslocked, encryptedSecrets managerephemeral readsAccess reviewsstate readers
Teams compose modules through templates; pipelines are the only path to production; state is small and guarded.

The data flow, end to end

  1. ChangeTeam edits a stack built from a template, pinned to module versions
  2. PlanCI runs a plan against that stack's own state only
  3. CheckPolicy checks: tags, encryption, public exposure, secrets in arguments
  4. ReviewPlan output reviewed in the pull request by the owning team
  5. ApplyPipeline identity applies after approval; humans cannot apply to production
  6. WatchScheduled plans detect drift and open issues for the owners
The same path for every change, so reviewers know exactly what they are approving.

The hard part: resisting the universal module

The first instinct was a set of cloud-agnostic modules: one "database" module that worked on all three clouds. It would have hidden the features that made each managed service worth using and created a large abstraction nobody could debug. HashiCorp's guidance favours composition instead: a root module assembling small building blocks and passing dependencies between them as inputs and outputs1.

  • Building blocks were provider-specific, thin and opinionated about security defaults.
  • Shared interfaces covered what really is common: naming, tagging, ownership labels and output shapes.
  • Templates composed blocks into patterns teams actually use, such as "service with database and queue".
  • Each block had examples and tests, and was released with semantic versions.

Secrets were the other urgent fix. Terraform 1.10 introduced ephemeral values that are not persisted in plan or state, and 1.11 added write-only arguments on managed resources, so secrets can flow from a secrets manager into resources without landing in state23. Every secret that had ever been in state was rotated as part of the move. The checklist is in keeping secrets out of Terraform state.

Multi-cloud doesn't need multi-cloud modules. It needs consistent interfaces, good building blocks for each cloud, and one way to ship changes.

How it ran

Platform team
  1. Module library and registry
  2. Pipelines and policy
  3. Templates and docs
Product teams
  1. Migrate stacks to templates
  2. Own plans and reviews
  3. Self-serve new services
Security
  1. Policy rules
  2. State access model
  3. Secret rotation
The platform team builds the road; product teams drive on it; security sets the speed limits.
Phase 1Assess
  • Inventory stacks and state
  • Find secrets in state
  • Map ownership
Phase 2Foundations
  • Pipelines and policy
  • State per blast radius
  • First building blocks
Phase 3Migrate
  • Highest-risk stacks first
  • Secrets out of state
  • Templates for common patterns
Phase 4Hand over
  • Team self-service
  • Drift detection
  • Module release process
Safety controls first, then migration, then self-service.

Key decisions and trade-offs

DecisionAlternativeWhy
Provider-specific building blocksUniversal cross-cloud modulesKeeps each service's strengths and stays debuggable
State split by blast radius and ownerOne state per environmentLimits damage and lets access follow ownership
Pipeline-only appliesEngineers apply from laptopsConsistent, reviewed and auditable changes
Ephemeral values and write-only secretsMark secrets as sensitiveSensitive hides values from output; it doesn't keep them out of state
Modules compatible with Terraform and OpenTofuCommit to one tool nowKeeps the licensing decision open without blocking work

The tooling question came up early. OpenTofu was forked from Terraform under the Linux Foundation after HashiCorp's 2023 licence change4. Rather than let that decision block the refactor, modules were kept compatible with both while legal and procurement reached a view; the trade-offs are in OpenTofu or Terraform in 2026.

What I'd do again, and differently

Again: put pipelines, policy and state boundaries in place before migrating anything, and rotate every secret that touched state. Differently: publish the module interface conventions (names, tags and outputs) in the first week. Teams started writing new stacks immediately, and every one written before the conventions existed needed rework. More on structure in Terraform module design for multi-cloud.

If your infrastructure code has become something people avoid touching, my fractional CTO work covers estates like this, or analyse your challenge and bring the result to a call.

References
  1. 1
    Module compositionHashiCorp developer documentation
  2. 2
  3. 3
  4. 4
FAQ

Common questions

Should one Terraform module work on every cloud?

Usually not. Terraform deliberately doesn't abstract over providers, and a lowest-common-denominator module hides the features that make each service worth using. Compose small provider-specific modules behind a shared interface instead.

How should Terraform state be split?

By blast radius and ownership: separate states for foundations, shared services and each application environment, so a mistake in one cannot destroy another, and access can follow ownership.

Should we move from Terraform to OpenTofu?

Both remain viable. OpenTofu is an MPL-licensed fork under the Linux Foundation, created after HashiCorp moved Terraform to the Business Source License in 2023. Decide on licensing, ecosystem and features, and keep modules compatible while you decide.

Keep reading

Related

Open to advisory, fractional CTO & select builds

Let's talk about what you're building.

The fastest route is a free 30-minute call. Prefer to write? Send a message and I'll reply personally within two business days.

Start a conversation

How would you like to reach me?

Book a 30-min callFastest. Pick any open slot on my calendar.→ Write in GmailOpens a pre-filled draft in your browser.→ Use my email appOutlook, Apple Mail or your default client.→

I reply personally within two business days. Time zones from the Americas and Europe to the Middle East and Asia-Pacific.