Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Infrastructure as Code By James Joyner IV · · 10 min read

Versioning a Shared IaC Module Registry Without Breaking Everyone

Once dozens of teams consume a module, a sneaky minor bump becomes a fleet-wide incident. Learn the semver contract and the CI guard that enforces it.

  • #iac
  • #ai
  • #modules
  • #registry
  • #versioning
  • #semver

A shared IaC module registry changes the nature of your problems. When one team owns a module, a mistake is a local annoyance. When forty teams consume it, a single bad release is a fleet-wide incident — and the most common bad release isn’t a bug in the logic, it’s a breaking change shipped under the wrong version number. Someone renames an input, removes an output, or flips a default, tags it as a minor bump, and every consumer inherits the break on their next apply with zero warning. The module worked perfectly in the author’s repo and detonated everywhere else.

The defense is a versioning contract that’s explicit, plus a CI guard that enforces it mechanically so it doesn’t depend on anyone remembering. This guide covers both, because the policy without the enforcement is just a wiki page nobody reads.

Semver means something specific for modules

“Use semver” is useless advice until you define what MAJOR, MINOR, and PATCH mean for your module type. The general rule maps cleanly to IaC interfaces:

  • MAJOR — anything that breaks a consumer: renaming or removing an input, removing or renaming an output, changing a default in a way that alters provisioned resources, or changing behavior such that the same inputs now produce a different plan.
  • MINOR — additive, backward-compatible changes: a new optional input with a safe default, a new output, support for a new provider feature that’s off by default.
  • PATCH — internal fixes that don’t change the interface or the resulting plan: a documentation fix, a refactor with identical output, a corrected validation message.

The trap is the default change. Renaming an input is obviously breaking and people usually catch it. Changing a default — say, bumping a module’s default instance size or flipping encrypted from false to true — feels minor but alters what consumers provision on their next apply. That’s a MAJOR change, and the discipline to treat it as one is what separates a registry teams trust from one they fear.

The CI guard is the real control

Humans forget to bump major versions, especially under deadline. The policy only holds if a machine enforces it. The control is a CI check that diffs the module’s public interface between the released version and the proposed one, and fails the release if a breaking change ships without a major bump:

#!/usr/bin/env bash
# Compare the public interface of the module between the last release and HEAD.
set -euo pipefail

last_tag=$(git describe --tags --abbrev=0)
proposed_version=$(cat VERSION)

# Extract input/output names from both versions (tool-specific; shown for Terraform-style modules)
git show "$last_tag:variables.tf" | extract_var_names | sort > /tmp/old_inputs
extract_var_names < variables.tf | sort > /tmp/new_inputs

# A removed or renamed input is breaking.
if comm -23 /tmp/old_inputs /tmp/new_inputs | grep -q .; then
  if ! is_major_bump "$last_tag" "$proposed_version"; then
    echo "BREAKING: inputs removed/renamed without a major version bump:"
    comm -23 /tmp/old_inputs /tmp/new_inputs
    exit 1
  fi
fi
echo "Interface diff OK for $proposed_version"

The exact extraction differs by tooling — Pulumi packages, Bicep modules in ACR, cdk constructs on npm — but the principle is identical: compare the contract, not the implementation, and block a breaking change that’s hiding under a non-major version. This single check converts your semver policy from aspiration to enforcement.

Pinning is the consumer’s half of the contract

Versioning protects consumers only if they pin sensibly. The pragmatic default is a pessimistic constraint that accepts patches and minors but never a major automatically:

module "network" {
  source  = "registry.internal/platform/network"
  version = "~> 2.4"   # accepts 2.4.x and 2.5.x, never 3.0
}

Pessimistic pinning balances two failure modes: exact pins (= 2.4.1) leave consumers stranded on old versions and drowning in upgrade PRs, while loose ranges (>= 2.0) let a major bump sneak in. The ~> constraint lets safe upgrades flow automatically while making major upgrades a deliberate, reviewed act — which is exactly when you want a human reading the migration guide.

Deprecate gracefully, with AI help on the migration guide

The part teams always skip is graceful retirement. You can’t just delete an input in v3 and tell consumers to deal with it. Ship an overlap release where the old and new inputs both work, emit a deprecation warning, and provide a migration guide. AI is genuinely useful for drafting that guide:

You are a platform engineer. We’re renaming the input subnet_ids to subnet_id_list and removing legacy_mode in the next major release of our network module. Write a migration guide for consumers: what changes, how to update their module block, and a before/after example. Note anything that could change their plan.

The model drafted a clean guide and added the caveat that mattered:

Here’s the migration guide. One thing to verify before publishing: removing legacy_mode doesn’t just change the input — for consumers who had it set to true, the resulting plan will change because the module’s routing behavior differs. I called this out in the guide, but you should confirm exactly which resources get recreated so consumers can schedule the apply during a maintenance window.

That’s the AI-drafts, human-verifies loop. The model produced the document and flagged that a “simple rename” actually triggers resource recreation for some consumers — but confirming the blast radius and recommending a maintenance window is the platform team’s call, made with real plan output, not the model’s guess.

Make the registry boring

A module registry succeeds when upgrades are uneventful. Consumers should be able to take patches and minors without thinking, take majors deliberately with a migration guide, and never get surprised by a breaking change wearing a minor’s clothes. The CI interface-diff guard is what makes that promise enforceable.

For designing the full policy, see our module registry and versioning strategy prompt, and pair it with the reusable module design prompt for the modules themselves. The Infrastructure as Code category covers the surrounding registry tooling. Write the interface-diff check before you onboard your second consumer — it’s the cheapest insurance you’ll buy.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.