Terraform Module Versioning Policy Template
A ready-to-adapt policy for versioning and publishing Terraform modules: semver rules, breaking-change and deprecation workflows, changelog format, registry publishing, and CI/CD release pipelines.
Overview
A policy your team can copy and adapt for versioning Terraform modules: semantic versioning rules, breaking-change and deprecation workflows, changelog requirements, registry publishing, and CI/CD release pipelines. Use it as-is or strip out the sections your setup doesn’t need — it works for the public Terraform Registry, private registries on Terraform Cloud/Enterprise, and plain git-based sources.
1. Semantic Versioning Rules
1.1 Version Format
Format: MAJOR.MINOR.PATCH (e.g., 2.3.1)
MAJOR (X.0.0) — Breaking changes
- Variable renamed or removed
- Resource type changed or removed
- Output renamed or removed
- Minimum Terraform version increased
- Provider version constraint changed to incompatible
MINOR (0.X.0) — New features, backward compatible
- New variable added (with default)
- New resource added
- New output added
- New feature flag added
PATCH (0.0.X) — Bug fixes, backward compatible
- Bug fix in resource configuration
- Documentation update
- Dependency version bump (compatible)
- Example update
1.2 Pre-release Versions
1.0.0-alpha.1 — Early preview, expect breaking changes
1.0.0-beta.1 — Feature complete, testing in progress
1.0.0-rc.1 — Release candidate, final testing
1.0.0 — Stable release
1.3 Choosing the Bump
When you’re not sure which number to move, walk the public API of the module: inputs, outputs, and version constraints.
2. Module Structure
2.1 Required Files
modules/
└── my-module/
├── main.tf # Resource definitions
├── variables.tf # Input variables
├── outputs.tf # Output values
├── versions.tf # Terraform and provider version constraints
├── README.md # Module documentation
├── CHANGELOG.md # Version history
├── examples/ # Usage examples
│ └── basic/
│ ├── main.tf
│ └── README.md
└── tests/
└── basic_test.go # Terratest or kitchen-terraform
2.2 versions.tf — Version Constraints
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0, < 6.0"
}
}
}
2.3 README.md Template
# my-module
Short description of what this module does.
## Usage
module "my_module" {
source = "myorg/my-module/aws"
version = "2.3.1"
name = "my-app"
environment = "production"
}
## Inputs
| Name | Description | Type | Default | Required |
|------|-------------|------|---------|----------|
| name | Resource name | string | n/a | yes |
| environment | Environment name | string | "production" | no |
## Outputs
| Name | Description |
|------|-------------|
| id | Resource ID |
| arn | Resource ARN |
3. Breaking Change Management
Breaking changes are inevitable in shared modules — what matters is that consumers see them coming. This section defines what has to ship alongside every breaking release.
3.1 Breaking Change Checklist
- [ ] Bump MAJOR version
- [ ] Document breaking change in CHANGELOG.md
- [ ] Provide migration guide in CHANGELOG.md
- [ ] Add deprecation notice in previous MINOR version
- [ ] Update all examples to use new API
- [ ] Run tests against new version
- [ ] Notify consuming teams via Slack/email
- [ ] Set deprecation date for old version (minimum 90 days)
- [ ] Create issue tracker for migration progress
- [ ] Update module documentation
3.2 Deprecation Pattern
variable "instance_type" {
description = "(Deprecated) Use `instance_types` instead. Will be removed in v3.0.0."
type = string
default = null
deprecated = true
}
variable "instance_types" {
description = "List of instance types for the auto-scaling group"
type = list(string)
default = ["t3.micro"]
}
3.3 Migration Guide Template
## Migration Guide: v1.x to v2.x
### Breaking Changes
1. `instance_type` variable removed → use `instance_types` (list)
2. `tags` output changed from `map(string)` to `map(any)`
3. Minimum Terraform version raised from 1.0 to 1.5
### Migration Steps
1. Update module version in your configuration:
module "my_module" {
source = "myorg/my-module/aws"
version = "2.0.0"
}
2. Replace `instance_type` with `instance_types`:
# Before (v1.x)
module "my_module" {
instance_type = "t3.micro"
}
# After (v2.x)
module "my_module" {
instance_types = ["t3.micro"]
}
3. Run `terraform init -upgrade` to download the new version
4. Run `terraform plan` and review changes
5. Run `terraform apply` when ready
4. CHANGELOG Requirements
4.1 CHANGELOG Format
# Changelog
## [2.3.1] - 2026-07-04
### Fixed
- Fixed IAM policy when `enable_logging` is false
- Fixed panic when `tags` is empty map
## [2.3.0] - 2026-06-15
### Added
- New variable `enable_monitoring` (default: false)
- New output `monitoring_dashboard_url`
### Changed
- Updated AWS provider constraint to >= 5.0, < 6.0
## [2.2.0] - 2026-05-01
### Added
- Support for `kms_key_id` in S3 bucket module
### Deprecated
- `instance_type` variable deprecated, use `instance_types`
## [2.0.0] - 2026-03-01
### Breaking
- `instance_type` removed, replaced with `instance_types` (list)
- `tags` output type changed from `map(string)` to `map(any)`
- Minimum Terraform version raised to 1.5
5. Module Registry Publishing
Versioning only becomes real when consumers can fetch what you tagged. This section covers the three publishing channels — pick one per module and stay consistent.
5.1 Terraform Registry (Public)
# Tag the release in git
git tag v2.3.1
git push origin v2.3.1
# Terraform Registry auto-detects tags from GitHub
# Module URL: terraform-aws-modules/s3-bucket/aws
# Version: 2.3.1
5.2 Private Registry (Terraform Cloud/Enterprise)
# Configure Terraform CLI for private registry
terraform login app.terraform.io
# Use module from private registry
module "my_module" {
source = "app.terraform.io/myorg/my-module/aws"
version = "2.3.1"
}
5.3 Git-based Module Source
# Tag-based versioning
module "my_module" {
source = "git::https://github.com/myorg/terraform-modules.git//my-module?ref=v2.3.1"
}
# Branch-based (not recommended for production)
module "my_module" {
source = "git::https://github.com/myorg/terraform-modules.git//my-module?ref=main"
}
6. CI/CD Pipeline
6.1 GitHub Actions — Module Release Pipeline
name: Terraform Module Release
on:
push:
tags: ['v*']
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.7.0
- name: Terraform format check
run: terraform fmt -check -recursive
- name: Terraform validate
run: |
terraform init -backend=false
terraform validate
- name: Run Terratest
run: |
cd tests/
go test -v -timeout 30m
- name: Generate docs
uses: terraform-docs/terraform-docs@v1.0.0
with:
output-file: README.md
output-method: inject
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
generate_release_notes: true
body_path: CHANGELOG.md
- name: Notify teams
run: |
curl -X POST ${{ secrets.SLACK_WEBHOOK }} -d "{
\"text\": \"Terraform module ${{ github.ref_name }} released\"
}"
6.2 Pre-Release Validation
name: Terraform Module Validation
on: [pull_request]
jobs:
validate:
runs-on: ubuntu-latest
strategy:
matrix:
terraform_version: ['1.5.0', '1.6.0', '1.7.0']
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ matrix.terraform_version }}
- name: Format check
run: terraform fmt -check -recursive
- name: Init
run: terraform init -backend=false
- name: Validate
run: terraform validate
- name: Security scan (tfsec)
uses: aquasecurity/tfsec-action@v1.0.0
- name: Cost estimation (infracost)
uses: infracost/infracost-action@v3
with:
path: examples/basic
7. Version Pinning in Consuming Projects
The publishing rules cover half the contract — the other half is how consumers pin your module. These are the recommended defaults that keep upgrades boring.
7.1 Pinning Strategies
Strategy | Example | Risk
──────────────────────┼──────────────────────┼──────────────────────────
Exact pin | version = "2.3.1" | No auto-updates, manual
Pessimistic constraint| version = "~> 2.3" | Auto patch + minor updates
Pessimistic major | version = "~> 2.0" | Auto minor updates only
Range | version = ">= 2.0" | Risky, may get breaking
Latest | (no version) | Very risky, not recommended
7.2 Recommended Pinning
# Production — exact pin
module "production_db" {
source = "terraform-aws-modules/rds/aws"
version = "6.10.0"
}
# Staging — pessimistic constraint (auto patch updates)
module "staging_db" {
source = "terraform-aws-modules/rds/aws"
version = "~> 6.10"
}
# Development — pessimistic major (auto minor + patch)
module "dev_db" {
source = "terraform-aws-modules/rds/aws"
version = "~> 6.0"
}
See Also
- Complete Guide to Terraform Modules
- Terraform Best Practices — Modules, State, and Workspaces
- Infrastructure as Code — Terraform and Pulumi
- CI/CD Pipeline Security Checklist
- Complete Guide to Terraform in Production
External references:
Runnable versions of this policy live in the companion repository.
Frequently Asked Questions
How do I know when to bump the major version?
Bump major whenever consumers have to touch their own configuration to adopt your change. Typical triggers:
- A variable or output is removed or renamed
- A variable changes type (string → list, for example)
- A default changes in a way that produces different resources
- The minimum Terraform or provider version goes up
When in doubt, bump major — over-communicating a break is far cheaper than silently breaking someone's terraform apply. And soften the landing: mark the old interface deprecated in the current minor version before removing it in the next major.
Should I use the Terraform Registry or git sources for internal modules?
Use the private registry if you've got Terraform Cloud/Enterprise — you get version management, a browsable UI, and workspace integration for free.
If you don't, git sources with tag refs are the fallback: git::https://github.com/myorg/modules.git//my-module?ref=v2.3.1. Two rules for that path:
- Always pin a tag (
?ref=v2.3.1) — branch refs like?ref=maincan change under your feet and aren't reproducible. - Once you pass ~5 shared modules or ~10 consuming teams, the registry pays for itself; git-based distribution doesn't scale organizationally.
How do I deprecate a module feature without breaking existing consumers?
Run a deprecation window instead of a hard cut:
- Mark the variable or output
deprecatedand point to the replacement in the description. - Announce it in the README, the CHANGELOG, and a message to consuming teams.
- Keep the old behavior working alongside the new one for at least one minor version.
- Give a deprecation date — 90 days minimum — and track migrations in your issue tracker.
- Ship the removal in the next major release, with a migration guide that shows before/after.
What should I do if a module release has a bug?
It depends on severity. If the bug breaks infrastructure or loses data, act like it's an incident:
- You can't delete a published registry version, but you can flag it in the README and notify consuming teams immediately.
- Cut a patch release (e.g., 2.3.2) with the fix and update the CHANGELOG.
- Non-critical bug? Patch release plus changelog entry is enough.
One rule holds either way: never force-push a tag. Consumers who already pulled it keep the old content while new downloads get the new one — the same version number then means two different things, which is worse than the bug.
How do I test a module before publishing?
Automate it in CI — the pipeline in section 6 is the reference setup:
- Write Terratest or kitchen-terraform tests that deploy the module in a throwaway cloud account, verify resources, and destroy everything.
- Run tests on every PR against a matrix of Terraform versions (and provider versions if you support a range).
- Add
terraform fmt -check,terraform validate, tfsec/checkov for security, and infracost for cost visibility. - Publish only after the PR is green, reviewed, and merged.
Related Resources
Docker Image Hardening Checklist
Checklist for hardening Docker container images for production: base image selection, user permissions, file system restrictions, network isolation, resource limits, secret management, vulnerability scanning, and CI/CD integration with Dockerfile examples.
DocKubernetes Resource Quotas Template
Template for defining Kubernetes resource quotas per namespace: CPU and memory limits, object count quotas, storage quotas, LimitRanges for default requests, priority class integration, and monitoring with examples for multi-tenant clusters.
DocDeployment Rollback Runbook
Runbook for rolling back failed deployments: triggers, kubectl/Helm/ArgoCD commands, database migration rollback, and verification checklists.
DocCI/CD Pipeline Security Template
A template for securing build and deployment pipelines against credential leaks, tampering, supply chain attacks, and unauthorized deployments.
GuideTerraform Best Practices: Modules, State, and Workspaces
A practical guide to Terraform best practices: module design, remote state management, workspaces, and security for production-grade infrastructure as code.
GuideComplete Guide to Terraform Modules
Build reusable Terraform modules with proper structure, inputs, outputs, and versioning. Covers module composition, testing, and registry publishing.