skip to content

For an organisation's internal Terraform modules, how would you decide between publishing them to a private module registry and having consumers reference Git URLs directly?

level: principalimportance: nice to knowfreq 32%

answer

  1. What does a version list get you?
  2. Constraints need something to query
  3. Discovery versus operating cost
  4. Count consumers, not modules
  5. One canonical source per module

basics

~20 s

Choose a private registry when you want queryable version lists, constraint resolution with the version argument, and published documentation; choose Git sources when you want no extra infrastructure and accept that every consumer pins a ref by hand.

solid answer

~50 s

The decision is about what you get from a version *list*. Only a registry source accepts the `version` argument, so only a registry lets consumers write `~> 1.4` and pick up patches without editing code, and only a registry gives you a browsable index with rendered inputs and outputs. That is worth real money when many teams consume modules they did not write. Git sources cost nothing to stand up — the repositories already exist and CI already authenticates to them — but every pin is an exact `?ref=` that a human edits, so upgrades are manual and discovery is tribal. I would default to Git for a small team with a handful of internal modules, and move to a private registry once the consumer count makes discovery and controlled patch flow the bottleneck. The one thing I would not do is run both for the same module, because then "which source is canonical" becomes a question nobody can answer.

go deeper

for a junior

Know that both are valid module sources, and that the version argument only appears with the registry form while Git uses ?ref=.

for a middle

Explain what a registry adds mechanically — a queryable version list, constraint resolution, generated documentation — and what Git gives instead: exact refs, subdirectory support, and no service to run.

for a senior

Argue the operational side: which credential paths must work on every runner, how a security patch reaches many consumers, and why a publishing gate matters more than the index itself.

for a principal

Own the decision and its exit: what triggers the move, how the migration avoids two canonical addresses, and whether the organisation is actually buying a capability or papering over missing release discipline.

## The real axis: does a version list exist Both options fetch the same HCL. The difference is that a registry is a *service that knows which versions exist*, and a Git repository is not. Everything else follows from that: - Only a registry source accepts the `version` argument, so only there can a consumer express a constraint like `~> 1.4` and let Terraform resolve it at `init`. - Only a registry can render an index: modules, their versions, their inputs and outputs, and usage examples. - With Git, the address *is* the version, so every consumer holds an exact `?ref=` and every upgrade is a human editing a string. ## What a private registry buys **Discovery.** A new team can find that a vetted VPC module exists. In a Git-only estate this is a Slack question, and the usual outcome is a second VPC module. **Controlled patch flow.** `~> 1.4` means consumers get 1.4.x fixes without a code change while a major bump still requires a deliberate edit. That is a genuine operational property: a security fix in a widely used module can reach the estate on the next `init` rather than through twenty pull requests. **A publishing boundary.** Publishing is an event with a gate — tests ran, docs generated, semver applied. Git sources let a consumer pin any commit on any branch, which means the "release" is whatever they typed. **Documentation as a product.** Rendered inputs, outputs and examples for the exact version being consumed. ## What Git buys **Nothing to operate.** No registry service, no publishing pipeline, no extra identity to manage. The repositories exist, and CI already has SSH keys or a token for them. **One authentication story.** Anything that can clone can consume, which matters for airgapped or unusual runners. A registry adds a second credential path that must work on every machine that runs `init`, including a laptop at 3am during an incident. **Total flexibility.** Subdirectory selection with `//`, monorepos, pinning a specific commit SHA for a security-sensitive module, consuming a branch during development. A registry is more opinionated about repository shape. **Complete auditability by default.** Every consumer's exact revision is in code. With a range constraint, what a consumer actually got depends on when it last initialised — which is an argument for exact `version` pins in production root modules even on a registry. ## How I would decide Count consumers, not modules. Under roughly a dozen root configurations maintained by people who all know each other, Git sources with disciplined semver tags are simply cheaper, and the manual upgrade is a feature: nothing moves without review. Above that — when the module authors and the module consumers are different teams — discovery and patch flow become the bottleneck, and the registry earns its operational cost. Secondary factors worth naming: whether you already run a platform that includes a private registry (in which case the marginal cost is near zero); whether your modules live in monorepos, which the registry model resists; and whether compliance wants a published artifact with an approval gate rather than "a tag in a repository". ## The mistakes to avoid **Running both for the same module.** Two addresses for the same code means two version numbers, two sets of consumers and no canonical answer to "what is deployed". Pick one per module and migrate deliberately. **Buying a registry to fix a discipline problem.** If modules are untested and semver is decorative, a registry publishes that faster. The publishing gate is the value, and you can enforce most of it in a Git release pipeline without buying anything. **Assuming a registry means you stop pinning.** A range constraint is a policy choice, not a default to accept everywhere: production root modules usually still want an exact version so that two runs a month apart plan identically.

  • Which capability is strictly impossible with a Git-sourced module?
    A version constraint. The `version` argument is only accepted for registry sources, because resolving `~> 1.4` requires asking a service which versions exist. A Git source carries the revision in `?ref=`, so every consumer holds one exact tag or commit and upgrades are always an explicit edit — there is no mechanism for automatic patch pickup.
  • You move internal modules from Git sources to a private registry. What is the migration risk?
    A window where both addresses work and consumers are split, so "what version is deployed" has two answers. Mitigate by publishing the registry versions from the same tags, migrating consumers in a tracked batch, and then making the Git path stop being a valid release — otherwise the old address lingers for years and the two version lines drift.
  • Does a private registry remove the need to pin versions in production?
    No. It changes pinning from mandatory to a choice. A range like `~> 1.4` lets patches flow, which is often right for widely shared modules, but a production root module usually still wants an exact version so that two runs weeks apart plan identically. Decide per environment rather than adopting the range everywhere.

saying these in an interview costs you the question

  • Thinks a registry is required to reuse modules
  • Believes version constraints work on Git sources
  • Picks a registry without counting consumers
  • Keeps both a registry and a Git address canonical
  • Assumes a registry guarantees module quality

context