Your team keeps several Terraform modules in one Git repository. How do consumers reference just one of them, and how should you tag releases so each module can be pinned independently?
answer
- The package and the path inside it
- One tag, one whole repository
- Tag names may contain slashes
- Namespace the tag per module
- Semver means what happens to my plan
basics
~20 sConsumers point at the repository and select the module with a double-slash subdirectory, for example git::https://host/repo.git//modules/vpc?ref=…. Because a Git tag covers the whole repository, tag per module with a prefixed name such as vpc/v1.2.0 to version them independently.
solid answer
~40 sThe subdirectory selector does the referencing: `source = "git::ssh://[email protected]/acme/terraform-modules.git//modules/vpc?ref=vpc/v1.2.0"`. The awkward part is tagging, because a Git tag names a commit in the whole repository, not a folder — so a plain `v1.2.0` implicitly versions every module at once and forces consumers of an untouched module to see a new version they do not need. The usual fix is prefixed tags: `vpc/v1.2.0`, `rds/v3.0.1`, each pinned in its own `?ref=`. It works, but be honest about the cost: any commit can touch several modules, release automation has to know which folders changed, and a consumer reading `?ref=vpc/v1.2.0` cannot tell what else moved. If independent lifecycles matter more than a single review surface, one repository per module — which is also what the public registry expects — is the simpler shape.
code
hcl · 13 linesmodule "vpc" {
source = "git::ssh://[email protected]/acme/terraform-modules.git//modules/vpc?ref=vpc/v1.2.0"
name = "platform"
cidr = "10.0.0.0/16"
}
module "db" {
source = "git::ssh://[email protected]/acme/terraform-modules.git//modules/rds?ref=rds/v3.0.1"
identifier = "platform-primary"
subnet_ids = module.vpc.private_subnet_ids
}go deeper
Know the //subdir syntax and be able to write a source string that points at one module inside a shared repository. Recognise that ?ref= names a Git tag.
Explain why tagging is the real problem — a tag covers the whole repository — and describe prefixed tags as the fix, including how each consumer then pins its own ref.
Show the process around it: how release automation detects which module folders changed, what a major bump means for a consumer's plan, and how untagged changes leak out when the process is manual.
Own the repository-shape decision. Weigh lifecycle coupling and review surface against independent release cadence and registry publishing, and be explicit about which failure you are choosing to accept.
## Referencing one module out of many Terraform's remote source addresses support a subdirectory selector written with a double slash. Terraform fetches the whole package — here, a Git clone — and then uses only the named directory as the module: ```hcl module "vpc" { source = "git::ssh://[email protected]/acme/terraform-modules.git//modules/vpc?ref=vpc/v1.2.0" } ``` Everything before `//` identifies the package; everything after it is a path inside that package; query parameters configure the fetcher. This is the mechanism that makes a module monorepo possible at all. ## Why tagging is the hard half A Git tag points at a commit, and a commit belongs to the entire repository. There is no such thing as a tag on a folder. So with a single `v1.2.0` series: - Releasing a change to one module bumps the version number that *every* module shares. - A consumer of an untouched module either stays behind on an old tag or moves to a tag whose changelog is about someone else's module. - You cannot tell from the version number whether anything you depend on actually changed. The conventional workaround is a namespaced tag per module. Git tag names may contain slashes, so `vpc/v1.2.0` and `rds/v3.0.1` coexist happily and each consumer pins its own: ```hcl module "vpc" { source = "git::…/terraform-modules.git//modules/vpc?ref=vpc/v1.2.0" } module "db" { source = "git::…/terraform-modules.git//modules/rds?ref=rds/v3.0.1" } ``` Note that the same repository is now cloned under two different refs, which is fine — Terraform keeps each resolved module separately in `.terraform/modules`. ## What the release process has to do Prefixed tags only help if something enforces them. A workable process: 1. On merge, determine which module directories changed (a path filter over the diff). 2. For each changed directory, decide the semver bump — patch for a fix, minor for a backwards-compatible input or output, major for a removed or renamed input, a changed default that moves resources, or anything that makes a consumer's plan destroy something. 3. Create `<module>/vMAJOR.MINOR.PATCH` on the merge commit and publish a changelog entry scoped to that module. The failure mode when this is manual is predictable: someone edits two modules in one commit, tags only one, and the other module's consumers silently pick up an untagged change the next time they move their ref. ## Semver as a contract, not decoration Whatever the tag layout, the number has to mean something to consumers, and for a Terraform module the meaning is *what will this do to my plan*. A change that only adds an optional variable with a default is a minor bump. A change that renames a resource address inside the module is a major bump, because a consumer's plan will show a destroy and create unless the module ships refactor declarations to move the old addresses to the new ones. Treating a plan-destroying change as a patch is how a module ecosystem loses the trust that pinning was supposed to buy. ## The registry alternative The public Terraform Registry expects one module per repository, named `terraform-<PROVIDER>-<NAME>`, and derives versions from plain semver tags — which is exactly why monorepos are normally consumed by Git source rather than published to it. If you want the registry's version list, `~>` constraint resolution and generated documentation, the shape of your repositories is part of the price. ## When a monorepo is still right Monorepos win when the modules are genuinely developed together: one pull request can change a module and its callers, review happens in one place, and shared test scaffolding lives beside the code. They lose when the modules have independent consumers and independent release cadences, because then every one of the problems above is a weekly tax. Decide on lifecycle coupling, not on how many repositories you like looking at.
- What breaks if the team uses a single v1.2.0 tag series for all modules in the monorepo?Every module shares one version number, so a release for one of them offers nothing to consumers of the others while still looking like an upgrade. Consumers cannot tell from the number whether their dependency changed, changelogs become repository-wide, and pinning stops carrying information — the version is no longer a statement about the module you actually use.
- How would you decide that a module change deserves a major version bump?Ask what it does to a consumer's plan. Removing or renaming an input, changing a default that moves or replaces resources, or renaming a resource address inside the module are all major: someone's apply will destroy something or fail. Adding an optional input with a default, or a new output, is minor. Only changes invisible in the plan are patches.
saying these in an interview costs you the question
- Thinks a Git tag can apply to a subdirectory
- Uses a branch ref per module instead of tags
- Bumps only the patch number for breaking input changes
- Believes the public registry publishes monorepo subdirectories
- Omits //subdir and expects Terraform to find the module