Terraform Contribution Flow

This guide covers the end-to-end contribution flow for AVM Terraform modules.
Whether you are a module owner or an external contributor, the core workflow is the same — the key differences are called out using tabs below.

Important

Every new AVM Terraform module MUST use AzAPI for every control-plane resource and supported data-plane operation. AzureRM is permitted only for a specific unsupported data-plane/non-ARM API operation under the narrow TFFR3 exception. The same rule applies to submodules, examples, end-to-end tests, Terraform tests, fixtures, and documentation snippets.

This guide MUST be used in conjunction with the Terraform specifications. All AVM modules must meet the requirements described in those specifications.

Prerequisites

Install PowerShell 7.4 or later, start pwsh, and install the Avm.Authoring module:

Install-PSResource Avm.Authoring
Import-Module Avm.Authoring

Install-Module Avm.Authoring -Scope CurrentUser is also supported for environments that use PowerShellGet v2. Run avm with no arguments to list the supported verbs, then verify the installed version and diagnose your local environment:

avm
avm version
avm doctor

Run avm update whenever a newer Avm.Authoring release is available. Avm.Authoring downloads, verifies, and caches Terraform, TFLint, terraform-docs, Conftest, and mapotf on demand. Docker or Podman is not required. You can inspect or manage the tool cache:

avm tool list
avm tool which terraform
avm tool install terraform
avm doctor --install
Warning

The local Avm.Authoring migration and the centrally managed CI workflow rollout are separate changes. Do not pre-emptively rename required checks. After the updated workflow has run on a pull request, update branch protection to use the exact check names reported by that workflow. Per-example checks are derived from the repository’s example folders.

Overview


---
config:
  nodeSpacing: 20
  rankSpacing: 20
  diagramPadding: 50
  padding: 5
  flowchart:
    wrappingWidth: 300
    padding: 5
  layout: elk
  elk:
    mergeEdges: true
    nodePlacementStrategy: LINEAR_SEGMENTS
---

flowchart TD
  Z("1 - Fork [optional]")
    click Z "#1-fork-optional"
  A(2 - Branch)
    click A "#2-branch"
  B(3 - Implement your code change)
    click B "#3-implement-your-code-change"
  C(4 - Run avm pre-commit)
    click C "#4-run-avm-pre-commit"
  C2(5 - Run pr-check and test tiers locally)
    click C2 "#5-run-pr-check-and-test-tiers-locally"
  D(6 - Raise or Update PR)
    click D "#6-raise-or-update-pr"
  E("7 - Approve and monitor CI tests [owner]")
    click E "#7-approve-and-monitor-ci-tests"
  F{Tests passing?}
  G(8 - Review and merge PR)
    click G "#8-review-and-merge-pr"
  H(9 - Cut a release)
    click H "#9-cut-a-release"
  Z --> A
  A --> B
  B --> C
  C --> C2
  C2 --> D
  D --> E
  E --> F
  F -->|no| B
  F -->|yes| G
  G --> H


1. Fork [optional]

Note

This step is only needed if you do not have write access to the module repository. Module owners and invited collaborators can skip to step 2.

A fork is your own copy of the repository under your GitHub account. It lets you make changes without needing write access to the upstream repo. Once your changes are ready, you raise a pull request from your fork back to the original repository.

  1. Navigate to the module repository in the Azure GitHub organization.

  2. Click the Fork button in the top right.

  3. Select your GitHub account (or organization) as the destination.

  4. Click Create fork.

  5. Clone your fork locally:

    git clone https://github.com/<your-username>/terraform-azure-avm-res-<rp>-<modulename>.git
    cd terraform-azure-avm-res-<rp>-<modulename>

Keep your fork in sync with the upstream repository before creating a new branch. You can do this from the GitHub UI by clicking Sync fork on your fork’s main page, or locally:

git remote add upstream https://github.com/Azure/terraform-azure-avm-res-<rp>-<modulename>.git
git fetch upstream
git checkout main
git merge upstream/main

Use the GitHub CLI to fork and clone in one step. This automatically configures the upstream remote for you:

gh repo fork Azure/terraform-azure-avm-res-<rp>-<modulename> --clone
cd terraform-azure-avm-res-<rp>-<modulename>

Verify the remotes are set up correctly:

git remote -v
# origin    https://github.com/<your-username>/terraform-azure-avm-res-<rp>-<modulename>.git (fetch)
# upstream  https://github.com/Azure/terraform-azure-avm-res-<rp>-<modulename>.git (fetch)

2. Branch

Create a branch from main to work on your changes:

git checkout -b <your-branch-name>

If this is a new module and the repository does not exist yet, module owners should first follow the Repository Creation Process.

Note

If the module repository does not exist yet, check the Terraform Resource Modules index for the module owner’s contact details (PrimaryModuleOwnerGHHandle column).


