Bicep Utility Module Specifications
Contribution / Support
The content below is listed based on the following tags
| # | ID | Title | Severity | Persona | Lifecycle |
|---|---|---|---|---|---|
| 1 | SNFR8 | Module Owner(s) GitHub | MUST | Owner | Initial |
| 2 | SNFR20 | GitHub Teams Only | MUST | Owner | Initial |
| 3 | SNFR9 | AVM & PG Teams GitHub Repo Permissions | MUST | Owner | Initial |
| 4 | SNFR10 | MIT Licensing | MUST | Owner | Initial |
| 5 | SNFR11 | Issues Response Times | MUST | OwnerContributor | BAU |
| 6 | SNFR12 | Versions Supported | MUST | Owner | BAU |
| 7 | SNFR23 | GitHub Repo Labels | MUST | Owner | BAU |
| 8 | BCPNFR15 | AVM Module Issue template file | MUST | Owner | BAU |
โ See Specifications for this category
ID: SNFR8 - Category: Contribution/Support - Module Owner(s) GitHub
A module MUST have at least one owner recorded in the root metadata.json file’s owners array. Record every approved owner using bare individual handles or qualified handles for approved existing teams; children inherit that ownership. Changes require approval from either metadata code-owner team through the metadata review process.
Today this is only Microsoft FTEs, but everyone is welcome to contribute. The module just MUST be owned by a Microsoft FTE (today) so we can enforce and provide the long-term support required by this initiative.
Note
Module owners MUST obtain access through the Entra access package described in SNFR20.
ID: SNFR20 - Category: Contribution/Support - GitHub Teams Only
All GitHub repositories that AVM modules are published from and hosted within MUST only assign GitHub repository permissions to GitHub teams.
Module ownership MUST be recorded separately from access permissions. Maintain owners in the root metadata.json through the metadata review process. Owner access is managed through the access package described below.
There MUST NOT be any GitHub repository permissions assigned to individual users.
Info
Non-FTE / external contributors (subject matter experts that aren’t Microsoft employees) can’t be members of the teams described in this chapter, hence, they won’t gain any extra permissions on AVM repositories, therefore, they need to work in forks.
Bicep
Note
Access management for Bicep module owners is governed centrally through Microsoft Entra. Per-module GitHub teams and parent-team assignments are no longer required.
All Bicep module owners, including primary and secondary owners, MUST request and obtain approval through the Azure Verified Modules (AVM) Module Contributors access package.
Your GitHub account MUST be linked to your corporate identity and be a member of the Azure organization.
Once approved, access is granted through the azure-verified-modules-module-contributors Entra group and the corresponding @Azure/azure-verified-modules-module-contributors GitHub team. This shared access does not replace individual module ownership and review responsibilities. Adding a handle to metadata does not grant this access.
Bicep module owners MUST continue to work in forks of the BRM repository.
CODEOWNERS file
The BRM CODEOWNERS file retains the repository-wide @Azure/azure-verified-modules-tooling-contributors default and its *avm.core.team.tests.ps1 and *.e2eignore overrides. Its /avm/ entry intentionally has no owners, and it has no per-module entries. Change module ownership in the root metadata.json, not by adding CODEOWNERS entries.
The last rule in CODEOWNERS assigns metadata.json changes to @Azure/azure-verified-modules-engineering-owners and @Azure/azure-verified-modules-module-owners. An eligible member of either team can approve a metadata change; both teams are not required. This special rule still applies to module metadata despite the ownerless /avm/ entry.
The Bicep reviewer-routing workflow uses each root module’s owners array to request reviewers for code changes; children inherit those owners. When a module has no owners, it requests @Azure/azure-verified-modules-module-owners and applies an orphaned-module triage label. These are notifications, not code-owner approvals: ordinary Bicep module code changes may be approved and merged by any eligible repository team member under repository rules. Authors cannot approve their own changes. Being listed in metadata does not grant review permission.
For Bicep and Terraform, both metadata code-owner teams must be visible and have repository write access. Access administration and environment approvals remain separate responsibilities.
Tip
For the full onboarding process and ownership handover steps, see the Bicep Owner Contribution Flow.
Terraform
Note
Access management for Terraform repositories is governed centrally through Microsoft Entra. Module owner access is granted via an Entra access package โ it is no longer managed through a per-module GitHub team or the legacy Core Identity entitlement.
All module owners MUST request access via the Azure Verified Modules (AVM) Module Contributors Entra access package:
Once approved, you are added to the azure-verified-modules-module-contributors Entra group, which is the source of truth for who is authorized to own and approve changes on AVM Terraform module repositories. Day-to-day repository access is then granted through this group together with just-in-time (JIT) elevation.
Tip
For the full onboarding process, see the Terraform Prerequisites and Repository Setup pages.
ID: SNFR9 - Category: Contribution/Support - AVM & PG Teams GitHub Repo Permissions
A module owner MUST make the following GitHub teams in the Azure GitHub organization admins on the GitHub repo of the module in question:
Bicep
@Azure/azure-verified-modules-tooling-contributors= AVM Core Team@Azure/bicep-admins= Bicep PG team
Note
These required GitHub teams are already associated to the BRM repository and have the required permissions.
Terraform
@Azure/azure-verified-modules-tooling-contributors= AVM Core Team@Azure/terraform-avm= Terraform PG
Important
Module owners MUST assign these GitHub teams as admins on the GitHub repo of the module in question.
For detailed steps, please follow this guidance.
ID: SNFR10 - Category: Contribution/Support - MIT Licensing
A module MUST be published with the MIT License in the Azure GitHub organization.
ID: SNFR11 - Category: Contribution/Support - Issues Response Times
A module owner MUST respond to logged issues as defined in the support statement. See Module Support for more information.
ID: SNFR12 - Category: Contribution/Support - Versions Supported
Only the latest released version of a module MUST be supported.
For example, if an AVM Resource Module is used in an AVM Pattern Module that was working but now is not. The first step by the AVM Pattern Module owner should be to upgrade to the latest version of the AVM Resource Module test and then if not fixed, troubleshoot and fix forward from the that latest version of the AVM Resource Module onward.
This avoids AVM Module owners from having to maintain multiple major release versions.
ID: SNFR23 - Category: Contribution/Support - GitHub Repo Labels
GitHub repositories where modules are held MUST use the below labels and SHOULD not use any additional labels:
The authoritative AVM standard labels are synced centrally to module repositories. The CSV download and table below are generated publication copies, not the source of truth.
โ AVM Standard GitHub Labels
Download the published CSV copy.
| Name | Description | HEX |
|---|---|---|
| AZD ๐งโ๐ป | These modules are requested/used by the AZD team. | |
| Needs: Attention ๐ | Reply has been added to issue, maintainer to review | |
| Needs: Immediate Attention โผ๏ธ | Immediate attention of module owner / AVM team is needed | |
| Needs: Author Feedback ๐ | Awaiting feedback from the issue/PR author | |
| Needs: External Changes โ๏ธ | When an issue/PR requires changes that are outside of the control of the module. e.g. to an RP. | |
| Needs: More Evidence โ | We are looking for more evidence to make a decision on this | |
| Needs: Triage ๐ | Maintainers need to triage still | |
| Needs: Module Owner ๐ฃ | In the AVM repository: this module needs an owner to develop or maintain it. In the BRM repository: the module owner needs to review a PR. | |
| Needs: Module Contributor ๐ฃ | This module needs secondary owner(s) or contributor(s) to develop or maintain it | |
| Needs: Core Team ๐งโโ๏ธ | This item needs the AVM Core Team to review it | |
| Status: Awaiting Release To Be Cut โ๏ธ | This is fixed in the main branch but not in the latest release, will be fixed with next release cut | |
| Status: Do Not Merge โ | Do not merge PRs with this label attached as they are not ready or aligned to future direction etc. | |
| Status: External Contribution ๐ | This is being worked on by someone outside of the AVM module owners/contributors or AVM core team | |
| Status: Fixed โ | Auto label applied when issue fixed by merged PR | |
| Status: Help Wanted ๐ | Extra attention is needed | |
| Status: In Triage ๐ | Picked up for triaging by an AVM core team member | |
| Status: In PR ๐ | This is when an issue is due to be fixed in an open PR | |
| Status: Invalid โ | This doesn't seem right | |
| Status: Long Term โณ | We will do it, but will take a longer amount of time due to complexity/priorities | |
| Status: No Recent Activity ๐ค | When an issue/PR has not been modified for X amount of days | |
| Status: Won't Fix ๐ | This will not be worked on | |
| Status: Owners Identified ๐ค | This module has its owners identified | |
| Status: Module Available ๐ข | The module is published | |
| Status: Module Deprecated ๐ด | This is a request to deprecate a module | |
| Status: Module Orphaned ๐ก | The module has no owner and is therefore orphaned at this time | |
| Status: Ready For Repository Creation ๐ | This module is approved and the owner is ready for the repository to be created (Terraform) | |
| Status: Repository Created ๐ | This module has had it's repository created and configured ready for owner contribution (Terraform) | |
| Status: Response Overdue ๐ฉ | When an issue/PR has not been responded to for X amount of days | |
| Status: Looking For Assistance ๐ฆ | This item is looking for anyone to help develop the code and submit a PR for resolution | |
| Type: Bug ๐ | Something isn't working | |
| Type: CI ๐ | This issue is related to the AVM CI | |
| Type: Documentation ๐ | Improvements or additions to documentation | |
| Type: Duplicate ๐คฒ | This issue or pull request already exists | |
| Type: Feature Request โ | New feature or request | |
| Type: Hygiene ๐งน | things related to testing, issue triage etc. | |
| Type: New Module Proposal ๐ก | A new module for AVM is being proposed | |
| Type: Question/Feedback ๐โโ๏ธ | Further information is requested or just some feedback | |
| Type: Security Bug ๐ | This is a security bug | |
| Type: AVM ๐ ฐ๏ธ โ๏ธ โ๏ธ | This is an AVM related issue | |
| Language: Terraform ๐ | This is related to the Terraform IaC language | |
| Language: Bicep ๐ช | This is related to the Bicep IaC language | |
| Class: Resource Module ๐ฆ | This is a resource module | |
| Class: Pattern Module ๐ฆ | This is a pattern module | |
| Class: Utility Module ๐ฆ | This is a utility module | |
| Class: Child Module ๐ฆ | This is a child module | |
ID: BCPNFR15 - Category: Contribution/Support - AVM Module Issue template file
The module-name-dropdown in the BRM AVM Module Issue template MUST list top-level Bicep modules with Available or Orphaned status, sorted by module class and name. Proposed, deprecated, and child modules are excluded.
The module list sync workflow compares the dropdown with the published module catalog and updates it through a verified, auto-merged bot pull request. Module owners maintain root metadata and the required publication or deprecation evidence instead of editing the dropdown directly.
Telemetry
The content below is listed based on the following tags
| # | ID | Title | Severity | Persona | Lifecycle |
|---|---|---|---|---|---|
| 1 | SFR3 | Deployment/Usage Telemetry | MUST | Owner | Initial |
| 2 | SFR4 | Telemetry Enablement Flexibility | MUST | Owner | Initial |
| 3 | BCPFR4 | Telemetry Enablement | MUST | OwnerContributor | BAU |
โ See Specifications for this category
ID: SFR3 - Category: Telemetry - Deployment/Usage Telemetry
Modules MUST provide the capability to collect deployment/usage telemetry as detailed in Telemetry further.
To highlight that AVM modules use telemetry, an information notice MUST be included in the footer of each module’s README.md file with the below content. See the telemetry guidance for more details.
Telemetry Information Notice
Note
The following information notice is automatically added at the bottom of the README.md file of the module when
- Bicep: Using the
utilities/tools/Set-AVMModule.ps1utility - Terraform: Running
avm pre-commitwith the note and header## Data Collectionplaced in the module’s_footer.mdbeforehand
### Data Collection
The software may collect information about you and your use of the software and send it to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may turn off the telemetry as described in the [repository](https://aka.ms/avm/telemetry). There are also some features in the software that may enable you and Microsoft to collect data from users of your applications. If you use these features, you must comply with applicable law, including providing appropriate notices to users of your applications together with a copy of Microsoft's privacy statement. Our privacy statement is located at <https://go.microsoft.com/fwlink/?LinkID=824704>. You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices.Module Class Applicability
This specification applies to all AVM module classes (resource, pattern, utility), however, in case of utility modules, telemetry collection MUST only be added when the utility module deploys any resources (e.g., a deployment script resource). If the utility module does not deploy any resources, telemetry collection MUST NOT be added.
Bicep
Important
Published CSV files in the AVM Central Repo (Azure/Azure-Verified-Modules) remain available for consumers and checks that look up assigned telemetry prefixes. To see their formatted content with additional information, visit the AVM Module Indexes page.
Record the assigned current prefix in telemetryIdPrefix in the module’s metadata.json, including a child’s own file when applicable. Place it alongside the module’s main.bicep before compiling, and read only that value in Bicep with loadJsonContent('metadata.json', 'telemetryIdPrefix') instead of hardcoding it. Retain previous identifiers in alternativeTelemetryIdPrefixes when assigning a new current prefix. Corrections follow the metadata review process; assignment of a new identifier requires the AVM core team.
Assigned values are also published in the Resource Module, Pattern Module, and Utility Module indexes. Ask the AVM core team to resolve any discrepancy with metadata rather than changing an identifier without approval.
The Bicep ARM deployment name used for telemetry MUST follow <telemetryIdPrefix>.<version>.<uniqueness> and MUST be no longer than 64 characters, as shown in BCPFR4.
<telemetryIdPrefix>is the current value in the module’smetadata.jsonand MUST use46d3xbcp.<res|ptn|utl>.<seven lowercase hexadecimal characters>(20 characters).<version>is the module version token with periods replaced by hyphens.<uniqueness>is a four-character value derived fromuniqueStringand the deployment context.
Do not truncate an identifier or version to meet the 64-character limit. When replacing a previous prefix, retain it and any earlier prefixes in alternativeTelemetryIdPrefixes in the same module’s metadata.json for historical reporting.
Tip
Terraform: Terraform uses a metadata-backed empty Azure deployment generated by Avm.Authoring; it does not require a separate telemetry provider.
General: See the language specific contribution guides for detailed guidance and sample code to use in AVM modules to achieve this requirement.
Terraform
Instrumented Terraform roots and child modules MUST obtain their assigned 46d3xtrf telemetryIdPrefix and canonicalType from their own metadata.json. The prefix MUST NOT be reconstructed from a module source or hardcoded in generated Terraform. Current Terraform prefixes MUST be 46d3xtrf.<res|ptn|utl>.<seven lowercase hexadecimal characters> (20 characters). Previously deployed identifiers, including descriptive Terraform prefixes up to 59 characters, MAY be retained in alternativeTelemetryIdPrefixes with the same ecosystem and module kind; generated deployments use only the current prefix. Children without a telemetry prefix, including telemetry-free helpers and utilities, MUST NOT create a telemetry deployment.
Avm.Authoring MUST generate and maintain main.telemetry.tf rather than requiring module authors to maintain telemetry resources. When var.enable_telemetry is true, this file MUST create an empty, incremental Microsoft.Resources/deployments@2025-04-01 deployment using azapi_resource at the active subscription scope. The deployment location follows SFR4.
The deployment name is the reporting payload. It MUST have the form <telemetryIdPrefix>.<version>.<source>.<instance> and MUST NOT exceed 64 characters:
| Segment | Value |
|---|---|
telemetryIdPrefix | The fixed metadata identifier, which the module catalog maps to its canonical type. |
version | The installed full version from the Terraform modules manifest entry matching path.module, with periods replaced by hyphens. Use 0-0-0 when a version is unavailable. |
source | One character derived from the manifest source: t for Terraform Registry, o for OpenTofu Registry, g for Git, or x for other sources. Never include a raw source path. |
instance | The first four lowercase hex characters of a hash of the stable provider-free terraform_data.telemetry instance ID. |
The generated resource MUST reject an invalid or overlong version token rather than truncate reporting data. It MUST NOT send telemetry tags; Azure deployment events provide the time, subscription, and caller context. An output in the empty template MUST change with plantimestamp() on every normal plan solely to force an in-place deployment write, including on otherwise no-op applies; this output is not reporting data. Refresh-only operations do not create a telemetry write.
The deployment MUST fail the apply if Azure rejects it, unless the consumer disables telemetry with enable_telemetry = false. With telemetry enabled, the deployment identity needs Microsoft.Resources/deployments/read, Microsoft.Resources/deployments/write, and Microsoft.Resources/deployments/delete at the active subscription scope. See the telemetry guidance for the opt-out and required location input.
The generated configuration MUST NOT require the modtm provider or add per-resource AzAPI telemetry headers. During the supported migration window, existing modtm_telemetry.telemetry and telemetry-only random_uuid.telemetry state MUST be forgotten with declarative removed blocks using destroy = false, without destroying either object. Terraform still needs the old providers available for one final initialization when an existing state refers to them; new installations and subsequent plans do not require modtm.
ID: SFR4 - Category: Telemetry - Telemetry Enablement Flexibility
The telemetry collection MUST be on/enabled by default, however module consumers MUST be allowed to disable it by setting the below parameter/variable value to false:
- Bicep:
enableTelemetry - Terraform:
enable_telemetry
Note
Whenever a module references AVM modules that implement the telemetry parameter (e.g., a pattern module that uses AVM resource modules), the telemetry parameter value MUST be passed through to these modules. This is necessary to ensure a consumer can reliably enable & disable the telemetry feature for all used modules.
This general specification can be modified for some use-cases, that are language specific:
Bicep
For cross-references in resource modules, the spec BCPFR7 also applies.
Terraform
Every Terraform module root MUST declare a string input named location. The only root exception is a utility module that deploys no Azure resources. Every local child module that deploys Azure resources MUST also declare location, whether or not it reports its own telemetry; child modules that deploy no Azure resources are exempt. This requirement applies even when the resources themselves are global or scope-based, because the subscription-scoped telemetry deployment needs an Azure region.
The generated telemetry deployment MUST use var.location. Consumers of modules with a required location input must supply a region available in their cloud, including sovereign clouds.
Local module calls MUST pass the parent’s var.location to children that require location when the call has no authored location argument. A call that already supplies a location for an individual resource or region MUST retain that value. A pattern with optional resource-specific locations can pass the corresponding override to each child when supplied and fall back to the parent’s var.location when omitted; overrides for different children remain independent. Instrumented children MUST also receive the parent’s enable_telemetry value so a parent opting out cannot enable child telemetry. Example calls MUST expose and forward a missing required location and the opt-out where supported, without replacing authored per-item locations.
ID: BCPFR4 - Category: Composition - Telemetry Enablement
To comply with specifications outlined in SFR3 & SFR4 you MUST incorporate the following code snippet into your modules. Place this code sample in the “top level” main.bicep file; it is not necessary to include it in any nested Bicep files (child modules), unless they are marked for direct publishing (Ref Child module publishing).
Before compiling, ensure the module’s metadata.json exists alongside its main.bicep and contains its assigned telemetryIdPrefix. The example uses the jsonPath argument of loadJsonContent to load only that value. Do not load the full metadata object: that embeds unrelated values, including owners and descriptions, in the compiled ARM template. If an assigned prefix is missing or conflicts with the module index, follow the module metadata guidance rather than hardcoding or inventing one.
The current Bicep telemetryIdPrefix MUST use 46d3xbcp.<res|ptn|utl>.<seven lowercase hexadecimal characters> (20 characters). When replacing a previously assigned identifier, retain it and any earlier prefixes in alternativeTelemetryIdPrefixes in the same metadata.json; the deployment uses only the current prefix. Check that the complete deployment name fits within 64 characters without truncation.
After changing telemetryIdPrefix, regenerate main.json with Set-AVMModule.ps1 without -SkipBuild. Consumers deploying the compiled main.json do not need metadata.json.
@description('Optional. Location for all resources.')
param location string = resourceGroup().location
@description('Optional. Enable/Disable usage telemetry for module.')
param enableTelemetry bool = true
var telemetryIdPrefix = loadJsonContent('metadata.json', 'telemetryIdPrefix')
#disable-next-line no-deployments-resources
resource avmTelemetry 'Microsoft.Resources/deployments@2025-04-01' = if (enableTelemetry) {
name: '${telemetryIdPrefix}.${replace('-..--..-', '.', '-')}.${substring(uniqueString(deployment().name, location), 0, 4)}'
properties: {
mode: 'Incremental'
template: {
'$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#'
contentVersion: '1.0.0.0'
resources: []
outputs: {
telemetry: {
type: 'String'
value: 'For more information, see https://aka.ms/avm/TelemetryInfo'
}
}
}
}
}Naming / Composition
The content below is listed based on the following tags
| # | ID | Title | Severity | Persona | Lifecycle |
|---|---|---|---|---|---|
| 1 | SFR1 | Preview Services | MUST | Owner | BAU |
| 2 | SFR2 | WAF Aligned | SHOULD | Owner | BAU |
| 3 | SNFR25 | Resource Naming | MUST | Owner | Initial |
| 4 | UMNFR1 | Module Naming | MUST | Owner | Initial |
| 5 | BCPFR1 | Cross-Referencing Modules | MAY | OwnerContributor | BAU |
| 6 | BCPNFR19 | User-defined types - Naming | MUST | OwnerContributor | BAU |
| 7 | BCPNFR23 | Module composition | MUST | OwnerContributor | BAU |
| 8 | BCPNFR24 | Deterministic Deployment Names | MUST | OwnerContributor | BAU |
| 9 | BCPNFR14 | Versioning | MUST | OwnerContributor | BAU |
โ See Specifications for this category
ID: SFR1 - Category: Composition - Preview Services
Modules MAY create/adopt public preview services and features at their discretion.
Preview API versions MAY be used when:
- The resource/service/feature is GA but the only API version available for the GA resource/service/feature is a preview version
- For example, Diagnostic Settings (
Microsoft.Insights/diagnosticSettings) the latest version of the API available with GA features, like Category Groups etc., is2021-05-01-preview - Otherwise the latest “non-preview” version of the API SHOULD be used
- For example, Diagnostic Settings (
Preview services and features, SHOULD NOT be promoted and exposed, unless they are supported by the respective PG, and it’s documented publicly.
However, they MAY be exposed at the module owners discretion, but the following rules MUST be followed:
- The description of each of the parameters/variables used for the preview service/feature MUST start with:
- “THIS IS A <PARAMETER/VARIABLE> USED FOR A PREVIEW SERVICE/FEATURE, MICROSOFT MAY NOT PROVIDE SUPPORT FOR THIS, PLEASE CHECK THE PRODUCT DOCS FOR CLARIFICATION”
ID: SFR2 - Category: Composition - WAF Aligned
Modules SHOULD set defaults in input parameters/variables to align to high priority/impact/severity recommendations, where appropriate and applicable, in the following frameworks and resources:
- Well-Architected Framework (WAF)
- Reliability Hub
- Azure Proactive Resiliency Library (APRL)
- Only Product Group (PG) verified
- Microsoft Defender for Cloud (MDFC)
They SHOULD NOT align to these recommendations when it requires an external dependency/resource to be deployed and configured and then associated to the resources in the module.
Alignment SHOULD prioritize best-practices and security over cost optimization, but MUST allow for these to be overridden by a module consumer easily, if desired.
Tip
Read the FAQ of What does AVM mean by “WAF Aligned”? for more detailed information and examples.
ID: SNFR25 - Category: Composition - Resource Naming
Module owners MUST set the default resource name prefix for child, extension, and interface resources to the associated abbreviation for the specific resource as documented in the following CAF article Abbreviation examples for Azure resources, if specified and documented. This reduces the amount of input values a module consumer MUST provide by default when using the module.
For example, a Private Endpoint that is being deployed as part of a resource module, via the mandatory interfaces, MUST set the Private Endpoint’s default name to begin with the prefix of pep-.
Module owners MUST also provide the ability for these default names, including the prefixes, to be overridden via a parameter/variable if the consumer wishes to.
Furthermore, as per RMNFR2, Resource Modules MUST not have a default value specified for the name of the primary resource and therefore the name MUST be provided and specified by the module consumer.
The name provided MAY be used by the module owner to generate the rest of the default name for child, extension, and interface resources if they wish to. For example, for the Private Endpoint mentioned above, the full default name that can be overridden by the consumer, MAY be pep-<primary-resource-name>.
Tip
If the resource does not have a documented abbreviation in Abbreviation examples for Azure resources, then the module owner is free to use a sensible prefix instead.
ID: UMNFR1 - Category: Naming - Module Naming
Utility Modules MUST follow the below naming conventions (all lower case).
Important
The module’s approved name is captured in the module proposal issue. The related module index page and CSV file remain published lookup references.
Module owners must use the name approved in the module proposal, not construct a new one. If it differs from the index, confirm the correction with the AVM core team.
Correct descriptive fields through the metadata review process. Changing moduleDisplayName does not rename the module or change its repository path.
Bicep Utility Module Naming
- Naming convention:
avm/utl/<hyphenated grouping/category name>/<hyphenated utility module name> - Example:
avm/utl/general/get-environmentoravm/utl/types/avm-common-types - Segments:
utldefines this as a utility module<hyphenated grouping/category name>is a hierarchical grouping of utility modules by category, with each word separated by dashes, such as:generalortypes<hyphenated utility module name>is a term describing the module’s function, with each word separated by dashes, e.g.,get-environment= to get environmental details;avm-common-types= to use common types.
Terraform Utility Module Naming
- Naming convention:
avm-utl-<utility module name>(Module name for registry)terraform-<provider>-avm-utl-<utility module name>(GitHub repository name to meet registry naming requirements)
- Example:
avm-utl-sku-finderoravm-utl-naming - Segments:
<provider>is a legacy requirement of the Terraform registry. For AVM Terraform utility modules this MUST be set toazure(for exampleAzure/avm-utl-naming/azure). Older utility modules may still use theazurermorazureadsegments. These segments are names only and do not permit use of the AzureRM provider; TFFR3 still requires every module to be built with AzAPI.utldefines this as a utility module<utility module name>is a term describing the module’s function, e.g.,sku-finder= to find available SKUs;naming= to handle naming conventions.
ID: BCPFR1 - Category: Composition - Cross-Referencing Modules
Module owners MAY cross-reference other modules to build either Resource or Pattern modules.
However, they MUST be referenced only by a public registry reference to a pinned version e.g. br/public:avm/[res|ptn|utl]/<publishedModuleName>:>version<. They MUST NOT use local parent path references to a module e.g. ../../xxx/yyy.bicep.
The only exception to this rule are child modules as documented in BCPFR6.
Modules MUST NOT contain references to non-AVM modules.
ID: BCPNFR19 - User-defined types - Naming
User-defined types (UDTs) MUST always end with the suffix (...)Type to make them obvious to users. In addition it is recommended to extend the suffix to (...)OutputType if a UDT is exclusively used for outputs.
type subnet = { ... } // Wrong
type subnetType = { ... } // Correct
type subnetOutputType = { ... } // Correct, if used only for outputsSince User-defined types (UDTs) MUST always be singular as per BCPNFR18, their naming should reflect this and also be singular.
type subnetsType = { ... } // Wrong
type subnetType = { ... } // CorrectID: BCPNFR23 - Category: Composition
Each Bicep AVM module that lives within the Azure/bicep-registry-modules (BRM) repository in the avm directory MUST have the following directories and files:
/tests- (for unit tests and additional E2E/integration if required - e.g. Pester etc.)/e2e- (all examples must deploy successfully - these will be used to automatically generate the examples in the README.md for the module)
/src- (for scripts and other files - e.g., scripts used by the template)exampleFile.ps1
/modules- (for sub-modules only if used and NOT children of the primary resource - e.g. RBAC role assignments)exampleTemplate.bicep
/main.bicep(AVM Module main.bicepfile and entry point/orchestration module)/main.json(auto generated and what is published to the MCR via BRM)/version.json(BRM requirement)/README.md(auto generated AVM Module documentation)/CHANGELOG.md(manually maintained changelog file with one entry per published version)
Directory and File Structure Example
/ Root of Azure/bicep-registry-modules
โ
โโโโavm
โ โโโโptn
โ โ โโโโapptiervmss
โ โ โ main.bicep
โ โ โ main.json
โ โ โ README.md
โ โ โ CHANGELOG.md
โ โ โ version.json
โ โ โโโโsrc (optional)
โ โ โ โโโโGet-Cake.ps1
โ โ โ โโโโFind-Waldo.ps1
โ โ โโโโmodules (optional)
โ โ โ โโโโhelper.bicep
โ โ โ โโโโrole-assignment.bicep
โ โ โโโโtests
โ โ โโโโunit (optional)
โ โ โโโโe2e
โ โ โโโโdefaults
โ โ โโโโwaf-aligned
โ โ โโโโmax
โ โ
โ โโโโres
โ โโโโcompute
โ โโโโvirtual-machine
โ โ main.bicep
โ โ main.json
โ โ README.md
โ โ CHANGELOG.md
โ โ version.json
โ โโโโsrc (optional)
โ โ โโโโSet-Bug.ps1
โ โ โโโโInvoke-Promotion.ps1
โ โโโโmodules (optional)
โ โ โโโโhelper.bicep
โ โ โโโโrole-assignment.bicep
โ โโโโtests
โ โโโโunit (optional)
โ โโโโe2e
โ โโโโdefaults
โ โโโโwaf-aligned
โ โโโโmax
โโโโother repo dirs...
โโโโother repo files...ID: BCPNFR24 - Category: Naming/Composition - Deterministic Deployment Names
When a module references child, utility, or other modules, the deployment name MUST be deterministic. This means the deployment name must produce the same value for the same set of inputs across repeated deployments.
Why deterministic?
Azure Resource Manager has an 800-deployment limit per scope (resource group, subscription, management group, tenant). Non-deterministic names (e.g., those incorporating timestamps or utcNow()) create a new deployment object on every run, which can lead to this limit being reached over time.
While an automatic cleanup process exists for resource group and subscription scopes, it can take some time to take effect. Due to eventual consistency in the backend, the deployment count may not reflect the cleanup immediately, which can lead to failed deployments even when the actual number of deployments is below the 800 limit. Additionally, automatic cleanup does not apply to management group or tenant scopes.
We are actively working with the product team to enhance the cleanup process. In the meantime, deterministic deployment names provide a reliable way to keep deployment counts stable by overwriting previous deployment objects rather than creating new ones.
Deterministic deployment names cause Azure to overwrite the previous deployment object, keeping the deployment count stable regardless of how many times the module is deployed.
Requirement
Module owners MUST construct deployment names for referenced modules using uniqueString() seeded with the parent resource’s ID (<parentResource>.id) and location, rather than deployment().name, subscription().id, resourceGroup().id, utcNow(), or other non-deterministic or scope-level values.
The deployment name MUST follow the pattern:
'${uniqueString(<parentResource>.id, location)}-<ChildModuleDescriptor>-${index}'Where:
| Segment | Description |
|---|---|
uniqueString(<parentResource>.id, location) | A deterministic hash derived from the parent resource’s resource ID and deployment location. This is both unique per resource instance and stable across deployments. |
<ChildModuleDescriptor> | A short, human-readable label identifying the child module being deployed (e.g., DB, Subnet, FederatedIdentityCred). |
${index} | The loop index variable, included when deploying in a loop. Omit for single (non-looped) deployments. |
location parameter
If location is not available, for example when deploying a global resource that does not have a location property, it is acceptable to omit it. However, the <parentResource>.id MUST always be included as the primary seed for uniqueString.
Why parent resource ID?
Using the parent resource’s ID as the uniqueString seed provides two critical properties:
- Deterministic โ the same parent resource always produces the same hash, so repeated deployments overwrite rather than accumulate.
- Collision-free โ different parent resource instances produce different hashes, so deploying multiple instances of the same module type within the same scope does not cause naming collisions.
Why not subscription().id and resourceGroup().id separately?
The parent resource’s ID (e.g., /subscriptions/.../resourceGroups/.../providers/.../resourceName) already contains the subscription ID and resource group ID as segments. Using <parentResource>.id as a single input to uniqueString captures all of this context in one value, keeping the code concise and readable rather than passing multiple scope-level values separately.
Supporting multiple deployments of the same module at the same scope
A common scenario is deploying the same module type more than once within the same scope โ for example, two different SQL servers each with their own set of databases, or two user-assigned identities each with their own federated credentials. Because the parent resource ID is unique per resource instance, the resulting deployment names will differ even when the child module type and index are identical. This ensures that parallel deployments of the same module at the same scope do not collide.
Other approaches fail on one or both of these properties:
| Approach | Deterministic? | Collision-free? | Issue |
|---|---|---|---|
deployment().name | โ | โ | Changes every deployment; hits 800-limit |
utcNow() / timestamps | โ | โ | Changes every deployment; hits 800-limit |
subscription().id + resourceGroup().id | โ | โ | Same hash for all resources in the same RG; collisions when deploying multiple instances |
<parentResource>.id, location | โ | โ | Recommended โ stable and unique per instance |
Examples
Example 1: Single child module deployment
resource server 'Microsoft.Sql/servers@2023-05-01-preview' = { ... }
module server_database 'database/main.bicep' = {
name: '${uniqueString(server.id, location)}-Sql-DB'
params: {
serverName: server.name
(...)
}
}Example 2: Child module deployment in a loop
resource server 'Microsoft.Sql/servers@2023-05-01-preview' = { ... }
module server_databases 'database/main.bicep' = [for (database, index) in (databases ?? []): {
name: '${uniqueString(server.id, location)}-Sql-DB-${index}'
params: {
serverName: server.name
(...)
}
}]ID: BCPNFR14 - Category: Composition - Versioning
To meet SNFR17 and depending on the changes you make, you may need to bump the version in the version.json file.
{
"$schema": "https://aka.ms/bicep-registry-module-version-file-schema#",
"version": "0.1"
}
The version value is in the form of MAJOR.MINOR. The PATCH version will be incremented by the CI automatically when publishing the module to the Public Bicep Registry once the corresponding pull request is merged. Therefore, contributions that would only require an update of the patch version, can keep the version.json file intact.
For example, the version value should be:
0.1for new modules, so that they can be released asv0.1.0.1.0once the module owner signs off the module is stable enough for it’s first Major release ofv1.0.0.0.xfor all feature updates between the first releasev0.1.0and the first Major release ofv1.0.0.
Inputs / Outputs
The content below is listed based on the following tags
| # | ID | Title | Severity | Persona | Lifecycle |
|---|---|---|---|---|---|
| 1 | SNFR14 | Data Types | SHOULD | OwnerContributor | BAU |
| 2 | SNFR22 | Parameters/Variables for Resource IDs | MUST | OwnerContributor | BAU |
| 3 | SNFR26 | Output - Parameters - Decorators | MUST | OwnerContributor | BAU |
| 4 | BCPNFR1 | Complex data types - General | MUST | OwnerContributor | BAU |
| 5 | BCPNFR9 | Inputs - Decorators | MUST | OwnerContributor | BAU |
| 6 | BCPNFR18 | User-defined types - Specification | MUST | OwnerContributor | BAU |
| 7 | BCPNFR19 | User-defined types - Naming | MUST | OwnerContributor | BAU |
| 8 | BCPNFR20 | User-defined types - Export | MUST | OwnerContributor | BAU |
| 9 | BCPNFR21 | User-defined types - Decorators | MUST | OwnerContributor | BAU |
| 10 | BCPNFR7 | Parameter Requirement Types | MUST | OwnerContributor | BAU |
โ See Specifications for this category
ID: SNFR14 - Category: Inputs - Data Types
A module SHOULD use either: simple data types. e.g., string, int, bool.
OR
Complex data types (objects, arrays, maps) when the language-compliant schema is defined.
ID: SNFR22 - Category: Inputs - Parameters/Variables for Resource IDs
A module parameter/variable that requires a full Azure Resource ID as an input value, e.g. /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.KeyVault/vaults/{keyVaultName}, SHOULD contain ResourceId/resource_id in its parameter/variable name when that parameter/variable is part of a user-defined type. This assists users in knowing what value to provide at a glance of the parameter/variable name.
Example for the property workspaceId for the Diagnostic Settings resource in a user-defined type: in Bicep its parameter name should be workspaceResourceId and the variable name in Terraform should be workspace_resource_id.
In that user-defined context, workspaceId is not descriptive enough and is ambiguous as to which ID is required to be input.
Special considerations for Bicep
If the property is nested in a parameter and you opt for a resource-derived type (that is, a schema defined by the resource provider), this requirement does not apply. We do however recommend to use a user-defined type whenever these cases occur to increase the module’s usability.
Example for the property subnetArmId of the Cognitive Service’s property networkInjections:
If using a user-defined type, you may define a type for the networkInjections parameter like
param networkInjections networkInjectionType?
@export()
type networkInjectionType = {
subnetResourceId: string
// (...)
}
resource cognitiveService 'Microsoft.CognitiveServices/accounts@2025-06-01' = {
// (...)
properties: {
// (...)
networkInjections: [{
subnetArmId: networkInjections.?subnetResourceId
// (...)
}]
}
}or a resource-derived type like
param networkInjections resourceInput<'Microsoft.CognitiveServices/accounts@2025-06-01'>.properties.networkInjections
resource cognitiveService 'Microsoft.CognitiveServices/accounts@2025-06-01' = {
// (...)
properties: {
// (...)
networkInjections: networkInjections
}
}ID: SNFR26 - Output-Parameters - Decorators
Output parameters MUST implement:
- Decorators in Bicep such as
description&secure(if sensitive) - Arguments in Terraform such as
description&sensitive(if sensitive)
@description('The resourceId of your resource.')
output sampleResourceId string = sampleResource.id
@description('The key of your resource.')
@secure()
output sampleResourceKey string = sampleResource.key# Resource output
output "foo" {
description = "MyResource foo attribute"
value = azapi_resource.myresource.output.properties.foo
}
# Output of a sensitive attribute
output "bar" {
description = "MyResource bar attribute"
value = azapi_resource.myresource.output.properties.bar
sensitive = true
}ID: BCPNFR1 - Category: Inputs - Complex data types - General
To simplify the consumption experience for module consumers when interacting with complex data types input parameters, mainly objects and arrays, the Bicep features of Resource-Derived Types or User-Defined Types MUST be used and declared.
Tip
User-Defined Types are GA in Bicep as of version v0.21.1, Resource-Derived Types are GA as of version v0.34.1, please ensure you have this version(s) installed as a minimum.
Resource-Derived Types and User-Defined Types allow intellisense support in supported IDEs (e.g. Visual Studio Code) for complex input parameters using objects and array of objects.
v0.x Exemption
While we allow the release of major versions, starting with v1.0.0, retrofitting Resource-Derived Types and User-Defined Types for all modules will take a considerable amount of time.
Therefore, the addition of these features is currently NOT mandated/enforced. However, all modules MUST implement Resource-Derived Types and User-Defined Types prior to the release of their v1.0.0 version.
ID: BCPNFR9 - Inputs - Decorators
Similar to BCPNFR21, input parameters MUST implement decorators such as description & secure (if sensitive).
Further, input parameters SHOULD implement decorators like allowed, minValue, maxValue, minLength & maxLength (and others if available) as they have a big positive impact on the module’s usability.
@description('Optional. The threshold of your resource.')
@minValue(1)
@maxValue(10)
param threshold: int?
@description('Required. The SKU of your resource.')
@allowed([
'Basic'
'Premium'
'Standard'
])
param sku stringID: BCPNFR18 - User-defined types - Specification
User-defined types (UDTs) MUST always be singular and non-nullable. The configuration of either should instead be done directly at the parameter or output that uses the type.
For example, instead of
param subnets subnetsType
type subnetsType = { ... }[]?the type should be defined like
param subnets subnetType[]?
type subnetType = { ... }The primary reason for this requirement is clarity. If not defined directly at the parameter or output, a user would always be required to check the type to understand how e.g., a parameter is expected.
ID: BCPNFR19 - User-defined types - Naming
User-defined types (UDTs) MUST always end with the suffix (...)Type to make them obvious to users. In addition it is recommended to extend the suffix to (...)OutputType if a UDT is exclusively used for outputs.
type subnet = { ... } // Wrong
type subnetType = { ... } // Correct
type subnetOutputType = { ... } // Correct, if used only for outputsSince User-defined types (UDTs) MUST always be singular as per BCPNFR18, their naming should reflect this and also be singular.
type subnetsType = { ... } // Wrong
type subnetType = { ... } // CorrectID: BCPNFR20 - User-defined types - Export
User-defined types (UDTs) SHOULD always be exported via the @export() annotation in every template they’re implemented in.
@export()
type subnetType = { ... }Doing so has the benefit that other (e.g., parent) modules can import them and as such reduce code duplication. Also, if the module itself is published, users of the Public Bicep Registry can import the types independently of the module itself. One example where this can be useful is a pattern module that may re-use the same interface when referencing a module from the registry.
ID: BCPNFR21 - User-defined types - Decorators
Similar to BCPNFR9, User-defined types (UDTs) MUST implement decorators such as description & secure (if sensitive). This is true for every property of the UDT, as well as the UDT itself.
Further, User-defined types SHOULD implement decorators like allowed, minValue, maxValue, minLength & maxLength (and others if available) as they have a big positive impact on the module’s usability.
@description('My type''s description.')
type myType = {
@description('Optional. The threshold of your resource.')
@minValue(1)
@maxValue(10)
threshold: int?
@description('Required. The SKU of your resource.')
sku: ('Basic' | 'Premium' | 'Standard')
}ID: BCPNFR7 - Category: Inputs - Parameter Requirement Types
Modules will have lots of parameters that will differ in their requirement type (required, optional, etc.). To help consumers understand what each parameter’s requirement type is, module owners MUST add the requirement type to the beginning of each parameter’s description. Below are the requirement types with a definition and example for the description decorator:
| Parameter Requirement Type | Definition | Example Description Decorator |
|---|---|---|
| Required | The parameter value must be provided. The parameter does not have a default value and hence the module expects and requires an input. | @description('Required. <PARAMETER DESCRIPTION HERE...>') |
| Conditional | The parameter value can be optional or required based on a condition, mostly based on the value provided to other parameters. Should contain a sentence starting with ‘Required if (…).’ to explain the condition. | @description('Conditional. <PARAMETER DESCRIPTION HERE...>') |
| Optional | The parameter value is not mandatory. The module provides a default value for the parameter. | @description('Optional. <PARAMETER DESCRIPTION HERE...>') |
| Generated | The parameter value is generated within the module and should not be specified as input in most cases. A common example of this is the utcNow() function that is only supported as the input for a parameter value, and not inside a variable. | @description('Generated. <PARAMETER DESCRIPTION HERE...>') |
Testing
The content below is listed based on the following tags
| # | ID | Title | Severity | Persona | Lifecycle |
|---|---|---|---|---|---|
| 1 | SNFR1 | Prescribed Tests | MUST | OwnerContributor | BAU |
| 2 | SNFR2 | E2E Testing | MUST | OwnerContributor | BAU |
| 3 | SNFR3 | AVM Compliance Tests | MUST | OwnerContributor | Initial |
| 4 | SNFR4 | Unit Tests | SHOULD | OwnerContributor | BAU |
| 5 | SNFR5 | Upgrade Tests | SHOULD | OwnerContributor | BAU |
| 6 | SNFR6 | Static Analysis/Linting Tests | MUST | OwnerContributor | BAU |
| 7 | SNFR7 | Idempotency Tests | MUST | OwnerContributor | BAU |
| 8 | BCPNFR10 | Test Bicep File Naming | MUST | OwnerContributor | BAU |
| 9 | BCPNFR11 | Test Tooling | MUST | OwnerContributor | BAU |
| 10 | BCPNFR12 | Deployment Test Naming | MUST | OwnerContributor | BAU |
| 11 | BCPNFR13 | Test file metadata | MUST | OwnerContributor | BAU |
| 12 | BCPNFR16 | Post-deployment tests | MUST | OwnerContributor | BAU |
| 13 | BCPRMNFR1 | Expected Test Directories | MUST | OwnerContributor | BAU |
โ See Specifications for this category
ID: SNFR1 - Category: Testing - Prescribed Tests
Modules MUST use the prescribed tooling and testing frameworks defined in the language specific specs.
ID: SNFR2 - Category: Testing - E2E Testing
Modules MUST implement end-to-end (deployment) testing that create actual resources to validate that module deployments work. In Bicep tests are sourced from the directories in /tests/e2e. In Terraform, these are in /examples.
Each test MUST run and complete without user inputs successfully, for automation purposes.
Each test MUST also destroy/clean-up its resources and test dependencies following a run.
Tip
Resources/Dependencies Required for E2E Tests
It is likely that to complete E2E tests, a number of resources will be required as dependencies to enable the tests to pass successfully. Some examples:
- When testing the Diagnostic Settings interface for a Resource Module, you will need an existing Log Analytics Workspace to be able to send the logs to as a destination.
- When testing the Private Endpoints interface for a Resource Module, you will need an existing Virtual Network, Subnet and Private DNS Zone to be able to complete the Private Endpoint deployment and configuration.
Module owners MUST:
- Create the required resources that their module depends upon in the test file/directory
- They MUST either use:
- Simple/native resource declarations/definitions in their respective IaC language,
OR - Another already published AVM Module that MUST be pinned to a specific published version.
- They MUST NOT use any local directory path references or local copies of AVM modules in their own modules test directory.
- Simple/native resource declarations/definitions in their respective IaC language,
- They MUST either use:
โ Terraform & Bicep Log Analytics Workspace examples using simple/native declarations for use in E2E tests
Terraform
resource "azapi_resource" "resource_group" {
type = "Microsoft.Resources/resourceGroups@2024-03-01"
name = "rsg-test-001"
parent_id = "/subscriptions/${data.azapi_client_config.current.subscription_id}"
location = "West Europe"
body = {}
response_export_values = []
}
resource "azapi_resource" "log_analytics_workspace" {
type = "Microsoft.OperationalInsights/workspaces@2023-09-01"
name = "law-test-001"
parent_id = azapi_resource.resource_group.id
location = azapi_resource.resource_group.location
body = {
properties = {
sku = {
name = "PerGB2018"
}
retentionInDays = 30
}
}
response_export_values = []
}Bicep
resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = {
name: 'law-test-001'
location: resourceGroup().location
properties: {
sku: {
name: 'PerGB2018'
}
retentionInDays: 30
}
}Skipping Deployments (SHOULD NOT)
Deployment tests are an important part of a module’s validation and a staple of AVM’s CI environment. However, there are situations where certain e2e-test-deployments cannot be performed against AVM’s test environment (e.g., if a special configuration/registration (such as certain AI models) is required). For these cases, the CI offers the possibility to ‘skip’ specific test cases by placing a file named .e2eignore in their test folder.
Note
A skipped test case is still added to the ‘Usage Examples’ section of the module’s readme and should be manually validated in regular intervals.
You MUST add a note to the tests metadata description, which explains the excemption.
If you require that a test is skipped and add an โ.e2eignoreโ file (e.g. \<module\>/tests/e2e/\<testname\>/.e2eignore) to a pull request, a member of the AVM Core Technical Bicep Team must approve set pull request. The content of the file is logged the module’s workflow runs and transparently communicates why the test case is skipped during the deployment validation stage. It iss hence important to specify the reason for skipping the deployment in this file.
Sample filecontent:
The test is skipped, as only one instance of this service can be deployed to a subscription.Note
For resource modules, the ‘defaults’ and ‘waf-aligned’ tests can’t be skipped.
The deployment of a test can be skipped by adding a .e2eignore file into a test folder (e.g. /examples/<testname>).
ID: SNFR3 - Category: Testing - AVM Compliance Tests
Modules MUST pass all tests that ensure compliance to AVM specifications. These tests MUST pass before a module version can be published.
Important
Please note these are still under development at this time and will be published and available soon for module owners.
Module owners MUST request a manual GitHub Pull Request review, prior to their first release of version 0.1.0 of their module, from the related GitHub Team: @Azure/azure-verified-modules-tooling-contributors, OR @Azure/azure-verified-modules-tooling-contributors.
ID: SNFR4 - Category: Testing - Unit Tests
Modules SHOULD implement unit testing to ensure logic and conditions within parameters/variables/locals are performing correctly. These tests MUST pass before a module version can be published.
Unit Tests test specific module functionality, without deploying resources. Used on more complex modules. In Bicep and Terraform these live in tests/unit.
ID: SNFR5 - Category: Testing - Upgrade Tests
Modules SHOULD implement upgrade testing to ensure new features are implemented in a non-breaking fashion on non-major releases.
ID: SNFR6 - Category: Testing - Static Analysis/Linting Tests
Modules MUST use static analysis, e.g., linting, security scanning (PSRule, tflint, etc.). These tests MUST pass before a module version can be published.
There may be differences between languages in linting rules standards, but the AVM core team will try to close these and bring them into alignment over time.
ID: SNFR7 - Category: Testing - Idempotency Tests
Modules MUST implement idempotency end-to-end (deployment) testing. E.g. deploying the module twice over the top of itself.
Modules SHOULD pass the idempotency test, as we are aware that there are some exceptions where they may fail as a false-positive or legitimate cases where a resource cannot be idempotent.
For example, Virtual Machine Image names must be unique on each resource creation/update.
ID: BCPNFR10 - Category: Testing - Test Bicep File Naming
Module owners MUST name their test .bicep files in the /tests/e2e/<defaults/waf-aligned/max/etc.> directories: main.test.bicep as the test framework (CI) relies upon this name.
ID: BCPNFR11 - Category: Testing - Test Tooling
Module owners MUST use the below tooling for unit/linting/static/security analysis tests. These are also used in the AVM Compliance Tests.
- PSRule for Azure
- Pester
- Some tests are provided as part of the AVM Compliance Tests, but you are free to also use Pester for your own tests.
ID: BCPNFR12 - Category: Testing - Deployment Test Naming
Module owners MUST invoke the module in their test using the syntax:
module testDeployment '../../../main.bicep' =Example 1: Working example with a single deployment
module testDeployment '../../../main.bicep' = {
scope: resourceGroup
name: '${uniqueString(deployment().name, location)}-test-${serviceShort}'
params: {
(...)
}
}Example 2: Working example using a deployment loop
@batchSize(1)
module testDeployment '../../main.bicep' = [for iteration in [ 'init', 'idem' ]: {
scope: resourceGroup
name: '${uniqueString(deployment().name, location)}-test-${serviceShort}-${iteration}'
params: {
(...)
}
}]The syntax is used by the ReadMe-generating utility to identify, pull & format usage examples.
ID: BCPNFR13 - Category: Testing - Test file metadata
By default, the ReadMe-generating utility will create usage examples headers based on each e2e folder’s name.
Module owners MAY provide a custom name & description by specifying the metadata blocks name & description in their main.test.bicep test files.
For example:
metadata name = 'Using Customer-Managed-Keys with System-Assigned identity'
metadata description = 'This instance deploys the module using Customer-Managed-Keys using a System-Assigned Identity. This required the service to be deployed twice, once as a pre-requisite to create the System-Assigned Identity, and once to use it for accessing the Customer-Managed-Key secret.'would lead to a header in the module’s readme.md file along the lines of
### Example 1: _Using Customer-Managed-Keys with System-Assigned identity_
This instance deploys the module using Customer-Managed-Keys using a System-Assigned Identity. This required the service to be deployed twice, once as a pre-requisite to create the System-Assigned Identity, and once to use it for accessing the Customer-Managed-Key secret.ID: BCPNFR16 - Category: Testing - Post-deployment tests
For each test case in the e2e folder, you can optionally add post-deployment Pester tests that are executed once the corresponding deployment completed and before the removal logic kicks in.
To leverage the feature you MUST:
Use Pester as a test framework in each test file
Name the file with the suffix
"*.tests.ps1"Place each test file the
e2etest’s folder or any subfolder (e.g.,e2e/max/myTest.tests.ps1ore2e/max/tests/myTest.tests.ps1)Implement an input parameter
TestInputDatain the following way:param ( [Parameter(Mandatory = $false)] [hashtable] $TestInputData = @{} )Through this parameter you can make use of every output the
main.test.bicepfile returns, as well as the path to the test template file in case you want to extract data from it directly.For example, with an output such as
output resourceId string = testDeployment[1].outputs.resourceIddefined in themain.test.bicepfile, the$TestInputDatawould look like:$TestInputData = @{ DeploymentOutputs = @{ resourceId = @{ Type = "String" Value = "/subscriptions/***/resourceGroups/dep-***-keyvault.vaults-kvvpe-rg/providers/Microsoft.KeyVault/vaults/***kvvpe001" } } ModuleTestFolderPath = "/home/runner/work/bicep-registry-modules/bicep-registry-modules/avm/res/key-vault/vault/tests/e2e/private-endpoint" }A full test file may look like:
โ Pester post-deployment test file example
param ( [Parameter(Mandatory = $false)] [hashtable] $TestInputData = @{} ) Describe 'Validate private endpoint deployment' { Context 'Validate sucessful deployment' { It "Private endpoints should be deployed in resource group" { $keyVaultResourceId = $TestInputData.DeploymentOutputs.resourceId.Value $testResourceGroup = ($keyVaultResourceId -split '\/')[4] $deployedPrivateEndpoints = Get-AzPrivateEndpoint -ResourceGroupName $testResourceGroup $deployedPrivateEndpoints.Count | Should -BeGreaterThan 0 } } }
ID: BCPRMNFR1 - Category: Testing - Expected Test Directories
Module owners MUST create the defaults, waf-aligned folders within their /tests/e2e/ directory in their resource module source code and SHOULD create a max folder also. Module owners CAN create additional folders as required. Each folder will be used as described for various test cases.
Note
If a module can deploy varying styles of the same resource, e.g., VMs can be Linux or Windows, each style should be tested as both defaults and waf-aligned. Each must then be used as suffixes in the directory name to denote the style, e.g., for a VM we would expect to see:
/tests/e2e/linux.defaults/main.test.bicep/tests/e2e/linux.waf-aligned/main.test.bicep/tests/e2e/windows.defaults/main.test.bicep/tests/e2e/windows.waf-aligned/main.test.bicep
Defaults tests (MUST)
The defaults folder contains a test instance that deploys the module with the minimum set of required parameters.
This includes input parameters of type Required plus input parameters of type Conditional marked as required for WAF compliance.
This instance has heavy reliance on the default values for other input parameters. Parameters of type Optional SHOULD NOT be used.
WAF aligned tests (MUST)
The waf-aligned folder contains a test instance that deploys the module in alignment with the best-practices of the Azure Well-Architected Framework.
This includes input parameters of type Required, parameters of type Conditional marked as required for WAF compliance, and parameters of type Optional useful for WAF compliance.
Parameters and dependencies which are not needed for WAF compliance, SHOULD NOT be included.
Max tests (SHOULD)
The max folder contains a test instance that deploys the module using a large parameter set, enabling most of the modules’ features.
The purpose of this instance is primarily parameter validation and not necessarily to serve as a real example scenario. Ideally, all features, extension resources and child resources should be enabled in this test, unless not possible due to conflicts, e.g., in case parameters are mutually exclusive.
Note
Please note that this test is not mandatory to have, but recommended for bulk parameter validation. It can be skipped in case the module parameter validation is covered already by additional, more scenario-specific tests.
Additional tests (CAN)
Additional folders CAN be created by module owners as required.
For example, to validate parameters not covered by the max test due to conflicts, or to provide a real example scenario for a specific use case.
Documentation
The content below is listed based on the following tags
| # | ID | Title | Severity | Persona | Lifecycle |
|---|---|---|---|---|---|
| 1 | SNFR15 | Automatic Documentation Generation | MUST | OwnerContributor | BAU |
| 2 | SNFR16 | Examples/E2E | MUST | OwnerContributor | BAU |
| 3 | BCPNFR2 | Module Documentation Generation | MUST | OwnerContributor | BAU |
| 4 | BCPNFR3 | Usage Example formats | MUST | OwnerContributor | BAU |
| 5 | BCPNFR4 | Parameter Input Examples | MAY | OwnerContributor | BAU |
โ See Specifications for this category
ID: SNFR15 - Category: Documentation - Automatic Documentation Generation
README documentation MUST be automatically/programmatically generated. MUST include the sections as defined in the language specific requirements BCPNFR2, TFNFR2.
ID: SNFR16 - Category: Documentation - Examples/E2E
An examples/e2e directory MUST exist to provide named scenarios for module deployment.
ID: BCPNFR2 - Category: Documentation - Module Documentation Generation
Note
This script/tool is currently being developed by the AVM team and will be made available very soon.
Bicep modules documentation MUST be automatically generated via the provided script/tooling from the AVM team, providing the following headings:
- Title
- Description
- Navigation
- Resource Types
- Usage Examples
- Parameters
- Outputs
- Cross-referenced modules
ID: BCPNFR3 - Category: Documentation - Usage Example formats
Usage examples for Bicep modules MUST be provided in the following formats:
Bicep file (orchestration module style) -
.bicepmodule <resourceName> 'br/public:avm/[res|ptn|utl]/<publishedModuleName>:>version<' = { name: '${uniqueString(deployment().name, location)}-test-<uniqueIdentifier>' params: { (...) } }JSON / ARM Template Parameter Files -
.json{ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", "contentVersion": "1.0.0.0", "parameters": { (...) } }
Note
The above formats are currently automatically taken & generated from the tests/e2e tests. It is enough to run the Set-ModuleReadMe or Set-AVMModule functions (from the utilities folder) to update the usage examples in the readme(s).
Note
Bicep Parameter Files (.bicepparam) are being reviewed and considered by the AVM team for the usability and features at this time and will likely be added in the future.
ID: BCPNFR4 - Category: Documentation - Parameter Input Examples
Bicep modules MAY provide parameter input examples for parameters using the metadata.example property via the @metadata() decorator.
Example:
@metadata({
example: 'uksouth'
})
@description('Optional. Location for all resources.')
param location string = resourceGroup().location
@metadata({
example: '''
{
keyName: 'myKey'
keyVaultResourceId: '/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/my-rg/providers/Microsoft.KeyVault/vaults/myvault'
keyVersion: '6d143c1a0a6a453daffec4001e357de0'
userAssignedIdentityResourceId '/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/my-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myIdentity'
}
'''
})
@description('Optional. The customer managed key definition.')
param customerManagedKey customerManagedKeyTypeIt is planned that these examples are automatically added to the module readme’s parameter descriptions when running either the Set-ModuleReadMe or Set-AVMModule scripts (available in the utilities folder).
Release / Publishing
The content below is listed based on the following tags
| # | ID | Title | Severity | Persona | Lifecycle |
|---|---|---|---|---|---|
| 1 | SNFR17 | Semantic Versioning | MUST | OwnerContributor | BAU |
| 2 | SNFR18 | Breaking Changes | SHOULD | OwnerContributor | BAU |
| 3 | SNFR19 | Registries Targeted | MUST | OwnerContributor | BAU |
| 4 | SNFR21 | Cross Language Collaboration | SHOULD | OwnerContributor | BAU |
| 5 | BCPNFR22 | Bicep Module Changelog | MUST | OwnerContributor | BAU |
โ See Specifications for this category
ID: SNFR17 - Category: Release - Semantic Versioning
Important
You cannot specify the patch version for Bicep modules in the public Bicep Registry, as this is automatically incremented by 1 each time a module is published. You can only set the Major and Minor versions.
See the Bicep Contribution Guide for more information.
Modules MUST use semantic versioning (aka semver) for their versions and releases in accordance with: Semantic Versioning 2.0.0
For example all modules should be released using a semantic version that matches this pattern: X.Y.Z
X== Major VersionY== Minor VersionZ== Patch Version
Module versioning before first Major version release 1.0.0
Initially modules MUST be released as version
0.1.0and incremented via Minor and Patch versions only until the AVM Core Team are confident the AVM specifications are mature enough and appropriate CI test coverage is in place, plus the module owner is happy the module has been “road tested” and is now stable enough for its first Major release of version1.0.0.Note
Releasing as version
0.1.0initially and only incrementing Minor and Patch versions allows the module owner to make breaking changes more easily and frequently as it’s still not an official Major/Stable release. ๐Until first Major version
1.0.0is released, given a version numberX.Y.Z:XMajor version MUST NOT be bumped.YMinor version MUST be bumped when introducing breaking changes (which would normally bump Major after1.0.0release) or feature updates (same as it will be after1.0.0release).ZPatch version MUST be bumped when introducing non-breaking, backward compatible bug fixes (same as it will be after1.0.0release).
ID: SNFR18 - Category: Release - Breaking Changes
A module SHOULD avoid breaking changes, e.g., deprecating inputs vs. removing. If you need to implement changes that cause a breaking change, the major version should be increased.
Info
Modules that have not been released as 1.0.0 may introduce breaking changes, as explained in the previous ID SNFR17. That means that you have to introduce non-breaking and breaking changes with a minor version jump, as long as the module has not reached version 1.0.0.
There are, however, scenarios where you want to include breaking changes into a commit and not create a new major version. If you want to introduce breaking changes as part of a minor update, you can do so. In this case, it is essential to keep the change backward compatible, so that the existing code will continue to work. At a later point, another update can increase the major version and remove the code introduced for the backward compatibility.
Tip
See the language specific examples to find out how you can deal with deprecations in AVM modules.
ID: SNFR19 - Category: Publishing - Registries Targeted
Modules MUST be published to their respective language public registries.
- Bicep = Bicep Public Module Registry
- Within the
avmdirectory
- Within the
- Terraform = HashiCorp Terraform Registry
Tip
ID: SNFR21 - Category: Publishing - Cross Language Collaboration
When the module owners of the same Resource, Pattern or Utility module are not the same individual or team for all languages, each languages team SHOULD collaborate with their sibling language team for the same module to ensure consistency where possible.
ID: BCPNFR22 - Category: Publishing - Changelog
When a module to be published (i.e., that has a version.json file) is changed, an entry MUST be created in the CHANGELOG.md file in the module folder. A link to the latest version of the changelog file has to be included at the top of the file, just below the # Changelog line. It is surrounded by empty lines.
# Changelog
The latest version of the changelog can be found [here](https://github.com/Azure/bicep-registry-modules/blob/main/avm/<ptn|res|utl>/<namespace/modulename[/submodulePath]>/CHANGELOG.md).For each new version, an entry MUST be created above all existing versions in the CHANGELOG.md file of the module.
## <version>
### Changes
- This changed
- And this also
### Breaking Changes
- NoneEach version’s entry:
- MUST contain two sections:
ChangesandBreaking Changes. At least one of them must have a meaningful entry and sections must not be left empty. A- Nonemay be added as content for a section. - MUST exist only once.
- All versions appear in descending order, which puts the most recent changes at the top.
What SHOULD be listed in the (Breaking) Changes section:
- Relevant changes for the module
- Changes in tests do not need to be added
Note
The versioning is following the SNFR17 - Semantic Versioning spec.
Example content of the CHANGELOG.md
A CHANGELOG.md file in the module’s root folder MUST start with the # Changelog header, followed by an empty line and a link to the latest published version of the changelog file, followed by another empty line. A section for each published version follows. Newer versions are placed above older versions.
# Changelog
The latest version of the changelog can be found [here](https://github.com/Azure/bicep-registry-modules/blob/main/avm/res/aad/domain-service/CHANGELOG.md).
## 0.2.1
### Changes
- Updated the referenced AVM common types
### Breaking Changes
- None
## 0.2.0
### Changes
- Implemented the minCPU parameter
- Updated the referenced VirtualNetwork module
- Updated the referenced AVM common types
### Breaking Changes
- The minCPU parameter is mandatory
## 0.1.0
### Changes
- Initial Release
### Breaking Changes
- NoneEach bullet point should start with a capital letter.
Manual Editing
It is possible to modify the changelog content any time, e.g., to add missing versions, which will not create a new release of the module itself. Please note the following requirements in all cases:
- All versions in the file, need to be valid and available as published version
- Every version needs the two sections
## Changesand## Breaking Changeswith content
Note
Azure Verified Modules are artifacts in the Microsoft Container Registry (MCR). Every version of a module exists as a tag in the Container Registry and can be listed at https://mcr.microsoft.com/v2/bicep/avm/(res|ptn|utl)/<namespace/modulename>/tags/list. For example, see the FinOps hub module tags.
Code Style
The content below is listed based on the following tags
| # | ID | Title | Severity | Persona | Lifecycle |
|---|---|---|---|---|---|
| 1 | BCPNFR8 | Code Styling - lower camelCasing | SHOULD | OwnerContributor | BAU |
| 2 | BCPNFR17 | Code Styling - Type casting | SHOULD | OwnerContributor | BAU |
โ See Specifications for this category
ID: BCPNFR8 - Category: Composition - Code Styling - lower camelCasing
Module owners SHOULD use lower camelCasing for naming the following:
- Parameters
- Variables
- Outputs
- User Defined Types
- Resources (symbolic names)
- Modules (symbolic names)
For example: camelCasingExample (lowercase first word (entirely), with capital of first letter of all other words and rest of word in lowercase)
ID: BCPNFR17 - Category: Composition - Code Styling - Type casting
To improve the usability of primitive module properties declared as strings, you SHOULD declare them using a type which better represents them, and apply any required casting in the module on behalf of the user.
For reference, please refer to the following examples:
Boolean as String
@allowed([
'false'
'true'
])
param myParameterValue string = 'false'
resource myResource '(...)' = {
(...)
properties: {
myParameter: myParameterValue
}
}param myParameterValue string = false
resource myResource '(...)' = {
(...)
properties: {
myParameter: string(myParameterValue)
}
}Integer Array as String Array
@allowed([
'1'
'2'
'3'
])
param zones array
resource myResource '(...)' = {
(...)
properties: {
zones: zones
}
}@allowed([
1
2
3
])
param zones int[]
resource myResource '(...)' = {
(...)
properties: {
zones: map(zones, zone => string(zone))
}
}