Terraform

Terraform Private Modules: Practical Guidance for In-Org Module Operations

2026-04-19
NicheeLab Editorial Team

When you want to distribute modules safely inside your organization without using the public Terraform Registry, the most reliable answer is the Private Module Registry (PMR) in Terraform Cloud/Enterprise.

This article covers the big picture of the PMR, naming and SemVer operations, the publish/consume flow, permissions and visibility, testing strategy, and the key points for migration and troubleshooting. It also summarizes common Terraform exam themes (registry references vs. Git references, handling of version constraints).

Big Picture and Options: Why Private Modules?

The Private Module Registry ingests tags from VCS repositories as semantic versions and provides name resolution and distribution to users inside your organization. The Terraform CLI prefers source addresses resolved via the registry and can also interpret constraints in the version argument.

You can also distribute modules via raw Git URL references (git::), but the PMR is superior for discoverability, version selection, access control, and auditing. Hosting your own Registry API implementation is advanced; starting with Terraform Cloud/Enterprise is the realistic choice.

  • The pillars of stable operations: naming conventions + semantic versioning + automated tests + least privilege.
  • The module block's version works with registry references. With Git references it has no effect; you must pin via ref=.
  • At organizational scale, module discoverability (search and documentation) determines productivity.
OptionMain BenefitsCaveats / ConstraintsSource Syntax
Terraform Cloud/Enterprise Private Module RegistryIn-org distribution, search, visibility, version resolution, audit logsRequires VCS integration plus naming/tag conventions; publishing is tag-drivenapp.terraform.io/<org>/<name>/<provider>
Git source reference (git::https...)Simple; works with any Git; usable without a registryThe version argument is not usable; pin via ref=; low discoverabilitygit::https://.../repo.git?ref=v1.2.3
Self-hosted Registry API implementationStays within the corporate network; integrated controlHigh protocol implementation and maintenance cost; start with TFE/TFC<host>/<org>/<name>/<provider>

PMR-based distribution flow (conceptual diagram)

git push/tag v1.2.0index / tag scanVCS integration / tag syncSentinel / Run TasksModule DeveloperVCSGitHub / GitLabTerraform Cloud / EnterpriseOrg: acmePrivate Module Registryapp.terraform.io/acme/vpc/awsmodule source.terraform/modules/terraform init / getModule Developer → VCS tag → Terraform Cloud/Enterprise PMR → consumer fetches via terraform init

Minimal registry reference example (with version constraint)

module "vpc" {
  source  = "app.terraform.io/acme/vpc/aws"
  version = "~> 1.2"

  name    = "core"
  cidr    = "10.0.0.0/16"
}