3. Implement your code change

Before writing code, review the Terraform specifications and composition guidelines to ensure your contribution complies with AVM’s design principles. For a new module, confirm first that every control-plane resource and supported data-plane operation uses AzAPI. Any AzureRM block must satisfy and document the unsupported data-plane exception in TFFR3.

Once you’ve made your changes, stage, commit, and push them:

git add -A
git commit -m "feat: description of your change"
git push

Lifecycle hooks

Some examples need setup work before Terraform runs — deploying prerequisites, generating a terraform.tfvars, or seeding a random prefix. AVM supports optional hook scripts for this:

Terraform configurations created for examples, end-to-end tests, tests, or fixtures are part of the module repository and MUST follow TFFR3. Use AzAPI for every supporting control-plane resource. AzureRM may be configured or exercised only when the test covers the module’s documented unsupported data-plane operation; it must not be used to make setup more convenient.

HookLocationRuns
pre.ps1examples/<name>/before Terraform commands for the example during policy checks and e2e tests
post.ps1examples/<name>/after the example, including when its pre-hook or Terraform initialization fails
tflint-pre.ps1examples/<name>/after terraform init, before TFLint
setup.ps1tests/<tier>/ or modules/<name>/tests/<tier>/before terraform init and terraform test for the target
Warning

Hooks must be PowerShell. Avm.Authoring rejects per-example pre.sh, post.sh, and tflint-pre.sh files, plus setup.sh and teardown.sh files under tests/<tier>/, on presence alone. Adding a .ps1 while leaving the corresponding .sh in place still fails before Terraform runs:

The terraform unit test engine runs PowerShell hooks only.
Refactor these shell hooks to '.ps1': tests/unit/setup.sh

Legacy root-level examples/setup.sh and examples/teardown.sh files are different: Avm.Authoring does not execute or reject them, and there is no global PowerShell equivalent. Move required logic into idempotent per-example pre.ps1 and post.ps1 hooks. Coordinate removal of legacy global hooks with the repository’s centrally managed CI workflow migration.

Each hook runs in its own isolated pwsh subprocess, so environment variables it exports do not reach subsequent Terraform commands. An e2e pre.ps1 can pass values by writing KEY=VALUE lines to examples/<name>/.env; the runner reads the file after the hook and passes the values to the example’s Terraform subprocesses. A unit or integration setup.ps1 uses a .env file at its target root: the repository root or modules/<name>/. For these test hooks, the .env file is two directories above setup.ps1, not beside it.

Because hooks are invoked from an isolated process, anchor paths on $PSScriptRoot rather than relying on the current working directory. Note also that PowerShell does not stop on a failed native command the way set -e does in bash — check $LASTEXITCODE after each Terraform call and throw so a failed hook surfaces immediately instead of later as a confusing downstream error.


4. Run avm pre-commit

Before raising a pull request, run pre-commit to update your files:

avm pre-commit

For Terraform modules, this command:

  1. Synchronizes the centrally governed managed files, which can add, update, or remove files.
  2. Applies deterministic fixes for AVM convention rules.
  3. Runs mapotf transformations.
  4. Formats Terraform files.
  5. Regenerates documentation.

The command intentionally updates the working tree. Review every change it makes, then commit and push again:

git add -A
git commit -m "chore: pre-commit fixes"
git push

5. Run pr-check and test tiers locally

After committing the pre-commit changes, run the broader pull request checks:

az login
avm pr-check

avm pr-check requires a clean Git working tree and Azure credentials because its Conftest policy checks create Terraform plans for the examples. It checks managed-file, formatting, and transformation drift, then runs TFLint, Conftest policy checks, AVM convention checks, terraform validate, and documentation drift checks. It does not run the Terraform test tiers; those remain separate commands so failures are reported independently.

Unit testing

AVM convention checks require a unit test fixture under tests/unit. Use mocked providers to keep unit tests fast and free of external dependencies:

avm test unit

The command also runs unit test tiers found under direct modules/<name>/ submodules.

Integration testing

Integration tests under tests/integration deploy real resources and require Azure credentials:

az login
avm test integration

The command also runs integration test tiers found under direct modules/<name>/ submodules. A repository without integration tests reports the tier as skipped rather than passed.

Local e2e testing

Run the e2e test tier to deploy, check idempotency, and destroy resources for each example:

az login
avm test e2e

This tier requires real Azure credentials. Azure CLI authentication is sufficient for local development; no environment variables or service principals are needed. To run one example while iterating:

avm test e2e --example <name>

An example containing .e2eignore is excluded. Apply failures caused by transient region, SKU capacity, or quota errors are destroyed and retried up to two times by default; use -MaxRetry 0 to disable retries.

