| name | avm-terraform-module-development |
|---|---|
| description | Azure Verified Modules (AVM) Terraform development workflow for reviewing, fixing, and extending Resource Modules and Pattern Modules |
| glob | **/*.terraform,**/*.tf,**/*.tfvars,**/*.tfstate,**/*.tflint.hcl,**/*.tf.json,**/*.tfvars.json |
Azure Verified Modules (AVM) are pre-built, tested, and validated Terraform and Bicep modules that follow Azure best practices. Use this skill when reviewing, fixing, or extending an AVM Terraform module so the change stays aligned with the published AVM specifications.
Look at the repo name and _header.md to classify the module. The naming convention is terraform-<provider>-avm-<type>-<name>:
| Type | Name token | Purpose |
|---|---|---|
| Resource Module | res |
Deploys a single primary Azure resource plus its tightly-coupled children (e.g. terraform-azurerm-avm-res-storage-storageaccount). |
| Pattern Module | ptn |
Composes multiple resource modules into an opinionated workload (e.g. terraform-azurerm-avm-ptn-aks-production). |
| Utility Module | utl |
Helper module exposing shared inputs/outputs (e.g. Azure/avm-utl-interfaces/azure). |
The composition rules differ slightly per type. The shared rules below apply to resource and pattern modules — if you are working on a utility module, fetch its dedicated spec.
Every AVM rule has an ID (e.g. TFRMFR1, TFFR4, SNFR3). When you need the authoritative text:
- Fetch the AVM spec index once per session:
https://azure.github.io/Azure-Verified-Modules/llms.txt
- Look up the raw markdown URL for the spec ID you need. URLs follow the pattern:
https://raw.githubusercontent.com/Azure/Azure-Verified-Modules/refs/heads/main/docs/content/specs-defs/includes/terraform/<scope>/<functional|non-functional>/<ID>.md
- Fetch and read the specific spec markdown.
Never cite a spec ID without first confirming its current text from the index — wording and severity can change.
AVM uses RFC 2119 keywords: MUST, SHOULD, MAY. Treat MUST rules as blocking and SHOULD as defaults that need an explicit reason to skip.
Before you claim a change is done, verify the module still satisfies these MUST-level gates. For full text, look up each ID via llms.txt.
RMNFR1— Module names followterraform-<provider>-avm-<res|ptn|utl>-<service>[-<descriptor>]. The approved name is the one in the module proposal / module-index CSV — don't construct it yourself.TFNFR39— Standard file layout:main.tf,variables.tf,outputs.tf,terraform.tfare MUST;locals.tfis required if any locals exist. Files MAY be split asmain.<topic>.tf/variables.<topic>.tf/outputs.<topic>.tf/locals.<topic>.tfusing the canonical prefix. Theterraform {}block appears exactly once, interraform.tf. Noproviders.tfat module root.TFNFR2/SNFR15—README.mdis auto-generated. Edit_header.md(and_footer.md) only — they are required inputs to docs generation, in every submodule too.TFNFR4—snake_caseeverywhere in Terraform code.
TFNFR1/TFNFR17/TFNFR18— Every variable and output has adescriptionand a precisetype.TFNFR20— Collection variables (map,set,list) default to{}/[]withnullable = falserather thannull.RMFR7/TFFR2/TFNFR16— Outputs follow AVM minimum requirements and naming rules. For Terraform-specific additional outputs, prefer discrete computed attributes over whole resource object outputs.TFRMFR1— Resource Module Parent ID: expose the parent scope as a singlestringvariable namedparent_id,nullable = false, no default. Assign it toparent_idon every primaryazapi_resource. Modules MUST NOT acceptresource_group_name,resource_group_resource_id, or any other parent-scope-specific variable. Modules MUST NOT create the parent scope themselves (supersedes the Terraform clause ofRMFR3). Submodules typically receiveparent_id = azapi_resource.this.idfrom the parent.TFNFR38— Validateparent_idwithcan(provider::azapi::parse_resource_id("<ExpectedParentType>", var.parent_id)). The expected parent type MUST be a literal string (e.g."Microsoft.Resources/resourceGroups"or"Microsoft.Network/virtualNetworks"). Hand-rolledregex/startswith/lengthchecks are not allowed. Extension-resource modules (locks, role assignments, diagnostic settings, tags, etc.) are the only exception and use a genericstartswithcheck on/subscriptions/or/providers/, with the reason documented in the README.
TFRMNFR2— Primary Resource Naming: the primaryazapi_resource(or equivalent AzAPI resource) MUST be namedthis. Every satellite resource (lock, role assignments, diagnostic settings, private endpoints, child resources required by the primary, etc.) MUST NOT be namedthis— it MUST be named after what it represents (e.g.azapi_resource.lock,azapi_resource.role_assignments,azapi_resource.diagnostic_settings). Each submodule has its ownthis. This is what lets consumers and the AVM interface utility module rely onazapi_resource.this.idandazapi_resource.this.output.TFRMNFR1— Subresources as submodules: every ARM subresource (a child resource type in the API spec) MUST be implemented as a Terraform submodule undermodules/<singular-subresource-name>/. Submodules are full AVM modules in their own right (same shared/RM/TF specs apply), each with their own_header.mdand_footer.md. Submodules MUST NOT declarecount/for_eachon their primaryazapi_resource— cardinality is the parent's responsibility. Parent modules MUST reference submodules by local relative path (./modules/<name>), not via the registry or git.TFFR3— Resources are implemented with the AzAPI provider (Azure/azapi>= 2.0, < 3.0). Only fall back toazurerm(preferring data sources) when AzAPI genuinely lacks an equivalent; document the reason in code and inREADME.mdper the exception requirements.TFFR4— Everyazapi_resourceMUST specifyresponse_export_values, even if it is[]. Use it (list or map form) to surface read-only properties needed by the module's outputs.TFFR5— Everyazapi_resourceMUST specifyreplace_triggers_refs, listing the body paths that should force replacement when changed.nameandlocationalready trigger replacement and don't need to be listed.TFFR6— Thetypeargument MUST NOT be hard-coded. Source it from aresource_typesobject variable with oneoptional(string, "<provider>/<resource>@<api-version>")field per AzAPI resource the module declares. Defaults must be stable (non-preview) API versions. Parent modules MUST cascade the relevant subset ofresource_typesto each submodule.TFFR7— Exposeretryandtimeoutsvariables and apply them to everyazapi_resource.retryis an attribute (assign directly);timeoutsis a block (usedynamic "timeouts"so thenulldefault works). Cascade unchanged into submodules. See AzAPI.md.
For full AzAPI patterns, the parent_id variable shape, the Get-AzureSchema lookup CLI, and provider configuration, read AzAPI.md.
SFR3/SFR4—main.telemetry.tfmust declare themodtmtelemetry resource gated onvar.enable_telemetry. Do not remove or rename it.TFFR3/TFNFR26— Pinrequired_providersversions (Azure/azapi,Azure/modtm,hashicorp/random, any other providers used) in the singleterraform {}block interraform.tf. AzAPI version policy is governed byTFFR3.TFNFR27— No provider configuration blocks in modules (onlyrequired_providers). Provider configuration belongs in the consumer's root module.
The mapotf pre-commit config under mapotf-configs/pre-commit enforces the telemetry block and provider versions automatically — do not hand-edit those generated files.
AVM defines a fixed set of standard interfaces that resource modules expose where the underlying Azure resource supports them. They standardise variable names, types, and behaviour across every module:
- Resource features (apply only when the underlying resource supports them): diagnostic settings (v2 schema), role assignments, locks, managed identities, private endpoints, customer-managed keys, tags.
- AzAPI mechanics (apply to every module):
resource_types(API-version pinning perazapi_resource, module-specific keys, cascaded to submodules),retry(assigned as an attribute),timeouts(emitted via adynamic "timeouts"block).
The resource-feature interfaces are backed by the shared utility module Azure/avm-utl-interfaces/azure — compose it rather than redefining variable shapes by hand. The diagnostic-settings interface MUST use the v2 shape (diagnostic_settings_v2 input / diagnostic_settings_azapi_v2 output on the utility module).
For variable shapes, defaults, the v2 diagnostic-settings details, and which interfaces apply to which resource, read interfaces.md. For the retry / timeouts variable schemas and the required dynamic "timeouts" wiring on azapi_resource, read AzAPI.md.
For a single concise summary of how a resource or pattern module fits together (file layout, parent-child resource splitting, sub-module rules, examples folder conventions), read module-composition.md.
Follow these steps in order when fixing an issue or adding a feature.
git checkout main
git pullgit checkout -b feature/<short-description>
# or
git checkout -b fix/<issue-number>-<short-description>Make the necessary code changes, keeping the composition checklist above in mind.
For AzAPI resource patterns, schema lookups, and the Get-AzureSchema CLI tool, read AzAPI.md. To query Terraform provider schemas (resources, data sources, functions, ephemeral resources), use the tfpluginschema CLI — see tfpluginschema.md.
Unit tests use provider mocking and live in the tests/unit directory. Add or update unit tests when your change introduces new logic, variables, or outputs that can be validated without deploying real infrastructure. For test writing guidance, syntax, and patterns, read terraform-test.md.
PORCH_NO_TUI=1 ./avm tf-test-unitIntegration tests do not use provider mocking and live in the tests/integration directory. Add or update integration tests when your change requires validation against real Azure infrastructure. For test writing guidance, syntax, and patterns, read terraform-test.md.
PORCH_NO_TUI=1 ./avm tf-test-integrationIf your change affects module usage or introduces new functionality, add or update examples in the examples/ directory. Test only the pertinent example:
PORCH_NO_TUI=1 AVM_EXAMPLE="<ExampleDir>" ./avm test-examplesWhen running on Windows, distributing tests across multiple Azure subscriptions, or retaining deployed resources for manual validation, see example-test.md for manual local testing of examples (init, plan, apply, idempotency check, and optional destroy).
If documentation changes are needed, edit _header.md. NEVER edit README.md directly -- it is auto-generated and will be overwritten.
This must always be run before committing:
PORCH_NO_TUI=1 ./avm pre-commitgit add .
git commit -m "<type>: <meaningful description>"This must always be run after committing:
PORCH_NO_TUI=1 ./avm pr-checkPush the branch to remote and open a pull request with a meaningful description. Reference any issues that should be closed.
git push -u origin HEADWhen creating the PR, include:
- A summary of the change.
- The issue number(s) the PR closes.
- Any relevant context for reviewers.
- Citing a spec from memory. AVM specs change. Always fetch the current text via
llms.txt. Several spec IDs are easy to mix up (e.g.TFFR4isresponse_export_values,TFFR5isreplace_triggers_refs,TFFR6isresource_types,TFFR7isretry/timeouts). - Reaching for
azurerm.TFFR3requires AzAPI; only fall back toazurermfor genuinely missing capabilities, and document why. - Naming the primary resource anything other than
this(TFRMNFR2), or naming a satellite resourcethis. The primaryazapi_resourceMUST bethis; satellites MUST be named after what they represent (lock,role_assignments,diagnostic_settings, ...). - Exposing
resource_group_name(or any other parent-scope-specific variable) instead ofparent_id(TFRMFR1), or validatingparent_idwith hand-rolled regex/startswith instead ofcan(provider::azapi::parse_resource_id("<ExpectedParentType>", var.parent_id))(TFNFR38). - Creating the parent scope inside the module (e.g. a
Microsoft.Resources/resourceGroupsazapi_resourcefor the resource group the module deploys into) —TFRMFR1forbids this; the consumer supplies an existing scope's ARM ID. - Hard-coding the
typeargument on anazapi_resourceinstead of sourcing it fromvar.resource_types(TFFR6), or forgetting to cascade the relevant subset to each submodule. - Omitting
response_export_values(TFFR4) orreplace_triggers_refs(TFFR5) — both are MUST on everyazapi_resource, even when the value is[]. - Editing
README.md,main.telemetry.tf, orterraform.tfprovider versions by hand. These are generated/enforced — edit_header.md, themodtmsource via mapotf configs, and so on. - Defaulting collection variables to
nullinstead of{}/[]withnullable = false(TFNFR20). - Outputting whole resource objects by default instead of discrete computed attributes (
TFFR2), or missing required outputs (RMFR7). - Implementing an ARM subresource inline in the parent module instead of as a submodule under
modules/<singular-name>/(TFRMNFR1), or declaringcount/for_eachon a submodule's primary resource. - Adding a new interface (locks, diagnostic settings, role assignments, etc.) without re-using
Azure/avm-utl-interfaces/azure. See interfaces.md. - Using the legacy
diagnostic_settingsshape instead of the v2 schema. The utility module'sdiagnostic_settings_v2input is the required entry point. - Omitting
retry,timeouts, orresource_typesfrom anazapi_resource— or failing to cascade them unchanged into submodules. All three are MUST-level AVM interfaces. - Treating
timeoutsas an attribute. It is a block; usedynamic "timeouts"so thenulldefault works. - Skipping
./avm pre-commitbefore commit, or./avm pr-checkafter commit. Both are mandatory.
The canonical source of every AVM rule is the spec index:
- Index of all specs and docs: https://azure.github.io/Azure-Verified-Modules/llms.txt
- Rendered docs site: https://azure.github.io/Azure-Verified-Modules/
Fetch llms.txt first, locate the raw markdown URL for the spec ID you care about, then fetch that markdown. Do not hard-code spec URLs into module source.