terraform {
  required_version = ">= 1.4"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

Naming Conventions and Semantic Versioning

The PMR recognizes modules from repository naming and tags. The naming convention is terraform-<provider>-<name> (e.g., terraform-aws-vpc). Tags must be consistent with semantic versions.

We recommend the v1.2.3 tag form (some environments accept 1.2.3, but standardize across the org). Express breaking changes as a major bump, backward-compatible additions as minor, and bug fixes as patch.

  • Repository: terraform-<provider>-<name> (e.g., terraform-azurerm-network)
  • Tag convention: vMAJOR.MINOR.PATCH (e.g., v1.4.2; pre-releases like v1.5.0-rc1)
  • Always update CHANGELOG and README. Ship usage examples in an examples/ directory.
  • Do not casually introduce breaking changes in outputs/variables; mark items deprecated and remove in stages.

Tag and CHANGELOG example (convention sample)

Repository name: terraform-aws-vpc

Tag: v1.2.0
CHANGELOG.md excerpt:
- feat: Add flow logs support (#123)
- fix: Correct IGW dependency (#125)
- docs: Update examples for multi-AZ (#126)

Planned breaking change:
- v2.0.0 will remove variable "enable_classiclink" (due to EOL)

Publish/Consume Workflow (Terraform Cloud/Enterprise)

On the publisher side, all you do is push a tag to VCS. The Terraform Cloud/Enterprise PMR detects the tag and registers it as a new version. Consumers specify a registry-format source and give a version constraint.

The CLI must authenticate to app.terraform.io. Use terraform login to create credentials (~/.terraform.d/credentials.tfrc.json), and they are picked up automatically during init.

  • VCS integration is configured once at the org level; after that, creating a repo and tagging registers modules automatically.
  • Run validate/plan in CI for development branches; restrict tagging to post-review only.
  • On the consumer side, use a conservative constraint like version = "~> 1.2" (minor-compatible range).
  • If you need to revert to a Git reference, switch source to git:: and pin with ref=.

Git reference vs. registry reference (right/wrong)

# OK: registry references can use version
module "vpc" {
  source  = "app.terraform.io/acme/vpc/aws"
  version = ">= 1.2, < 2.0"
}

# OK: Git references pin via ref= (do not write version)
module "vpc_git" {
  source = "git::https://git.example.com/terraform-aws-vpc.git?ref=v1.2.3"
}

# BAD: Git reference with a version argument (causes an error)
module "vpc_bad" {
  source  = "git::https://git.example.com/terraform-aws-vpc.git?ref=v1.2.3"
  version = "~> 1.2"  # ← not supported
}

Designing Permissions, Visibility, and Auditing

Access to the PMR depends on organization membership and team permission design. Restrict Publish and module Admin rights, while granting Read/Use broadly. In practice, the common split is "publishing is limited to the module maintainer team" and "consumption is open to all developers."

For auditing, cross-reference Terraform Cloud/Enterprise run logs, VCS tag history, and review history to see who referenced which version when. As a rule, prohibit retraction and consistently provide fixes via new patch versions.

  • Minimize publish permissions (mandatory review + tag protection).
  • Open consumption broadly within the org; everything stays inside the network boundary.
  • Treat breaking changes as a major bump and ship a migration guide.
  • Avoid version deletion; instead, mark items deprecated and ship a new patch.

Repo Layout, CI Testing, and Maintaining Compatibility

Make the minimal layout and representative use cases runnable under examples/, and run terraform fmt -check, validate, tflint, and ideally Terratest/Kitchen-Terraform in CI. Ideally, you can verify on PRs that the examples can plan/apply.

Express compatibility explicitly in versions.tf via required_version and required_providers, and change output/input schemas in stages. Avoid breaking changes from 0 → 1 — make them only at major bumps.

  • Directory: no modules/ needed (single-repo model); put main/variables/outputs at the root.
  • Provide examples/complete and examples/minimal, and run them in CI.
  • Pin provider versions on the strict side (~>) and announce updates in release notes.
  • Note: .terraform.lock.hcl is for providers; it does not apply to modules.

versions.tf (explicit compatibility)

terraform {
  required_version = ">= 1.4, < 2.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.31"
    }
  }
}

# Document the following in the module README:
# - Supported Terraform/Provider versions
# - Input/output compatibility policy
# - Announcements and migration steps for breaking changes

Migration, Troubleshooting, and Exam Angles

When migrating from Git references to PMR references, first normalize existing tags to SemVer and let the PMR recognize them, then swap module.source. In existing workspaces, run terraform init -upgrade to refresh resolution information.

Typical errors include naming convention mismatch (not terraform-<provider>-<name>), missing tags, mixing version with git::, and authentication gaps (credentials.tfrc.json not configured).

  • Normalize tags to SemVer; v1.2.3 is recommended (watch for environment differences).
  • module version is valid only with registry references; with Git, use ref= only.
  • If init returns 401/403, check tokens, org permissions, and the VCS integration.
  • Minimal repro: place just the module in a fresh directory and try terraform init.

Migration replacement example (Git → PMR)

# Before (Git reference)
module "vpc" {
  source = "git::https://git.example.com/terraform-aws-vpc.git?ref=v1.2.0"
}

# After (PMR reference)
module "vpc" {
  source  = "app.terraform.io/acme/vpc/aws"
  version = "~> 1.2"
}

Check Your Understanding

Pro

問題 1

For a module that references a Git repository directly, which behavior of Terraform is correct when you also write version = "~> 1.2" in the module block?

  1. It errors (version is valid only with registry references)
  2. ref takes precedence and version is silently ignored
  3. It auto-resolves from the available Git tags
  4. Initialization succeeds but plan fails

正解: A

The module version argument is valid only with registry references (e.g., app.terraform.io/org/name/provider). Writing it alongside a git:: reference causes an invalid-argument error. To pin a Git reference, use the ref= parameter.

Frequently Asked Questions

How do I safely migrate from existing Git references to a Private Module Registry?

First, normalize the tags in the existing repository to SemVer (e.g., v1.2.3) and confirm the PMR recognizes them. Next, in the consuming project change source to app.terraform.io/<org>/<name>/<provider> and set a conservative version constraint (~> 1.2). After terraform login, run terraform init -upgrade to refresh resolution. For a staged migration, swap only a subset of workspaces first and observe the impact.

I published a bad version by mistake. Should I delete it?

As a rule, avoid deletion. For consumer reproducibility and auditing, ship a patch release (e.g., v1.2.1) quickly and document the issue and fix in CHANGELOG and README. Whether you can unpublish in an emergency depends on operational policy and your Terraform Cloud/Enterprise settings, so agree on the rules in advance.

Are pre-release (-rc, -beta) tags selected?

The PMR indexes SemVer-compatible tags. Terraform version resolution follows SemVer ordering, and pre-releases become candidates if they match the constraint. That said, when a stable release exists it is usually ranked higher. Recommend a policy of using only stable releases in production.

Check what you learned with practice questions

Practice with certification-focused question sets

無料で問題を解いてみる
Author

NicheeLab Editorial Team

NicheeLab editorial team focused on data engineering and cloud certification learning. Content is structured around practical study needs and official exam domains.


Related articles
Terraform

HCL Syntax: Terraform's Configuration Language (2026)

HCL2 fundamentals for Terraform — blocks, attributes, expres...

Terraform

Terraform Authoring & Operations Pro: Complete Guide (2026)

Tactics for the Terraform Pro exam — module authoring, works...

Terraform

Terraform Providers: Plugin Management Fundamentals (2026)

Provider mechanics — required_providers, versions, mirrors, ...

Terraform

Terraform Resource Blocks: Declarative Infra Units (2026)

Resource block fundamentals — addresses, references, common ...

Terraform

Terraform Data Sources: Read-Only External Data (2026)

Data source basics — declaration, refresh behavior, dependen...

Browse all Terraform articles (102)
© 2026 NicheeLab All rights reserved.