Local e2e testing is especially useful for external contributors, since only module owners can approve credentialed CI e2e runs.


6. Raise or Update PR

Tip

Raise your PR early — don’t wait until everything is perfect. An early PR lets you run validation and test tiers in CI and get feedback sooner. You can continue pushing commits to the same branch.

  1. Navigate to the upstream repository on GitHub and click New pull request.
  2. Set the base repository to the upstream AVM repo and base branch to main.
  3. Set your head repository and compare branch to your fork and branch.
  4. Click Create pull request.
  1. Navigate to the repository on GitHub and click New pull request.
  2. Set the base branch to main and the compare branch to your branch.
  3. Click Create pull request.

7. Approve and monitor CI tests

Note

Credentialed CI jobs require approval from a module owner. Unit tests do not require Azure credentials, but external contributors should still run avm pr-check and all applicable test tiers locally before this step.

Once a PR is created, CI workflows are triggered automatically. A centrally managed Azure test subscription is provided for credentialed jobs, so contributors do not configure CI credentials themselves.

What CI runs

The centrally managed workflow keeps validation and test tiers in separate jobs so a failure produces an actionable signal:

  • PR validation — runs the equivalent of avm pr-check: managed-file and generated-file drift checks, Terraform formatting, mapotf transformations, TFLint, Conftest policy checks, AVM convention checks, terraform validate, and documentation checks.
  • Unit tests — runs avm test unit.
  • Integration tests — runs avm test integration when the repository has integration tests.
  • End-to-end tests — discovers runnable examples and tests each example independently with the equivalent of avm test e2e --example <name>.

Each e2e job deploys the example, checks idempotency with terraform plan, and destroys the resources. Examples containing .e2eignore are excluded.

If tests fail

Go back to step 3 — fix the issue, run avm pre-commit again, push your changes, and the CI tests will re-run automatically on the same PR.

Running e2e for external contributions

When approving a PR from an external contributor:

  1. Review the code for security — check for any malicious code or changes to workflow files before running tests. If found, close the PR and report the contributor.
  2. Create a release branch from main (e.g. release/<description>).
  3. Change the PR’s base branch to the release branch and merge it.
  4. Create a new PR from the release branch to main — this triggers the validation and test jobs.
  5. Approve the run and wait for results.
  6. If tests fail, send back to the contributor to fix and repeat from step 3.

Running e2e for your own contributions

For your own PRs, the tests trigger automatically — approve the run and wait for results.


8. Review and merge PR

Important

PR approvals are enforced on all AVM Terraform module repositories. A PR cannot be merged until it has been approved by an authorized module owner.

Finding an approver

  1. First port of call — find a friendly module owner. Look up another active Terraform module owner from the azure-verified-modules-module-contributors Entra group and request a review from them directly. This is the fastest path to approval.
  2. If no module owner is available, fall back to the AVM core team:
    • Assign the @Azure/azure-verified-modules-engineering-owners GitHub team as a reviewer on the PR.
    • Apply the  Needs: Core Team 🧞  label so the request is picked up during core team triage.
PR is submitted by a module ownerPR is submitted by anyone, other than the module owner
Module has a single module ownerAVM core team or in case of Terraform only, the owner of another module approves the PRModule owner approves the PR
Module has multiple module ownersAnother owner of the module (other than the submitter) approves the PROne of the owners of the module approves the PR
  • Address any review comments and push updates to your branch.
  • Request a re-review once changes are made.
  • The module owner will merge the PR once approved and tests pass.

For a brand new module being published for the first time, get the module reviewed by the AVM Core team by following the AVM Review Process before merging.

Owner responsibilities


9. Cut a release

Note

This step is performed by the module owner only.

After the PR is merged to main, create a release via GitHub Releases:

  1. Go to the Releases tab and click Draft a new release.
  2. Set Target to the main branch.
  3. Type a new tag (e.g. v0.1.0 for first publish, or increment for subsequent releases). Tags MUST include the v prefix.
  4. Use Generate release notes and credit external contributors.
  5. Click Publish release.

First module publish

For a brand new module, contact the AVM core team (e.g. via the AVM - Module Triage project) to request initial publication to the HashiCorp Registry. Subsequent releases are published automatically.

Important

Continue publishing in the v0.x.y range (e.g., v0.1.0, v0.1.1, v0.2.0) until the AVM team notifies you that v1.0.0 is allowed.


Common mistakes to avoid

  • Search and update TODO comments that come from the template — remove them once addressed.
  • Do not commit terraform.lock.hcl — it is excluded by .gitignore.
  • Update _header.md and SUPPORT.md.
  • Do not commit terraform.tfvars files.
  • Do not commit .env files created by lifecycle hooks.
  • Do not add shell (.sh) lifecycle hooks — see Lifecycle hooks.