This post is for cloud engineers building a GCP foundation with Terraform who have reached the point where the same networking or logging configuration keeps reappearing and someone suggests making it a module. My argument is about where to draw the module boundaries. Split modules by who is allowed to change the infrastructure and how often it changes. Keep each module narrow, and version it like a library. Getting the boundaries right early costs far less than refactoring a foundation that everything else sits on.
Why modules belong in a foundation
HashiCorp’s modules overview defines a module as a collection of resources that Terraform manages together. It recommends writing reusable modules whenever you repeatedly provision collections of resources with similar configuration. The Well-Architected guidance on reusable modules lists the situations where they pay off:
- you deploy the same group of resources multiple times with minor variations
- you need to enforce organizational standards and security policies
- multiple teams provision similar infrastructure
- you need consistency across development, staging and production
A foundation usually matches most of that list. That makes modules the default. The real question is how to cut them.
Draw boundaries by privilege first, volatility second
HashiCorp’s recommended pattern for module creation asks you to weigh three things: encapsulation (group infrastructure that is always deployed together), privileges (keep a module inside one privilege boundary), and volatility (separate long-lived infrastructure from short-lived). It warns that a module covering infrastructure owned by more than one group can quietly violate segregation of duties. It also warns that managing stateful, slow-changing infrastructure alongside things deployed many times a day exposes the stable part to unnecessary churn and risk.
For a foundation, I’d put privilege first. Your network layer is the clearest case. The same guide uses a network module as its example of high privilege and low volatility, and it says only people with permission to change network resources should use it. I’d treat org-wide policy, identity and logging the same way: each gets its own module, owned by the small group that is allowed to change it. The infrastructure application teams deploy on top of that belongs in separate modules that change at their own pace.
What I would not build is a single “foundation” module that does everything. The guide’s test is a good one. If you struggle to explain a module’s purpose, it is probably too complex.
Narrow inputs, wide outputs
The same guide gives concrete rules for a first version. Aim for a module that works for at least 80% of use cases. Do not code for edge cases. Avoid conditional expressions. Expose only the arguments people most often change. Then it flips the rule for outputs: output as much as possible, even data you have no current use for, and document those outputs in the README.
That asymmetry suits foundations well. Downstream modules and workspaces consume what the foundation creates, so generous outputs save every consumer from looking up identifiers some other way. Tight inputs keep the foundation opinionated, which is the point of having one. The guide also suggests starting with an early-adopter team and gathering its requirements. I’d do that with whichever application team is next to land on the platform.
Wrap, version, pin and test
The public registry holds modules maintained by HashiCorp, its partners and the community, according to the modules overview. The Well-Architected page describes module wrapping: you write your own module that calls an existing one and adds organization-specific configuration on top. I’d wrap rather than call a registry module directly from every root configuration. Then your defaults live in one place, and swapping the underlying module later touches one wrapper instead of every workspace.
On versioning, the same page recommends semantic versioning, tagging releases in version control, and pinning module versions in production. Pinning is what lets a foundation team ship a breaking change without breaking everyone at once. Consumers move when they choose to.
You do not need a private registry to start. Terraform can load modules from the local file system, a registry, or VCS repositories. HCP Terraform and Terraform Enterprise add a private registry when you want one. Before you publish, use Terraform’s built-in testing framework. It runs against short-lived infrastructure created for the test, which protects your real state. Also keep terraform.tfstate, the .terraform directory and *.tfvars files out of what you distribute. The local module tutorial notes these often contain secrets.
If your foundation began as a single configuration, you are not stuck with it. HashiCorp’s modules tutorial series covers using the moved block to refactor existing resources into modules.
When not to modularize yet
The local module tutorial recommends writing every configuration on the assumption that it may become a module. I agree with that as a design habit: clear variables, clear outputs, no hardcoded values. Publishing and versioning are a different matter. If one team owns a piece of infrastructure and it exists exactly once, a versioned module adds release overhead and buys little reuse. Keep it as a well-structured directory, and promote it to a published module when a second consumer actually shows up.
For your foundation, start by listing who owns each layer and how often it changes. Draw one module per owner-and-pace combination, keep its inputs minimal and its outputs generous, then tag and pin a first version before any application team depends on it.
