Table of Contents
I’ve been working on our Azure platform to keep up with a real jump in demand, most of it coming from AI-generated apps landing faster than governance could keep pace with. That pushed us to look hard at how we were approaching infrastructure deployments, and we decided it needed to change: simpler, more governed, more reliable, and reusable.
I’ll be sharing that journey as it happens. This is the first post in it, focuses on terraform repo structure, because it’s the foundation everything else in the platform gets built on.
Ask ten platform teams how they structure their Terraform repos and most will describe a folder layout. Environments folder, regions folder, a .tfvars file per stage. That’s a real decision, but it’s the last one, not the first.
Before you get anywhere near folders, you need to work out where three very different things live: the building blocks, the assembled solution built from those blocks, and the actual deployment a team ends up running. Mix those three together in one repo and you end up with something nobody can safely reuse, version, or hand off.
Here’s how I split it.
Module Repos: The Building Blocks
A module repo holds one thing: a single resource, or a small group of resources that always get deployed together and never make sense apart. A storage account with its private endpoint. A Container App Environment. A SQL Server and the database sat inside it.
These are versioned and tagged, and nobody outside the platform team edits them directly. Consumers don’t need to know how they work, only what inputs they take and what they return.
This is the level most Terraform advice stops at, and on its own it’s not enough. A pile of well-built modules doesn’t give a team anything to actually deploy. Something has to call them together.
The Golden Path: Where the Modules Become a Solution
This is the part that’s easy to gloss over, so I’ll be specific.
Say a team needs a container-based API with a database behind it, fronted by Front Door, with authentication enforced. That’s not one module, it’s several: a Container Instance, a SQL Database, a Front Door endpoint, and Easy Auth configuration wired onto the container. The golden path is the code that calls all four of those modules together and wires them up correctly, every time, the same way.
That’s the catalog item. Not “here are some modules, go compose them,” but “here is the solution, already composed, already correct.” The platform team defines it once. A team consuming it customises it by passing values through tfvars, region, sizing, naming, whatever variables the golden path exposes. They should never need to touch the calling code itself. If someone’s editing the module calls inside a golden path to get their deployment working, the golden path hasn’t done its job.
The golden path doesn’t change to fit a team. Teams fit into the golden path. If a team needs something the golden path doesn’t support, that’s a signal to go back to the platform team and extend the golden path itself, not to fork it.
Consumer Repos: Two Ways to Build One From a Golden Path
However you build the consumer repo, the rule stays the same: a team supplies tfvars, and that’s the only file they should ever touch. This is also the only place the environments-folder pattern I’ve written about before actually belongs, one repo, one deployment, environment-specific values, no platform logic living inside it.
Where the two approaches differ is what happens after the consumer repo exists, and how it catches up when the golden path improves.
Option 1: Golden path as a template repo
Set the golden path up as a GitHub template repo. A team clicks “use this template,” and that creates their consumer repo as a full copy, already wired up with the modules, the calling code, and the guardrails.
The consumer repo is a snapshot taken at the point of creation. When the golden path improves later, that change doesn’t reach back into repos that already exist. New consumers get the improved version, existing ones don’t until they’re rebuilt or the change is backported by hand.
Option 2: Golden path as a referenced module
A golden path doesn’t stop being a module just because it calls other modules internally. Terraform doesn’t distinguish a “module” from a “composition of modules,” it’s still just a module from the outside. So the golden path can be packaged and versioned the same way any module is, with its own tag.
The consumer repo becomes thin: a main.tf with a single module block pointing at a pinned version of the golden path, plus a .tfvars file. Upgrading means bumping that version tag and running plan, not rebuilding the repo.
Neither one is automatically right. The template repo gives every consumer a plain, self-contained repo with nothing hidden behind a version pin, which is easier for a team to read and reason about, at the cost of drifting from golden path improvements the moment it’s created. The referenced module keeps consumers thin and makes upgrading a one-line change, at the cost of every consumer needing to understand they’re running code they don’t see directly in their own repo.
And to be clear, neither option gives you free, automatic updates, nor would you want it to. A team’s infrastructure changing underneath them because the platform team quietly shipped a new golden path version is its own kind of incident. Both approaches still require someone to decide to take the update. The real difference is how much work that decision costs once they’ve made it: rebuild a repo, or bump a version tag.
Shared Workflow Repos
The same versioning problem exists one layer up, in the pipelines. If every consumer repo has its own copy of the plan/apply/drift-check YAML, you’ve got the same duplication problem as unshared Terraform code. A shared workflow repo holds the reusable GitHub Actions workflows or Azure DevOps templates, versioned the same way the modules are, so a change to how you run a plan doesn’t mean editing YAML in twenty places.
Why This Is the Golden Path, Not Just Good Practice
“Golden path” gets used loosely, so here’s what I mean by it: it’s not documentation telling a team the right way to do something. It’s the fact that the right way is the only way that’s been built. A team doesn’t read a wiki page and manually wire up Easy Auth, Front Door, and a database correctly. They consume the golden path, pass in a handful of variables, and it’s already right.
That’s what makes it different from governance sat in a pull request review. A reviewer can miss something, or get overruled under deadline pressure. A golden path that’s the only way to deploy that catalog item can’t be skipped, because there’s no other version to fall back on.

Where This Pays Off
Onboarding a new team stops being a platform-team task. They consume a golden path, supply their variables, and they’re deployed, without waiting on someone to hand-configure their setup.
Consistency stops depending on anyone remembering to check. You don’t find out six months later that one team’s deployment never had Easy Auth switched on, because there was never a version of that golden path without it.
None of this asks the consuming team to understand how the golden path works internally. That complexity sits with the platform team, in the module repos and the golden path itself. Everyone else gets the benefit without needing the knowledge.
Ownership
This only holds together if it’s clear who owns what. Module repos, golden paths, and shared workflow repos are platform-owned. Consumer repos are team-owned. I’ve gone into how I define that ownership split in [Post 2 of the Azure Platform Operations series], so I won’t repeat it here, but if that boundary is fuzzy, none of the repo structure above will save you.
Next Up
Once you’ve settled on module repos as the building blocks, the obvious next question is what actually belongs inside one, and where you draw the line between “this is one module” and “this should be two.”