Module Specifications

This section documents all the specifications for Azure Verified Modules (AVM) and their respective IaC languages.

Specifications by IaC Language

CategoryBicepTerraform
ResourcePatternUtilityResourcePatternUtility
Contribution/Support988988
Telemetry434222
Naming/Composition2518920149
CodeStyle222303030
Inputs/Outputs1411101298
Testing14131310109
Documentation555444
Release/Publishing555444
Summary786556918174

What changed recently?

See what specifications changed in the last 30 days...

#IDLast Modified (UTC)Git HistoryLast Commit
1TFRMNFR12026-05-29 10:34:42All Commits5807d20
2PMNFR12026-05-26 15:12:56All Commitsb7412ee
3RMFR32026-05-26 15:12:56All Commitsb7412ee
4RMNFR12026-05-26 15:12:56All Commitsb7412ee
5SNFR122026-05-26 15:12:56All Commitsb7412ee
6SNFR22026-05-26 15:12:56All Commitsb7412ee
7SNFR262026-05-26 15:12:56All Commitsb7412ee
8UMNFR12026-05-26 15:12:56All Commitsb7412ee
9TFRMFR12026-05-26 15:12:56All Commitsb7412ee
10TFRMNFR22026-05-26 15:12:56All Commitsb7412ee
11TFFR12026-05-26 15:12:56All Commitsb7412ee
12TFFR22026-05-26 15:12:56All Commitsb7412ee
13TFFR32026-05-26 15:12:56All Commitsb7412ee
14TFFR42026-05-26 15:12:56All Commitsb7412ee
15TFFR52026-05-26 15:12:56All Commitsb7412ee
16TFFR62026-05-26 15:12:56All Commitsb7412ee
17TFFR72026-05-26 15:12:56All Commitsb7412ee
18TFNFR112026-05-26 15:12:56All Commitsb7412ee
19TFNFR122026-05-26 15:12:56All Commitsb7412ee
20TFNFR172026-05-26 15:12:56All Commitsb7412ee
21TFNFR182026-05-26 15:12:56All Commitsb7412ee
22TFNFR212026-05-26 15:12:56All Commitsb7412ee
23TFNFR232026-05-26 15:12:56All Commitsb7412ee
24TFNFR252026-05-26 15:12:56All Commitsb7412ee
25TFNFR262026-05-26 15:12:56All Commitsb7412ee
26TFNFR272026-05-26 15:12:56All Commitsb7412ee
27TFNFR292026-05-26 15:12:56All Commitsb7412ee
28TFNFR342026-05-26 15:12:56All Commitsb7412ee
29TFNFR362026-05-26 15:12:56All Commitsb7412ee
30TFNFR382026-05-26 15:12:56All Commitsb7412ee
31TFNFR392026-05-26 15:12:56All Commitsb7412ee
32TFNFR52026-05-26 15:12:56All Commitsb7412ee
33TFNFR72026-05-26 15:12:56All Commitsb7412ee
34TFNFR82026-05-26 15:12:56All Commitsb7412ee

How to navigate the specifications?

The “Module Specifications” section uses tags to dynamically render content based on the selected attributes, such as the IaC language, module classification, category, severity and more. The tags are defined in header of each specification page.

To make it easier for module owners and contributors to navigate the documentation, the specifications are grouped to distinct pages by the IaC language (Bicep | Terraform) and module classification ( resource | pattern | utility). The specifications on each page are further ordered by the category (e.g., Composition, CodeStyle, Testing, etc.), severity of the requirements (MUST | SHOULD | MAY) and at what stage of the module’s lifecycle the specification is typically applicable (Initial | BAU | EOL).

To find what you need, simply decide which IaC language you’d like develop in and what classification your module falls under, then navigate to the respective page to find the specifications that are relevant to you.

Info

All specifications have a 4-9 character long unique ID - a combination of letters and numbers. These letters only carry legacy meaning only leveraged by the AVM core team and are no longer used to group the specifications in any visible way. The ID is used to reference the specification in the code, documentation, and discussions.

Specification Tags

The following tags are used to qualify the specifications:

KeyAllowed ValuesMultiple/Single
LanguageBicep, TerraformMultiple
ClassResource, Pattern, UtilityMultiple
TypeFunctional, NonFunctionalSingle
CategoryTesting, Telemetry, Contribution/Support, Documentation, CodeStyle, Naming/Composition, Inputs/Outputs, Release/PublishingSingle
SeverityMUST, SHOULD, MAYSingle
PersonaOwner, ContributorMultiple
LifecycleInitial, BAU, EOLSingle
ValidationBicep: BCP/Manual, BCP/CI/Informational, BCP/CI/Enforced
Terraform: TF/Manual, TF/CI/Informational, TF/CI/Enforced
Single per language

Each tag is a concatenation of exactly one of the keys and one of the values, e.g., Language-Bicep, Class-Resource, Type-Functional, etc. When it’s marked as Multiple, it means that the tag can have multiple values, e.g., Language-Bicep, Language-Terraform, or Persona-Owner, Persona-Contributor, etc. When it’s marked as Single, it means that the tag can have only one value, e.g., Type-Functional, Lifecycle-Initial, etc.

➕ Click here to see the definition of the Severity, Persona, Lifecycle and Validation tags...

Severity

What’s the severity or importance of this specification? See “How to read the specifications?” section for more details.

Persona

Who is this specification for? The Owner is the module owner, while the Contributor is anyone who contributes to the module.

Lifecycle

When is this specification mostly relevant?

  • The Initial stage is when the module is being developed first - e.g., naming related specs are labeled with Lifecycle-Initial as the naming of the module only happens once: at the beginning of their life.
  • The BAU (business as usual) stage is at any time during the module’s typical lifecycle - e.g., specs that describe coding standards are relevant throughout the module’s life, for any time a new module version is released.
  • The EOL (end of life) stage is when the module is being decommissioned - e.g., specs describing how a module should be retired are labeled with Lifecycle-EOL.

Validation

How is this specification checked/validated/enforced?

  • Manual means that the specification is manually enforced at the time of the module review (at the time of the first or any subsequent module version release).
  • CI/Informational means that the module is checked against the specification by a CI pipeline, but the failure is only informational and doesn’t block the module release.
  • CI/Enforced means that the specification is automatically enforced by a CI pipeline, and the failure blocks the module release.

Note: the BCP/ or TF/ prefix is required as shared (language-agnostic) specifications may have different level of validation/enforcement per each language - e.g., it is possible that a specification is enforced by a CI pipeline for Bicep modules, while it is manually enforced for Terraform modules.

Why are there language specific specifications?

While every effort is being made to standardize requirements and implementation details across all languages (and most specifications in fact, are applicable to all), it is expected that some of the specifications will be different between their respective languages to ensure we follow the best practices and leverage features of each language.

How to read the specifications?

Important

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

As you’re developing/maintaining a module as a module owner or contributor, you need to ensure that your module adheres to the specifications outlined in this section. The specifications are designed to ensure that all AVM modules are consistent, secure, and compliant with best practices.

There are 3 levels of specifications:

  • MUST: These are mandatory requirements that MUST be followed.
  • SHOULD: These are recommended requirements that SHOULD be followed, unless there are good reasons for not to.
  • MAY: These are optional requirements that MAY be followed at the module owner’s/contributor’s discretion.

Subsections of Module Specifications

Bicep Specifications

Specifications by Category and Module Classification

CategoryResourcePatternUtility
Contribution/Support988
Telemetry434
Naming/Composition25189
CodeStyle222
Inputs/Outputs141110
Testing141313
Documentation555
Release/Publishing555
Summary786556

How to propose changes to the specifications?

Important

Any updates to existing or new specifications for Bicep must be submitted as a draft for review by the AVM core team(@Azure/avm-core-team).

What changed recently?

See what specifications changed in the last 30 days...

#IDLast Modified (UTC)Git HistoryLast Commit
1PMNFR12026-05-26 15:12:56All Commitsb7412ee
2RMFR32026-05-26 15:12:56All Commitsb7412ee
3RMNFR12026-05-26 15:12:56All Commitsb7412ee
4SNFR122026-05-26 15:12:56All Commitsb7412ee
5SNFR22026-05-26 15:12:56All Commitsb7412ee
6SNFR262026-05-26 15:12:56All Commitsb7412ee
7UMNFR12026-05-26 15:12:56All Commitsb7412ee

Subsections of Bicep

Bicep Interfaces

This chapter details the interfaces/schemas for the AVM Resource Modules features/extension resources as referenced in RMFR4 and RMFR5.

Diagnostic Settings

Important

Allowed values for logs and metric categories or category groups MUST NOT be specified to keep the module implementation evergreen for any new categories or category groups added by RPs, without module owners having to update a list of allowed values and cut a new release of their module.

Diagnostic Settings
  
  // ============== //
  //   Parameters   //
  // ============== //
  
  import { diagnosticSettingFullType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('Optional. The diagnostic settings of the service. If neither metrics nor logs are specified, all metrics & logs are configured by default. If only one of them is specified, the other one will not be configured.')
  param diagnosticSettings diagnosticSettingFullType[]?
  
  // ============= //
  //   Resources   //
  // ============= //
  
  resource >singularMainResourceType<_diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = [for (diagnosticSetting, index) in (diagnosticSettings ?? []): {
    name: diagnosticSetting.?name ?? '${name}-diagnosticSettings'
    properties: {
      storageAccountId: diagnosticSetting.?storageAccountResourceId
      workspaceId: diagnosticSetting.?workspaceResourceId
      eventHubAuthorizationRuleId: diagnosticSetting.?eventHubAuthorizationRuleResourceId
      eventHubName: diagnosticSetting.?eventHubName
      metrics: [
        for group in (diagnosticSetting.?metricCategories ?? (empty(diagnosticSetting.?logCategoriesAndGroups)
          ? [{ category: 'AllMetrics' }]
          : [])): {
          category: group.category
          enabled: group.?enabled ?? true
          timeGrain: null
        }
      ]
      logs: [
        for group in (diagnosticSetting.?logCategoriesAndGroups ?? (empty(diagnosticSetting.?metricCategories)
          ? [{ categoryGroup: 'allLogs' }]
          : [])): {
          categoryGroup: group.?categoryGroup
          category: group.?category
          enabled: group.?enabled ?? true
        }
      ]
      marketplacePartnerId: diagnosticSetting.?marketplacePartnerResourceId
      logAnalyticsDestinationType: diagnosticSetting.?logAnalyticsDestinationType
    }
    scope: >singularMainResourceType<
  }]
  
  diagnosticSettings: [
    {
      name: 'diagSetting1'
      logCategoriesAndGroups: [
        {
          category: 'AzurePolicyEvaluationDetails'
        }
        {
          category: 'AuditEvent'
        }
      ]
      metricCategories: [
        {
          category: 'AllMetrics'
        }
      ]
      logAnalyticsDestinationType: 'Dedicated'
      workspaceResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}'
      storageAccountResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{storageAccountName}'
      eventHubAuthorizationRuleResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.EventHub/namespaces/{namespaceName}/eventhubs/{eventHubName}/authorizationrules/{authorizationRuleName}'
      eventHubName: '{eventHubName}'
      marketplacePartnerResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{partnerResourceProvider}/{partnerResourceType}/{partnerResourceName}'
    }
  ]
  
  
  // ============== //
  //   Parameters   //
  // ============== //
  
  import { diagnosticSettingMetricsOnlyType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('Optional. The diagnostic settings of the service.')
  param diagnosticSettings diagnosticSettingMetricsOnlyType[]?
  
  // ============= //
  //   Resources   //
  // ============= //
  
  resource >singularMainResourceType<_diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = [for (diagnosticSetting, index) in (diagnosticSettings ?? []): {
    name: diagnosticSetting.?name ?? '${name}-diagnosticSettings'
    properties: {
      storageAccountId: diagnosticSetting.?storageAccountResourceId
      workspaceId: diagnosticSetting.?workspaceResourceId
      eventHubAuthorizationRuleId: diagnosticSetting.?eventHubAuthorizationRuleResourceId
      eventHubName: diagnosticSetting.?eventHubName
      metrics: [for group in (diagnosticSetting.?metricCategories ?? [ { category: 'AllMetrics' } ]): {
        category: group.category
        enabled: group.?enabled ?? true
        timeGrain: null
      }]
      marketplacePartnerId: diagnosticSetting.?marketplacePartnerResourceId
      logAnalyticsDestinationType: diagnosticSetting.?logAnalyticsDestinationType
    }
    scope: >singularMainResourceType<
  }]
  
  diagnosticSettings: [
    {
      name: 'diagSetting1'
      metricCategories: [
        {
          category: 'AllMetrics'
        }
      ]
      logAnalyticsDestinationType: 'Dedicated'
      workspaceResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}'
      storageAccountResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{storageAccountName}'
      eventHubAuthorizationRuleResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.EventHub/namespaces/{namespaceName}/eventhubs/{eventHubName}/authorizationrules/{authorizationRuleName}'
      eventHubName: '{eventHubName}'
      marketplacePartnerResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{partnerResourceProvider}/{partnerResourceType}/{partnerResourceName}'
    }
  ]
  
  
  // ============== //
  //   Parameters   //
  // ============== //
  
  import { diagnosticSettingLogsOnlyType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('Optional. The diagnostic settings of the service.')
  param diagnosticSettings diagnosticSettingLogsOnlyType[]?
  
  // ============= //
  //   Resources   //
  // ============= //
  
  resource >singularMainResourceType<_diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = [for (diagnosticSetting, index) in (diagnosticSettings ?? []): {
    name: diagnosticSetting.?name ?? '${name}-diagnosticSettings'
    properties: {
      storageAccountId: diagnosticSetting.?storageAccountResourceId
      workspaceId: diagnosticSetting.?workspaceResourceId
      eventHubAuthorizationRuleId: diagnosticSetting.?eventHubAuthorizationRuleResourceId
      eventHubName: diagnosticSetting.?eventHubName
      logs: [for group in (diagnosticSetting.?logCategoriesAndGroups ?? [ { categoryGroup: 'allLogs' } ]): {
        categoryGroup: group.?categoryGroup
        category: group.?category
        enabled: group.?enabled ?? true
      }]
      marketplacePartnerId: diagnosticSetting.?marketplacePartnerResourceId
      logAnalyticsDestinationType: diagnosticSetting.?logAnalyticsDestinationType
    }
    scope: >singularMainResourceType<
  }]
  
  diagnosticSettings: [
    {
      name: 'diagSetting1'
      logCategoriesAndGroups: [
        {
          category: 'AzurePolicyEvaluationDetails'
        }
        {
          category: 'AuditEvent'
        }
      ]
      logAnalyticsDestinationType: 'Dedicated'
      workspaceResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}'
      storageAccountResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{storageAccountName}'
      eventHubAuthorizationRuleResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.EventHub/namespaces/{namespaceName}/eventhubs/{eventHubName}/authorizationrules/{authorizationRuleName}'
      eventHubName: '{eventHubName}'
      marketplacePartnerResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{partnerResourceProvider}/{partnerResourceType}/{partnerResourceName}'
    }
  ]
  
Note

In the provided example for Diagnostic Settings, both logs and metrics are enabled for the associated resource. However, it is IMPORTANT to note that certain resources may not support both diagnostic setting types/categories. In such cases, the resource configuration MUST be modified accordingly to ensure proper functionality and compliance with system requirements.

Role Assignments

  
  // ============== //
  //   Parameters   //
  // ============== //
  
  import { roleAssignmentType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('Optional. Array of role assignments to create.')
  param roleAssignments roleAssignmentType[]?
  
  // ============= //
  //   Variables   //
  // ============= //
  
  var builtInRoleNames = {
    // Add other relevant built-in roles here for your resource as per BCPNFR5
    Contributor: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')
    Owner: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')
    Reader: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')
    'Role Based Access Control Administrator (Preview)': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')
    'User Access Administrator': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')
  }
  
  var formattedRoleAssignments = [
    for (roleAssignment, index) in (roleAssignments ?? []): union(roleAssignment, {
      roleDefinitionId: builtInRoleNames[?roleAssignment.roleDefinitionIdOrName] ?? (contains(roleAssignment.roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/')
            ? roleAssignment.roleDefinitionIdOrName
            : subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleAssignment.roleDefinitionIdOrName))
    })
  ]
  
  // ============= //
  //   Resources   //
  // ============= //
  
  resource >singularMainResourceType<_roleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [
    for (roleAssignment, index) in (formattedRoleAssignments ?? []): {
      name: roleAssignment.?name ?? guid(>singularMainResourceType<.id, roleAssignment.principalId, roleAssignment.roleDefinitionId)
      properties: {
        roleDefinitionId: roleAssignment.roleDefinitionId
        principalId: roleAssignment.principalId
        description: roleAssignment.?description
        principalType: roleAssignment.?principalType
        condition: roleAssignment.?condition
        conditionVersion: !empty(roleAssignment.?condition) ? (roleAssignment.?conditionVersion ?? '2.0') : null // Must only be set if condtion is set
        delegatedManagedIdentityResourceId: roleAssignment.?delegatedManagedIdentityResourceId
      }
      scope: >singularMainResourceType<
    }
  ]
  
  roleAssignments: [
    {
      roleDefinitionIdOrName: 'Owner'
      principalId: nestedDependencies.outputs.managedIdentityPrincipalId
      principalType: 'ServicePrincipal'
    }
    {
      roleDefinitionIdOrName: 'b24988ac-6180-42a0-ab88-20f7382dd24c'
      principalId: nestedDependencies.outputs.managedIdentityPrincipalId
      principalType: 'ServicePrincipal'
    }
    {
      roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')
      principalId: nestedDependencies.outputs.managedIdentityPrincipalId
      principalType: 'ServicePrincipal'
    }
    {
      name: guid('Custom role assignment name seed')
      roleDefinitionIdOrName: 'Storage Blob Data Reader'
      principalId: '00000000-0000-0000-0000-000000000000'
      principalType: 'Group'
      description: 'Group with read-only access'
      condition: '@Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase 'foo_storage_container''
      conditionVersion: '2.0'
    }
  ]
  

Details on child, extension and cross-referenced resources:

  • Modules MUST support Role Assignments on child, extension and cross-referenced resources as well as the primary resource via parameters/variables

Resource Locks

  // ============== //
  //   Parameters   //
  // ============== //
  
  import { lockType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('Optional. The lock settings of the service.')
  param lock lockType?
  
  // ============= //
  //   Resources   //
  // ============= //
  
  resource >singularMainResourceType<_lock 'Microsoft.Authorization/locks@2020-05-01' = if (!empty(lock ?? {}) && lock.?kind != 'None') {
    name: lock.?name ?? 'lock-${name}'
    properties: {
      level: lock.?kind ?? ''
      notes: lock.?notes ?? (lock.?kind == 'CanNotDelete'
        ? 'Cannot delete resource or child resources.'
        : 'Cannot delete or modify the resource or child resources.')
    }
    scope: >singularMainResourceType<
  }
  
  lock: {
    kind: 'CanNotDelete'
    name: 'myCustomLockName'
    notes: 'This is a custom lock note.'
  }
  

Details on child and extension resources:

  • Locks SHOULD be able to be set for child resources of the primary resource in resource modules

Details on cross-referenced resources:

  • Locks MUST be automatically applied to cross-referenced resources if the primary resource has a lock applied.
    • This MUST also be able to be turned off for each of the cross-referenced resources by the module consumer via a parameter/variable if they desire

An example of this is a Key Vault module that has a Private Endpoints enabled. If a lock is applied to the Key Vault via the lock parameter/variable then the lock should also be applied to the Private Endpoint automatically, unless the privateEndpointLock/private_endpoint_lock (example name) parameter/variable is set to None

Tags

  @description('Optional. Tags of the resource.')
  param tags object?
  
  tags: {
    key: 'value'
    'another-key': 'another-value'
    integers: 123
  }
  

Details on child, extension and cross-referenced resources:

  • Tags MUST be automatically applied to child, extension and cross-referenced resources, if tags are applied to the primary resource.
    • By default, all tags set for the primary resource will automatically be passed down to child, extension and cross-referenced resources.
    • This MUST be able to be overridden by the module consumer so they can specify alternate tags for child, extension and cross-referenced resources, if they desire via a parameter/variable
      • If overridden by the module consumer, no merge/union of tags will take place from the primary resource and only the tags specified for the child, extension and cross-referenced resources will be applied

Managed Identities

  
  // ============== //
  //   Parameters   //
  // ============== //
  
  import { managedIdentityAllType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('Optional. The managed identity definition for this resource.')
  param managedIdentities managedIdentityAllType?
  
  // ============= //
  //   Variables   //
  // ============= //
  
  var formattedUserAssignedIdentities = reduce(map((managedIdentities.?userAssignedResourceIds ?? []), (id) => { '${id}': {} }), {}, (cur, next) => union(cur, next)) // Converts the flat array to an object like { '${id1}': {}, '${id2}': {} }
  var identity = !empty(managedIdentities) ? {
    type: (managedIdentities.?systemAssigned ?? false) ? (!empty(managedIdentities.?userAssignedResourceIds ?? {}) ? 'SystemAssigned,UserAssigned' : 'SystemAssigned') : (!empty(managedIdentities.?userAssignedResourceIds ?? {}) ? 'UserAssigned' : null)
    userAssignedIdentities: !empty(formattedUserAssignedIdentities) ? formattedUserAssignedIdentities : null
  } : null
  
  // ============= //
  //   Resources   //
  // ============= //
  
  resource >singularMainResourceType< '>providerNamespace</>resourceType<@>apiVersion<' = {
    name: name
    identity: identity
    properties: {
      ... // other properties
    }
  }
  
  // =========== //
  //   Outputs   //
  // =========== //
  
  @description('The principal ID of the system assigned identity.')
  output systemAssignedMIPrincipalId string? = >singularMainResourceType<.?identity.?principalId
  
  managedIdentities: {
    systemAssigned: true
    userAssignedResourceIds: [
      '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}'
      '/subscriptions/{subscriptionId2}/resourceGroups/{resourceGroupName2}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName2}'
    ]
  }
  
  
  // ============== //
  //   Parameters   //
  // ============== //
  
  import { managedIdentityOnlySysAssignedType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('Optional. The managed identity definition for this resource.')
  param managedIdentities managedIdentityOnlySysAssignedType?
  
  // ============= //
  //   Variables   //
  // ============= //
  
  var identity = !empty(managedIdentities)
    ? {
        type: (managedIdentities.?systemAssigned ?? false) ? 'SystemAssigned' : null
      }
    : null
  
  // ============= //
  //   Resources   //
  // ============= //
  
  resource >singularMainResourceType< '>providerNamespace</>resourceType<@>apiVersion<' = {
    name: name
    identity: identity
    properties: {
      ... // other properties
    }
  }
  
  // =========== //
  //   Outputs   //
  // =========== //
  
  @description('The principal ID of the system assigned identity.')
  output systemAssignedMIPrincipalId string? = >singularMainResourceType<.?identity.?principalId
  
  managedIdentities: {
    systemAssigned: true
  }
  
  
  // ============== //
  //   Parameters   //
  // ============== //
  
  import { managedIdentityOnlyUserAssignedType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('Optional. The managed identity definition for this resource.')
  param managedIdentities managedIdentityOnlyUserAssignedType?
  
  // ============= //
  //   Variables   //
  // ============= //
  
  var formattedUserAssignedIdentities = reduce(map((managedIdentities.?userAssignedResourceIds ?? []), (id) => { '${id}': {} }), {}, (cur, next) => union(cur, next)) // Converts the flat array to an object like { '${id1}': {}, '${id2}': {} }
  var identity = !empty(managedIdentities)
    ? {
        type: !empty(managedIdentities.?userAssignedResourceIds ?? {}) ? 'UserAssigned' : 'None'
        userAssignedIdentities: !empty(formattedUserAssignedIdentities) ? formattedUserAssignedIdentities : null
      }
    : null
  
  // ============= //
  //   Resources   //
  // ============= //
  
  resource >singularMainResourceType< '>providerNamespace</>resourceType<@>apiVersion<' = {
    name: name
    identity: identity
    properties: {
      ... // other properties
    }
  }
  
  managedIdentities: {
    userAssignedResourceIds: [
      '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}'
      '/subscriptions/{subscriptionId2}/resourceGroups/{resourceGroupName2}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName2}'
    ]
  }
  

Reason for differences in User Assigned data type in languages:

  • We do not foresee the Managed Identity Resource Provider team to ever add additional properties within the empty object ({}) value required on the input of a User Assigned Managed Identity.
  • In Bicep we therefore have removed the need for this to be declared and just converted it to a simple array of Resource IDs

Private Endpoints

Private Endpoints

E.g., for services that only have one private endpoint type.

  
  // ============== //
  //   Parameters   //
  // ============== //
  
  import { privateEndpointSingleServiceType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible.')
  param privateEndpoints privateEndpointSingleServiceType[]?
  
  var enableReferencedModulesTelemetry = false // resource module
  
  // ============= //
  //   Resources   //
  // ============= //
  
  module >singularMainResourceType<_privateEndpoints 'br/public:avm/res/network/private-endpoint:>version<' = [for (privateEndpoint, index) in (privateEndpoints ?? []): {
    name: '${uniqueString(deployment().name, location)}->singularMainResourceType<-PrivateEndpoint-${index}'
    scope: resourceGroup(
      split(privateEndpoint.?resourceGroupResourceId ?? resourceGroup().id, '/')[2],
      split(privateEndpoint.?resourceGroupResourceId ?? resourceGroup().id, '/')[4]
    )
    params: {
      // Variant 1: A default service can be assumed (i.e., for services that only have one private endpoint type)
      name: privateEndpoint.?name ?? 'pep-${last(split(>singularMainResourceType<.id, '/'))}-${privateEndpoint.?service ?? '>defaultServiceName<'}-${index}'
      privateLinkServiceConnections: privateEndpoint.?isManualConnection != true ? [
        {
          name: privateEndpoint.?privateLinkServiceConnectionName ?? '${last(split(>singularMainResourceType<.id, '/'))}-${privateEndpoint.?service ?? '>defaultServiceName<'}-${index}'
          properties: {
            privateLinkServiceId: >singularMainResourceType<.id
            groupIds: [
              privateEndpoint.?service ?? '>defaultServiceName<'
            ]
          }
        }
      ] : null
      manualPrivateLinkServiceConnections: privateEndpoint.?isManualConnection == true ? [
        {
          name: privateEndpoint.?privateLinkServiceConnectionName ?? '${last(split(>singularMainResourceType<.id, '/'))}-${privateEndpoint.?service ?? '>defaultServiceName<'}-${index}'
          properties: {
            privateLinkServiceId: >singularMainResourceType<.id
            groupIds: [
              privateEndpoint.?service ?? '>defaultServiceName<'
            ]
            requestMessage: privateEndpoint.?manualConnectionRequestMessage ?? 'Manual approval required.'
          }
        }
      ] : null
      subnetResourceId: privateEndpoint.subnetResourceId
      enableTelemetry: enableReferencedModulesTelemetry // resource module
      enableTelemetry: privateEndpoint.?enableTelemetry ?? enableTelemetry // pattern / utility module
      location: privateEndpoint.?location ?? reference(split(privateEndpoint.subnetResourceId, '/subnets/')[0], '2020-06-01', 'Full').location
      lock: privateEndpoint.?lock ?? lock
      privateDnsZoneGroup: privateEndpoint.?privateDnsZoneGroup
      roleAssignments: privateEndpoint.?roleAssignments
      tags: privateEndpoint.?tags ?? tags
      customDnsConfigs: privateEndpoint.?customDnsConfigs
      ipConfigurations: privateEndpoint.?ipConfigurations
      applicationSecurityGroupResourceIds: privateEndpoint.?applicationSecurityGroupResourceIds
      customNetworkInterfaceName: privateEndpoint.?customNetworkInterfaceName
    }
  }]
  
  @description('The private endpoints of the resource.')
  output privateEndpoints privateEndpointOutputType[] = [
    for (pe, index) in (privateEndpoints ?? []): {
      name: >singularMainResourceType<_privateEndpoints[index].outputs.name
      resourceId: >singularMainResourceType<_privateEndpoints[index].outputs.resourceId
      groupId: >singularMainResourceType<_privateEndpoints[index].outputs.?groupId!
      customDnsConfigs: >singularMainResourceType<_privateEndpoints[index].outputs.customDnsConfigs
      networkInterfaceResourceIds: >singularMainResourceType<_privateEndpoints[index].outputs.networkInterfaceResourceIds
    }
  ]
  
  // =============== //
  //   Definitions   //
  // =============== //
  
  @export()
  type privateEndpointOutputType = {
    @description('The name of the private endpoint.')
    name: string
  
    @description('The resource ID of the private endpoint.')
    resourceId: string
  
    @description('The group Id for the private endpoint Group.')
    groupId: string?
  
    @description('The custom DNS configurations of the private endpoint.')
    customDnsConfigs: {
      @description('FQDN that resolves to private endpoint IP address.')
      fqdn: string?
  
      @description('A list of private IP addresses of the private endpoint.')
      ipAddresses: string[]
    }[]
  
    @description('The IDs of the network interfaces associated with the private endpoint.')
    networkInterfaceResourceIds: string[]
  }
  
  privateEndpoints: {
    {
      name: 'myPeName'
      privateLinkServiceConnectionName: 'myPrivateLinkConnectionName'
      lock: 'CanNotDelete'
      tags: {
        'hidden-title': 'This is visible in the resource name'
      }
      subnetResourceId: '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myRg/providers/Microsoft.Network/virtualNetworks/myVnet/subnets/mysubnet'
      resourceGroupResourceId: '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myRg'
      applicationSecurityGroupResourceIds: [
        '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myRg/providers/Microsoft.Network/applicationSecurityGroups/myAsg'
      ]
      privateDnsZoneGroup: {
        privateDnsZoneGroupConfigs: [
          {
            name: 'config'
            privateDnsZoneResourceId: '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myRg/providers/Microsoft.Network/privateDnsZones/myZone'
          }
        ]
      }
      customDnsConfigs: [
        {
          fqdn: 'fqdn1.example.com'
          ipAddresses: [
            '10.0.0.1',
            '10.0.0.2'
          ]
        }
      ]
      networkInterfaceName: 'nic1'
      ipConfigurations: [
        {
          name: 'ipconfig1'
          groupId: 'vault'
          memberName: 'default'
          privateIpAddress: '10.0.0.7'
        }
      ]
      roleAssignments: [
        {
          roleDefinitionIdOrName: 'Owner'
          principalId: '11111111-1111-1111-1111-111111111111'
          principalType: 'ServicePrincipal'
        }
        {
          roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions','acdd72a7-3385-48ef-bd42-f606fba81ae7')
          principalId: '11111111-1111-1111-1111-111111111111'
          principalType: 'ServicePrincipal'
        }
      ]
    }
  }
  

E.g., for services that have more than one private endpoint type, like a Storage Account (blob, file, etc.)

  
  // ============== //
  //   Parameters   //
  // ============== //
  
  import { privateEndpointMultiServiceType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible.')
  param privateEndpoints privateEndpointMultiServiceType[]?
  
  var enableReferencedModulesTelemetry = false // resource module
  
  // ============= //
  //   Resources   //
  // ============= //
  
  module >singularMainResourceType<_privateEndpoints 'br/public:avm/res/network/private-endpoint:>version<' = [for (privateEndpoint, index) in (privateEndpoints ?? []): {
    name: '${uniqueString(deployment().name, location)}->singularMainResourceType<-PrivateEndpoint-${index}'
    scope: resourceGroup(
      split(privateEndpoint.?resourceGroupResourceId ?? resourceGroup().id, '/')[2],
      split(privateEndpoint.?resourceGroupResourceId ?? resourceGroup().id, '/')[4]
    )
    params: {
      // Variant 2: A default service cannot be assumed (i.e., for services that have more than one private endpoint type, like Storage Account)
      name: privateEndpoint.?name ?? 'pep-${last(split(>singularMainResourceType<.id, '/'))}-${privateEndpoint.service}-${index}'
      privateLinkServiceConnections: privateEndpoint.?isManualConnection != true ? [
        {
          name: privateEndpoint.?privateLinkServiceConnectionName ?? '${last(split(>singularMainResourceType<.id, '/'))}-${privateEndpoint.service}-${index}'
          properties: {
            privateLinkServiceId: >singularMainResourceType<.id
            groupIds: [
              privateEndpoint.service
            ]
          }
        }
      ] : null
      manualPrivateLinkServiceConnections: privateEndpoint.?isManualConnection == true ? [
        {
          name: privateEndpoint.?privateLinkServiceConnectionName ?? '${last(split(>singularMainResourceType<.id, '/'))}-${privateEndpoint.service}-${index}'
          properties: {
            privateLinkServiceId: >singularMainResourceType<.id
            groupIds: [
              privateEndpoint.service
            ]
            requestMessage: privateEndpoint.?manualConnectionRequestMessage ?? 'Manual approval required.'
          }
        }
      ] : null
      subnetResourceId: privateEndpoint.subnetResourceId
      enableTelemetry: enableReferencedModulesTelemetry // resource module
      enableTelemetry: privateEndpoint.?enableTelemetry ?? enableTelemetry // pattern / utility module
      location: privateEndpoint.?location ?? reference(split(privateEndpoint.subnetResourceId, '/subnets/')[0], '2020-06-01', 'Full').location
      lock: privateEndpoint.?lock ?? lock
      privateDnsZoneGroup: privateEndpoint.?privateDnsZoneGroup
      roleAssignments: privateEndpoint.?roleAssignments
      tags: privateEndpoint.?tags ?? tags
      customDnsConfigs: privateEndpoint.?customDnsConfigs
      ipConfigurations: privateEndpoint.?ipConfigurations
      applicationSecurityGroupResourceIds: privateEndpoint.?applicationSecurityGroupResourceIds
      customNetworkInterfaceName: privateEndpoint.?customNetworkInterfaceName
    }
  }]
  
  @description('The private endpoints of the resource.')
  output privateEndpoints privateEndpointOutputType[] = [
    for (pe, index) in (privateEndpoints ?? []): {
      name: >singularMainResourceType<_privateEndpoints[index].outputs.name
      resourceId: >singularMainResourceType<_privateEndpoints[index].outputs.resourceId
      groupId: >singularMainResourceType<_privateEndpoints[index].outputs.?groupId!
      customDnsConfigs: >singularMainResourceType<_privateEndpoints[index].outputs.customDnsConfigs
      networkInterfaceResourceIds: >singularMainResourceType<_privateEndpoints[index].outputs.networkInterfaceResourceIds
    }
  ]
  
  // =============== //
  //   Definitions   //
  // =============== //
  
  @export()
  type privateEndpointOutputType = {
    @description('The name of the private endpoint.')
    name: string
  
    @description('The resource ID of the private endpoint.')
    resourceId: string
  
    @description('The group Id for the private endpoint Group.')
    groupId: string?
  
    @description('The custom DNS configurations of the private endpoint.')
    customDnsConfigs: {
      @description('FQDN that resolves to private endpoint IP address.')
      fqdn: string?
  
      @description('A list of private IP addresses of the private endpoint.')
      ipAddresses: string[]
    }[]
  
    @description('The IDs of the network interfaces associated with the private endpoint.')
    networkInterfaceResourceIds: string[]
  }
  
  privateEndpoints: {
    {
      name: 'myPeName'
      privateLinkServiceConnectionName: 'myPrivateLinkConnectionName'
      lock: 'CanNotDelete'
      tags: {
        'hidden-title': 'This is visible in the resource name'
      }
      service: 'blob'
      subnetResourceId: '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myRg/providers/Microsoft.Network/virtualNetworks/myVnet/subnets/mysubnet'
      resourceGroupResourceId: '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myRg'
      applicationSecurityGroupResourceIds: [
        '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myRg/providers/Microsoft.Network/applicationSecurityGroups/myAsg'
      ]
      privateDnsZoneGroup: {
        privateDnsZoneGroupConfigs: [
          {
            name: 'config'
            privateDnsZoneResourceId: '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myRg/providers/Microsoft.Network/privateDnsZones/myZone'
          }
        ]
      }
      customDnsConfigs: [
        {
          fqdn: 'fqdn1.example.com'
          ipAddresses: [
            '10.0.0.1',
            '10.0.0.2'
          ]
        }
      ]
      networkInterfaceName: 'nic1'
      ipConfigurations: [
        {
          name: 'ipconfig1'
          groupId: 'blob'
          memberName: 'default'
          privateIpAddress: '10.0.0.7'
        }
      ]
      roleAssignments: [
        {
          roleDefinitionIdOrName: 'Owner'
          principalId: '11111111-1111-1111-1111-111111111111'
          principalType: 'ServicePrincipal'
        }
        {
          roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions','acdd72a7-3385-48ef-bd42-f606fba81ae7')
          principalId: '11111111-1111-1111-1111-111111111111'
          principalType: 'ServicePrincipal'
        }
      ]
    }
  }
  

Notes:

  • The properties defined in the schema above are the minimum amount of properties expected to be exposed for Private Endpoints in AVM Resource Modules.
    • A module owner MAY chose to expose additional properties of the Private Endpoint resource
      • However, module owners considering this SHOULD contact the AVM core team first to consult on how the property should be exposed to avoid future breaking changes to the schema that may be enforced upon them
  • Module owners MAY chose to define a list of allowed value for the ‘service’ (a.k.a. groupIds) property
    • However, they should do so with caution as should a new service appear for their resource module, a new release will need to be cut to add this new service to the allowed values
      • Whereas not specifying allowed values will allow flexibility from day 0 without the need for any changes and releases to be made

Customer Managed Keys

Customer Managed Keys
  // ============== //
  //   Parameters   //
  // ============== //
  
  import { customerManagedKeyType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('Optional. The customer managed key definition.')
  param customerManagedKey customerManagedKeyType?
  
  // ============= //
  //   Variables   //
  // ============= //
  
  // If user-assiged identities are supported => Adds any user assigned identity specified in the customer managed key definition to the general managed-identity spcification
  var formattedUserAssignedIdentities = reduce(
    map(
      union(
        (managedIdentities.?userAssignedResourceIds ?? []),
        (!empty(customerManagedKey.?userAssignedIdentityResourceId)
          ? [customerManagedKey.?userAssignedIdentityResourceId]
          : [])
      ),
      (id) => { '${id}': {} }
    ),
    {},
    (cur, next) => union(cur, next)
  ) // Converts the flat array to an object like { '${id1}': {}, '${id2}': {} }
  
  var identity = !empty(managedIdentities) || !empty(formattedUserAssignedIdentities) 
    ? {
        type: (managedIdentities.?systemAssigned ?? false)
          ? (!empty(formattedUserAssignedIdentities) ? 'SystemAssigned, UserAssigned' : 'SystemAssigned')
          : (!empty(formattedUserAssignedIdentities) ? 'UserAssigned' : null)
        userAssignedIdentities: !empty(formattedUserAssignedIdentities) ? formattedUserAssignedIdentities : null
      }
    : null
  
  var isHSMManagedCMK = split(customerManagedKey.?keyVaultResourceId ?? '', '/')[?7] == 'managedHSMs'
  
  // ============= //
  //   Resources   //
  // ============= //
  
  resource cMKKeyVault 'Microsoft.KeyVault/vaults@2025-05-01' existing = if (!empty(customerManagedKey) && !isHSMManagedCMK) {
    name: last(split((customerManagedKey!.?keyVaultResourceId!), '/'))
    scope: resourceGroup(
      split(customerManagedKey!.?keyVaultResourceId!, '/')[2],
      split(customerManagedKey!.?keyVaultResourceId!, '/')[4]
    )
  
    resource cMKKey 'keys@2025-05-01' existing = if (!empty(customerManagedKey) && !isHSMManagedCMK) {
      name: customerManagedKey!.?keyName!
    }
  }
  
  resource cMKUserAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' existing = if (!empty(customerManagedKey.?userAssignedIdentityResourceId)) {
    name: last(split(customerManagedKey!.?userAssignedIdentityResourceId!, '/'))
    scope: resourceGroup(
      split(customerManagedKey!.?userAssignedIdentityResourceId!, '/')[2],
      split(customerManagedKey!.?userAssignedIdentityResourceId!, '/')[4]
    )
  }
  
  resource >singularMainResourceType< '>providerNamespace</>resourceType<@>apiVersion<' = {
    name: '>exampleResource<'
    properties: {
      ... // other properties
      encryption: !empty(customerManagedKey)
        ? {
            keySource: 'Microsoft.KeyVault'
            keyVaultProperties: {
              keyVaultUri: !isHSMManagedCMK
                  ? cMKKeyVault!.properties.vaultUri
                  : 'https://${last(split((customerManagedKey!.keyVaultResourceId), '/'))}.managedhsm.azure.net/'
              keyName: customerManagedKey!.keyName
              keyVersion: !empty(customerManagedKey!.?keyVersion)
                ? customerManagedKey!.keyVersion!
                : (!isHSMManagedCMK
                  ? last(split(cMKKeyVault::cMKKey!.properties.keyUriWithVersion, '/'))
                  : fail('Managed HSM CMK encryption requires specifying the \'keyVersion\'.'))
              keyIdentifier: !empty(customerManagedKey!.?keyVersion)
                ? ( !isHSMManagedCMK
                  ? '${cMKKeyVault::cMKKey!.properties.keyUri}/${customerManagedKey!.keyVersion!}'
                  : 'https://${last(split((customerManagedKey!.keyVaultResourceId), '/'))}.managedhsm.azure.net/keys/${customerManagedKey!.keyName}/${customerManagedKey!.keyVersion!}')
                : ( !isHSMManagedCMK
                  ? cMKKeyVault::cMKKey!.properties.keyUriWithVersion
                  : fail('Managed HSM CMK encryption requires specifying the \'keyVersion\'.'))
              identityClientId: !empty(customerManagedKey!.?userAssignedIdentityResourceId)
                ? cMKUserAssignedIdentity!.properties.clientId
                : null
              identity: !empty(customerManagedKey!.?userAssignedIdentityResourceId)
                ? {
                    userAssignedIdentity: cMKUserAssignedIdentity!.id
                  }
                : null
            }
          }
        : null
    }
  }
  
  customerManagedKey: {
    keyVaultResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.KeyVault/vaults/{keyVaultName}'
    keyName: '{keyName}'
    keyVersion: '{keyVersion}'
    userAssignedIdentityResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{uamiName}'
  }
  
  // ============== //
  //   Parameters   //
  // ============== //
  import { customerManagedKeyWithAutoRotateType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('Optional. The customer managed key definition.')
  param customerManagedKey customerManagedKeyWithAutoRotateType?
  
  // ============= //
  //   Variables   //
  // ============= //
  
  // If user-assiged identities are supported => Adds any user assigned identity specified in the customer managed key definition to the general managed-identity spcification
  var formattedUserAssignedIdentities = reduce(
    map(
      union(
        (managedIdentities.?userAssignedResourceIds ?? []),
        (!empty(customerManagedKey.?userAssignedIdentityResourceId)
          ? [customerManagedKey.?userAssignedIdentityResourceId]
          : [])
      ),
      (id) => { '${id}': {} }
    ),
    {},
    (cur, next) => union(cur, next)
  ) // Converts the flat array to an object like { '${id1}': {}, '${id2}': {} }
  
  var identity = !empty(managedIdentities) || !empty(formattedUserAssignedIdentities) 
    ? {
        type: (managedIdentities.?systemAssigned ?? false)
          ? (!empty(formattedUserAssignedIdentities) ? 'SystemAssigned, UserAssigned' : 'SystemAssigned')
          : (!empty(formattedUserAssignedIdentities) ? 'UserAssigned' : null)
        userAssignedIdentities: !empty(formattedUserAssignedIdentities) ? formattedUserAssignedIdentities : null
      }
    : null
    
  var isHSMManagedCMK = split(customerManagedKey.?keyVaultResourceId ?? '', '/')[?7] == 'managedHSMs'
  
  // ============= //
  //   Resources   //
  // ============= //
  
  resource cMKKeyVault 'Microsoft.KeyVault/vaults@2025-05-01' existing = if (!empty(customerManagedKey) && !isHSMManagedCMK) {
    name: last(split((customerManagedKey!.?keyVaultResourceId!), '/'))
    scope: resourceGroup(
      split(customerManagedKey!.?keyVaultResourceId!, '/')[2],
      split(customerManagedKey!.?keyVaultResourceId!, '/')[4]
    )
  
    resource cMKKey 'keys@2025-05-01' existing = if (!empty(customerManagedKey) && !isHSMManagedCMK) {
      name: customerManagedKey!.?keyName!
    }
  }
  
  resource cMKUserAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' existing = if (!empty(customerManagedKey.?userAssignedIdentityResourceId)) {
    name: last(split(customerManagedKey!.?userAssignedIdentityResourceId!, '/'))
    scope: resourceGroup(
      split(customerManagedKey!.?userAssignedIdentityResourceId!, '/')[2],
      split(customerManagedKey!.?userAssignedIdentityResourceId!, '/')[4]
    )
  }
  
  resource >singularMainResourceType< '>providerNamespace</>resourceType<@>apiVersion<' = {
    name: '>exampleResource<'
    properties: {
      ... // other properties
      encryption: !empty(customerManagedKey)
        ? {
            keySource: 'Microsoft.KeyVault'
            keyVaultProperties: {
              keyVaultUri: !isHSMManagedCMK
                  ? cMKKeyVault!.properties.vaultUri
                  : 'https://${last(split((customerManagedKey!.keyVaultResourceId), '/'))}.managedhsm.azure.net/'
              keyName: customerManagedKey!.keyName
              keyVersion: !empty(customerManagedKey!.?keyVersion)
                  ? customerManagedKey!.keyVersion!
                  : (customerManagedKey!.?autoRotationEnabled ?? true)
                      ? null
                      : (!isHSMManagedCMK
                          ? last(split(cMKKeyVault::cMKKey!.properties.keyUriWithVersion, '/'))
                          : fail('Managed HSM CMK encryption requires either specifying the \'keyVersion\' or omitting the \'autoRotationEnabled\' property. Setting \'autoRotationEnabled\' to false without a \'keyVersion\' is not allowed.'))
              keyIdentifier: !empty(customerManagedKey!.?keyVersion)
                ? (!isHSMManagedCMK
                  ? '${cMKKeyVault::cMKKey!.properties.keyUri}/${customerManagedKey!.keyVersion!}'
                  : 'https://${last(split((customerManagedKey!.keyVaultResourceId), '/'))}.managedhsm.azure.net/keys/${customerManagedKey!.keyName}/${customerManagedKey!.keyVersion!}')
                : (customerManagedKey!.?autoRotationEnabled ?? true)
                  ? (!isHSMManagedCMK
                    ? cMKKeyVault::cMKKey!.properties.keyUri
                    : 'https://${last(split((customerManagedKey!.keyVaultResourceId), '/'))}.managedhsm.azure.net/keys/${customerManagedKey!.keyName}')
                  : (!isHSMManagedCMK
                    ? cMKKeyVault::cMKKey!.properties.keyUriWithVersion
                    : fail('Managed HSM CMK encryption requires either specifying the \'keyVersion\' or omitting the \'autoRotationEnabled\' property. Setting \'autoRotationEnabled\' to false without a \'keyVersion\' is not allowed.'))
              identityClientId: !empty(customerManagedKey!.?userAssignedIdentityResourceId)
                ? cMKUserAssignedIdentity!.properties.clientId
                : null
              identity: !empty(customerManagedKey!.?userAssignedIdentityResourceId)
                ? {
                    userAssignedIdentity: cMKUserAssignedIdentity!.id
                  }
                : null
            }
          }
        : null
    }
  }
  
  customerManagedKey: {
    keyVaultResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.KeyVault/vaults/{keyVaultName}'
    keyName: '{keyName}'
    autoRotationEnabled: {true|false}
    userAssignedIdentityResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{uamiName}'
  }
  

Secrets export (DEPRECATED)

Important

Since version Bicep 0.35.1, it is possible to export secrets securely using the secure() annotation.

As this approach is fairly simple compared with the below workaround it is highly recommended to use it instead.

Example

@secure()
@description('The primary connection string of the service bus namespace.')
output primaryConnectionString string = listkeys(
  '${serviceBusNamespace.id}/AuthorizationRules/RootManageSharedAccessKey',
  '2024-01-01'
).primaryConnectionString

@secure()
@description('The primary key of the service bus namespace.')
output primaryKey string = listkeys(
  '${serviceBusNamespace.id}/AuthorizationRules/RootManageSharedAccessKey',
  '2024-01-01'
).primaryKey

Secrets used inside a module can be exported to a Key Vault reference provided as per the below schema.
This implementation provides a secure way around the current limitation of Bicep on providing a secure template output (that can be used for secrets).

The user MUST

  • provide the resource Id to a Key Vault. The principal used for the deployment MUST be allowed to set secrets in this Key Vault.
  • provide a name for each secret they want to store (opt-in). The module will suggest which secrets are available via the implemented user-defined type.

The module returns an output table where the key is the name of the secret the user provided, and the value contains both the secret’s resource Id and URI.

Important

The feature MUST be implemented as per the below schema. Diversions are only allowed in places marked as >text< to ensure a consistent user experience across modules.

User Defined Type, Parameter & Resource Example

  
  // ============== //
  //   Parameters   //
  // ============== //
  
  @description('Optional. Key vault reference and secret settings for the module\'s secrets export.')
  param secretsExportConfiguration secretsExportConfigurationType?
  
  // ============= //
  //   Resources   //
  // ============= //
  
  module secretsExport 'modules/keyVaultExport.bicep' = if (secretsExportConfiguration != null) {
    name: '${uniqueString(deployment().name, location)}-secrets-kv'
    scope: resourceGroup(
      split(secretsExportConfiguration.?keyVaultResourceId, '/')[2],
      split(secretsExportConfiguration.?keyVaultResourceId, '/')[4]
    )
    params: {
      keyVaultName: last(split(secretsExportConfiguration.?keyVaultResourceId, '/'))
      secretsToSet: union(
        [],
        contains(secretsExportConfiguration!, '>secretToExport1<Name')
          ? [
              {
                name: secretsExportConfiguration!.?>secretToExport1<Name
                value: >secretReference1< // e.g., >singularMainResourceType<.listKeys().primaryMasterKey
              }
            ]
          : [],
        contains(secretsExportConfiguration!, '>secretToExport2<Name')
          ? [
              {
                name: secretsExportConfiguration!.?>secretToExport2<Name
                value:>secretReference2<  // e.g., >singularMainResourceType<.listKeys().secondaryMasterKey
              }
            ]
          : []
          // (...)
      )
    }
  }
  
  // =========== //
  //   Outputs   //
  // =========== //
  
  import { secretsOutputType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('A hashtable of references to the secrets exported to the provided Key Vault. The key of each reference is each secret\'s name.')
  output exportedSecrets secretsOutputType = (secretsExportConfiguration != null)
    ? toObject(secretsExport.outputs.secretsSet, secret => last(split(secret.secretResourceId, '/')), secret => secret)
    : {}
  
  // =============== //
  //   Definitions   //
  // =============== //
  
  @export()
  type secretsExportConfigurationType = {
    @description('Required. The resource ID of the key vault where to store the secrets of this module.')
    keyVaultResourceId: string
  
    @description('Optional. The >secretToExport1< secret name to create.')
    >secretToExport1<Name: string?
  
    @description('Optional. The >secretToExport2< secret name to create.')
    >secretToExport2<Name: string?
  
    // (...)
  }
  

Input Example with Values

  
  // ============== //
  //   Parameters   //
  // ============== //
  
  @description('Optional. Key vault reference and secret settings for the module\'s secrets export.')
  param secretsExportConfiguration secretsExportConfigurationType?
  
  // ============= //
  //   Resources   //
  // ============= //
  
  module secretsExport 'modules/keyVaultExport.bicep' = if (secretsExportConfiguration != null) {
    name: '${uniqueString(deployment().name, location)}-secrets-kv'
    scope: resourceGroup(
      split(secretsExportConfiguration.?keyVaultResourceId, '/')[2],
      split(secretsExportConfiguration.?keyVaultResourceId, '/')[4]
    )
    params: {
      keyVaultName: last(split(secretsExportConfiguration.?keyVaultResourceId, '/'))
      secretsToSet: union(
        [],
        contains(secretsExportConfiguration!, '>secretToExport1<Name')
          ? [
              {
                name: secretsExportConfiguration!.?>secretToExport1<Name
                value: >secretReference1< // e.g., >singularMainResourceType<.listKeys().primaryMasterKey
              }
            ]
          : [],
        contains(secretsExportConfiguration!, '>secretToExport2<Name')
          ? [
              {
                name: secretsExportConfiguration!.?>secretToExport2<Name
                value:>secretReference2<  // e.g., >singularMainResourceType<.listKeys().secondaryMasterKey
              }
            ]
          : []
          // (...)
      )
    }
  }
  
  // =========== //
  //   Outputs   //
  // =========== //
  
  import { secretsOutputType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('A hashtable of references to the secrets exported to the provided Key Vault. The key of each reference is each secret\'s name.')
  output exportedSecrets secretsOutputType = (secretsExportConfiguration != null)
    ? toObject(secretsExport.outputs.secretsSet, secret => last(split(secret.secretResourceId, '/')), secret => secret)
    : {}
  
  // =============== //
  //   Definitions   //
  // =============== //
  
  @export()
  type secretsExportConfigurationType = {
    @description('Required. The resource ID of the key vault where to store the secrets of this module.')
    keyVaultResourceId: string
  
    @description('Optional. The >secretToExport1< secret name to create.')
    >secretToExport1<Name: string?
  
    @description('Optional. The >secretToExport2< secret name to create.')
    >secretToExport2<Name: string?
  
    // (...)
  }
  

[modules/keyVaultExport.bicep] file

  
  // ============== //
  //   Parameters   //
  // ============== //
  
  @description('Required. The name of the Key Vault to set the secrets in.')
  param keyVaultName string
  
  import { secretToSetType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('Required. The secrets to set in the Key Vault.')
  param secretsToSet secretToSetType[]
  
  // ============= //
  //   Resources   //
  // ============= //
  
  resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = {
    name: keyVaultName
  }
  
  resource secrets 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = [
    for secret in secretsToSet: {
      name: secret.name
      parent: keyVault
      properties: {
        value: secret.value
      }
    }
  ]
  
  // =========== //
  //   Outputs   //
  // =========== //
  
  import { secretSetOutputType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('The references to the secrets exported to the provided Key Vault.')
  output secretsSet secretSetOutputType[] = [
    #disable-next-line outputs-should-not-contain-secrets // Only returning the references, not a secret value
    for index in range(0, length(secretsToSet ?? [])): {
      secretResourceId: secrets[index].id
      secretUri: secrets[index].properties.secretUri
      secretUriWithVersion: secrets[index].properties.secretUriWithVersion
    }
  ]
  

Output Usage Example

When using a module that implements the above interface, you can access its outputs for example in the following ways:

  
  // ============== //
  //   Parameters   //
  // ============== //
  
  @description('Optional. Key vault reference and secret settings for the module\'s secrets export.')
  param secretsExportConfiguration secretsExportConfigurationType?
  
  // ============= //
  //   Resources   //
  // ============= //
  
  module secretsExport 'modules/keyVaultExport.bicep' = if (secretsExportConfiguration != null) {
    name: '${uniqueString(deployment().name, location)}-secrets-kv'
    scope: resourceGroup(
      split(secretsExportConfiguration.?keyVaultResourceId, '/')[2],
      split(secretsExportConfiguration.?keyVaultResourceId, '/')[4]
    )
    params: {
      keyVaultName: last(split(secretsExportConfiguration.?keyVaultResourceId, '/'))
      secretsToSet: union(
        [],
        contains(secretsExportConfiguration!, '>secretToExport1<Name')
          ? [
              {
                name: secretsExportConfiguration!.?>secretToExport1<Name
                value: >secretReference1< // e.g., >singularMainResourceType<.listKeys().primaryMasterKey
              }
            ]
          : [],
        contains(secretsExportConfiguration!, '>secretToExport2<Name')
          ? [
              {
                name: secretsExportConfiguration!.?>secretToExport2<Name
                value:>secretReference2<  // e.g., >singularMainResourceType<.listKeys().secondaryMasterKey
              }
            ]
          : []
          // (...)
      )
    }
  }
  
  // =========== //
  //   Outputs   //
  // =========== //
  
  import { secretsOutputType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('A hashtable of references to the secrets exported to the provided Key Vault. The key of each reference is each secret\'s name.')
  output exportedSecrets secretsOutputType = (secretsExportConfiguration != null)
    ? toObject(secretsExport.outputs.secretsSet, secret => last(split(secret.secretResourceId, '/')), secret => secret)
    : {}
  
  // =============== //
  //   Definitions   //
  // =============== //
  
  @export()
  type secretsExportConfigurationType = {
    @description('Required. The resource ID of the key vault where to store the secrets of this module.')
    keyVaultResourceId: string
  
    @description('Optional. The >secretToExport1< secret name to create.')
    >secretToExport1<Name: string?
  
    @description('Optional. The >secretToExport2< secret name to create.')
    >secretToExport2<Name: string?
  
    // (...)
  }
  

Which returns a JSON-formatted output like:

  {
    "exportedSecrets": {
      "Type": "Object",
      "Value": {
        ">secretToExportName1<": {
          "secretResourceId": "/subscriptions/<subId>/resourceGroups/<rgName>providers/Microsoft.KeyVault/vaults/<vaultName>/secrets/>secretToExportName1<",
          "secretUri": "https://<vaultName>.vault.azure.net/secrets/>secretToExportName1<"
        },
        ">secretToExportName2<": {
          "secretResourceId": "/subscriptions/<subId>/resourceGroups/<rgName>providers/Microsoft.KeyVault/vaults/<vaultName>/secrets/>secretToExportName2<",
          "secretUri": "https://<vaultName>.vault.azure.net/secrets/>secretToExportName2<"
        }
      }
    },
    "specificSecret": {
      "Type": "String",
      "Value": "/subscriptions/<subId>/resourceGroups/<rgName>providers/Microsoft.KeyVault/vaults/<vaultName>/secrets/>secretToExportName1<"
    },
    "exportedSecretResourceIds": {
      "Type": "Array",
      "Value": [
        "/subscriptions/<subId>/resourceGroups/<rgName>providers/Microsoft.KeyVault/vaults/<vaultName>/secrets/>secretToExportName1<",
        "/subscriptions/<subId>/resourceGroups/<rgName>providers/Microsoft.KeyVault/vaults/<vaultName>/secrets/>secretToExportName2<"
      ]
    }
  }
  

Azure Monitor Alerts

Note

This interface is a SHOULD instead of a MUST and therefore the AVM core team have not mandated a interface schema to use.

Zonal & zone-redundant resources

Many Azure resources can be deployed into specific availability zones. Depending on whether a resource is ‘zonal’ (i.e., deploys a single instance into a single zone) or ‘zone-redundant’ (i.e., spreads multiple of its instances across the configured zones), implementing a different interface is required. Simply put, the zone of a zonal resource must be a required parameter (but give the user the option to ‘opt-out’), while zone-redundant resources must span all available zones by default, but still give the user the option to ‘opt-out’. Please note that the support for Availability Zones may differ from region to region.

  // ============== //
  //   Parameters   //
  // ============== //
  
  @description('Required. If set to 1, 2 or 3, the availability zone is hardcoded to that value. If set to -1, no zone is defined. Note that the availability zone numbers here are the logical availability zone in your Azure subscription. Different subscriptions might have a different mapping of the physical zone and logical zone. To understand more, please refer to [Physical and logical availability zones](https://learn.microsoft.com/en-us/azure/reliability/availability-zones-overview?tabs=azure-cli#physical-and-logical-availability-zones).')
  @allowed([
    -1
    1
    2
    3
  ])
  param availabilityZone int
  
  // ============= //
  //   Resources   //
  // ============= //
  
  resource >singularMainResourceType< '>providerNamespace</>resourceType<@>apiVersion<' = {
    name: '>exampleResource<'
    properties: {
      ... // other properties
      zones: availabilityZone != -1 ? array(string(availabilityZone)) : null // If expecting an array
      // Or
      availabilityZone: availabilityZone != -1 ? string(availabilityZone) : null // If expecting a single value
    }
  }
  
  availabilityZone: -1 // Deploy into no zone
  availabilityZone: 1 // Deploy into zone 1
  
  // ============== //
  //   Parameters   //
  // ============== //
  
  @description('Optional. The list of Availability zones to use for the zone-redundant resources.')
  @allowed([
    1
    2
    3
  ])
  param availabilityZones int[] = [1, 2, 3]
  
  // ============= //
  //   Resources   //
  // ============= //
  
  resource >singularMainResourceType< '>providerNamespace</>resourceType<@>apiVersion<' = {
    name: '>exampleResource<'
    properties: {
      ... // other properties
      zones: map(availabilityZones, zone => '${zone}')
    }
  }
  
  availabilityZones: [] // Deploy into no zone
  availabilityZones: [1, 2] // Deploy into zone 1 & 2
  

Bicep Pattern Module Specifications

Contribution / Support

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR8Module Owner(s) GitHubMUSTOwnerInitial
2SNFR20GitHub Teams OnlyMUSTOwnerInitial
3SNFR9AVM & PG Teams GitHub Repo PermissionsMUSTOwnerInitial
4SNFR10MIT LicensingMUSTOwnerInitial
5SNFR11Issues Response TimesMUSTOwnerContributorBAU
6SNFR12Versions SupportedMUSTOwnerBAU
7SNFR23GitHub Repo LabelsMUSTOwnerBAU
8BCPNFR15AVM Module Issue template fileMUSTOwnerBAU
➕ See Specifications for this category
See origin...

ID: SNFR8 - Category: Contribution/Support - Module Owner(s) GitHub

A module MUST have an owner that is defined and managed by a GitHub Team in the Azure GitHub organization.

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

The names for the GitHub teams for each approved module are already defined in the respective Module Indexes. These teams MUST be created (and used) for each module.




See origin...

ID: SNFR20 - Category: Contribution/Support - GitHub Teams Only

All GitHub repositories that AVM module are published from and hosted within MUST only assign GitHub repository permissions to GitHub teams only.

Each module MUST have a GitHub team assigned for module owners. This team MUST be created in the Azure organization in GitHub.

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

Important

As part of the module proposal process, the name of the GitHub team for each approved module is already defined in the respective Module Indexes (or CSV file). This team MUST be created (and used) for each module.

Module owners don’t need to construct the name of the GitHub team for their module themselves, instead they need use the name prescribed in the related CSV file, at the time of approval.

For a direct link, see the list of related index pages:

The @Azure prefix in the last column of the tables linked above represents the “Azure” GitHub organization all AVM-related repositories exist in. DO NOT include this segment in the team’s name!

Naming Convention

The naming convention for the GitHub teams MUST follow the below pattern:

  • <hyphenated module name>-module-owners-bicep - to grant permissions for module owners on Bicep modules

Segments:

  • <hyphenated module name> == the AVM Module’s name, with each segment separated by dashes, i.e., avm-res-<resource provider>-<ARM resource type>
    • See RMNFR1 for AVM Resource Module Naming
    • See PMNFR1 for AVM Pattern Module Naming
  • module-owners == the role the GitHub Team is assigned to
  • <bicep == the language the module is written in

Examples:

  • avm-res-compute-virtualmachine-module-owners-bicep
Note

The naming convention for Bicep modules is slightly different than the naming convention for their respective GitHub teams.

Add Team Members

All officially documented module owner(s) MUST be added to the -module-owners- team. The -module-owners- team MUST NOT have any other members.

Unless explicitly requested and agreed, members of the AVM core team or any PG teams MUST NOT be added to the -module-owners- teams as permissions for them are granted through the teams described in SNFR9.

Grant permissions through team memberships

Note

In case of Bicep modules, permissions to the BRM repository (the repo of the Bicep Registry) are granted via assigning the -module-owners- teams to parent teams that already have the required level access configured. While it is the module owner’s responsibility to initiate the addition of their team to the respective parent, only the AVM core team can approve this parent-child relationship.

Module owners MUST create their -module-owners- team and as part of the provisioning process, they MUST request the addition of this team to its respective parent team (see the table below for details).

GitHub Team NameDescriptionPermissionsPermissions granted throughWhere to work?
<hyphenated module name>-module-owners-bicepAVM Bicep Module Owners - <module name>WriteAssignment to the avm-technical-reviewers-bicep parent team.Need to work in a fork.

Example - GitHub team required for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

  • avm-res-network-virtualnetwork-module-owners-bicep –> assign to the avm-technical-reviewers-bicep parent team.
Tip

Direct link to create a new GitHub team and assign it to its parent: Create new team

Fill in the values as follows:

  • Team name: Following the naming convention described above, use the value defined in the module indexes.
  • Description: Follow the guidance above (see the Description column in the table above).
  • Parent team: Follow the guidance above (see the Permissions granted through column in the table above).
  • Team visibility: Visible
  • Team notifications: Enabled

CODEOWNERS file

As part of the “initial Pull Request” (that publishes the first version of the module), module owners MUST add an entry to the CODEOWNERS file in the BRM repository (here).

Note

Through this approach, the AVM core team will grant review permission to module owners as part of the standard PR review process.

Every CODEOWNERS entry (line) MUST include the following segments separated by a single whitespace character:

  • Path of the module, relative to the repo’s root, e.g.: /avm/res/network/virtual-network/
  • The -module-owners-team, with the @Azure/ prefix, e.g., @Azure/avm-res-network-virtualnetwork-module-owners-bicep
  • The GitHub team of the AVM Bicep reviewers, with the @Azure/ prefix, i.e., @Azure/avm-module-reviewers-bicep

Example - CODEOWNERS entry for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

  • /avm/res/network/virtual-network/ @Azure/avm-res-network-virtualnetwork-module-owners-bicep @Azure/avm-module-reviewers-bicep

Terraform

Note

Access management for Terraform repositories now uses a single team, membership of which is managed using an internal entitlement management tool (Core Identity).

All module owners MUST request access to the avm-module-owners-terraform GitHub team via the Azure Verified Module Owners Terraform entitlement in Core Identity (Microsoft internal tool).




See origin...

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

Note

These required GitHub teams are already associated to the BRM repository and have the required permissions.

Terraform

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.




See origin...

ID: SNFR10 - Category: Contribution/Support - MIT Licensing

A module MUST be published with the MIT License in the Azure GitHub organization.




See origin...

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.




See origin...

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.




See origin...

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:

➕ AVM Standard GitHub Labels

These labels are available in a CSV file from here

NameDescriptionHEX
AZD 🧑‍💻These modules are requested/used by the AZD team.
E0BFFA
Needs: Attention 👋Reply has been added to issue, maintainer to review
E99695
Needs: Immediate Attention ‼️Immediate attention of module owner / AVM team is needed
FF0000
Needs: Author Feedback 👂Awaiting feedback from the issue/PR author
F18A07
Needs: External Changes ⚒️When an issue/PR requires changes that are outside of the control of the module. e.g. to an RP.
DE389D
Needs: More Evidence ⚖We are looking for more evidence to make a decision on this
F64872
Needs: Triage 🔍Maintainers need to triage still
FBCA04
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.
FF0019
Needs: Module Contributor 📣This module needs secondary owner(s) or contributor(s) to develop or maintain it
C95474
Needs: Core Team 🧞‍♂️This item needs the AVM Core Team to review it
DB4503
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
800080
Status: Do Not Merge ⛔Do not merge PRs with this label attached as they are not ready or aligned to future direction etc.
8B4513
Status: External Contribution 🌍This is being worked on by someone outside of the AVM module owners/contributors or AVM core team
D8FA2C
Status: Fixed ✅Auto label applied when issue fixed by merged PR
90EE90
Status: Help Wanted 🆘Extra attention is needed
FF4500
Status: In Triage 🔍Picked up for triaging by an AVM core team member
D4AF37
Status: In PR 👉This is when an issue is due to be fixed in an open PR
EDEDED
Status: Invalid ❌This doesn't seem right
E4E669
Status: Long Term ⏳We will do it, but will take a longer amount of time due to complexity/priorities
B60205
Status: No Recent Activity 💤When an issue/PR has not been modified for X amount of days
808080
Status: Won't Fix 💔This will not be worked on
FFFFFF
Status: Owners Identified 🤘This module has its owners identified
FBEF2A
Status: Module Available 🟢The module is published
C8E6C9
Status: Module Deprecated 🔴This is a request to deprecate a module
000000
Status: Module Orphaned 🟡The module has no owner and is therefore orphaned at this time
F4A460
Status: Ready For Repository Creation 📝This module is approved and the owner is ready for the repository to be created (Terraform)
136A41
Status: Repository Created 📄This module has had it's repository created and configured ready for owner contribution (Terraform)
27AB03
Status: Response Overdue 🚩When an issue/PR has not been responded to for X amount of days
850000
Status: Looking For Assistance 🦆This item is looking for anyone to help develop the code and submit a PR for resolution
03FCC2
Type: Bug 🐛Something isn't working
D73A4A
Type: CI 🚀This issue is related to the AVM CI
74CFB0
Type: Documentation 📄Improvements or additions to documentation
0075CA
Type: Duplicate 🤲This issue or pull request already exists
CFD3D7
Type: Feature Request ➕New feature or request
A2EEEF
Type: Hygiene 🧹things related to testing, issue triage etc.
17016A
Type: New Module Proposal 💡A new module for AVM is being proposed
ADD8E6
Type: Question/Feedback 🙋‍♀️Further information is requested or just some feedback
CB6BA2
Type: Security Bug 🔒This is a security bug
FFFF00
Type: AVM 🅰️ ✌️ ⓜ️This is an AVM related issue
F0FFFF
Language: Terraform 🌐This is related to the Terraform IaC language
7740B6
Language: Bicep 💪This is related to the Bicep IaC language
1D73B3
Class: Resource Module 📦This is a resource module
D3D3D3
Class: Pattern Module 📦This is a pattern module
A9A9A9
Class: Utility Module 📦This is a utility module
CAD1DE
Class: Child Module 📦This is a child module
5E5186

To help apply these to a module GitHub repository you can use the below PowerShell script:

➕ Set-AvmGitHubLabels.ps1

For most scenario this is the command you’ll need to call the below PowerShell script with, replacing the value for RepositoryName:

  Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -CreateCsvLabelExports $false -NoUserPrompts $true
```shell
# Linux / MacOs
# For Windows replace $PWD with your the local path or your repository
#
docker run -it -v $PWD:/repo -w /repo mcr.microsoft.com/powershell pwsh -Command '
    #Invoke-WebRequest -Uri "https://azure.github.io/Azure-Verified-Modules/scripts/Set-AvmGitHubLabels.ps1" -OutFile "Set-AvmGitHubLabels.ps1"
    $gh_version = "2.44.1"
    Invoke-WebRequest -Uri "https://github.com/cli/cli/releases/download/v2.44.1/gh_2.44.1_linux_amd64.tar.gz" -OutFile "gh_$($gh_version)_linux_amd64.tar.gz"
    apt-get update && apt-get install -y git
    tar -xzf "gh_$($gh_version)_linux_amd64.tar.gz"
    ls -lsa
    mv "gh_$($gh_version)_linux_amd64/bin/gh" /usr/local/bin/
    rm "gh_$($gh_version)_linux_amd64.tar.gz" && rm -rf "gh_$($gh_version)_linux_amd64"
    gh --version
    ls -lsa
    gh auth login
    $OrgProject = "Azure/terraform-azurerm-avm-res-kusto-cluster"
    gh auth status
    ./Set-AvmGitHubLabels.ps1 -RepositoryName $OrgProject -CreateCsvLabelExports $false -NoUserPrompts $true

  '
```

By default this script will only update and append labels on the repository specified. However, this can be changed by setting the parameter -UpdateAndAddLabelsOnly to $false, which will remove all the labels from the repository first and then apply the AVM labels from the CSV only.

Make sure you elevate your privilege to admin level or the labels will not be applied to your repository. Go to repos.opensource.microsoft.com/orgs/Azure/repos/ to request admin access before running the script.

Full Script:

These Set-AvmGitHubLabels.ps1 can be downloaded from here.

  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Coloured output required in this script")]
  
  <#
  .SYNOPSIS
    This script can be used to create the Azure Verified Modules (AVM) standard GitHub labels to a GitHub repository.
  
  .DESCRIPTION
    This script can be used to create the Azure Verified Modules (AVM) standard GitHub labels to a GitHub repository.
  
    By default, the script will remove all pre-existing labels and apply the AVM labels. However, this can be changed by using the -RemoveExistingLabels parameter and setting it to $false. The tool will also output the labels that exist in the repository before and after the script has run to a CSV file in the current directory, or a directory specified by the -OutputDirectory parameter.
  
    The AVM labels to be created are documented here: TBC
  
  .NOTES
    Please ensure you have specified the GitHub repositry correctly. The script will prompt you to confirm the repository name before proceeding.
  
  .COMPONENT
    You must have the GitHub CLI installed and be authenticated to a GitHub account with access to the repository you are applying the labels to before running this script.
  
  .LINK
    TBC
  
  .Parameter RepositoryName
    The name of the GitHub repository to apply the labels to.
  
  .Parameter RemoveExistingLabels
    If set to $true, the default value, the script will remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels. If set to $false, the script will not remove any pre-existing labels.
  
  .Parameter UpdateAndAddLabelsOnly
    If set to $true, the default value, the script will only update and add labels to the repository specified in -RepositoryName. If set to $false, the script will remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels.
  
  .Parameter OutputDirectory
    The directory to output the pre-existing and post-existing labels to in a CSV file. The default value is the current directory.
  
  .Parameter CreateCsvLabelExports
    If set to $true, the default value, the script will output the pre-existing and post-existing labels to a CSV file in the current directory, or a directory specified by the -OutputDirectory parameter. If set to $false, the script will not output the pre-existing and post-existing labels to a CSV file.
  
  .Parameter GitHubCliLimit
    The maximum number of labels to return from the GitHub CLI. The default value is 999.
  
  .Parameter LabelsToApplyCsvUri
    The URI to the CSV file containing the labels to apply to the GitHub repository. The default value is https://raw.githubusercontent.com/jtracey93/label-source/main/avm-github-labels.csv.
  
  .Parameter NoUserPrompts
    If set to $true, the default value, the script will not prompt the user to confirm they want to remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels. If set to $false, the script will prompt the user to confirm they want to remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels.
  
    This is useful for running the script in automation workflows
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and remove all pre-existing labels.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo"
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -RemoveExistingLabels $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and output the pre-existing and post-existing labels to the directory C:\GitHubLabels.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels"
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and output the pre-existing and post-existing labels to the directory C:\GitHubLabels and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels" -RemoveExistingLabels $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not create the pre-existing and post-existing labels CSV files and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -RemoveExistingLabels $false -CreateCsvLabelExports $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not create the pre-existing and post-existing labels CSV files and do not remove any pre-existing labels, just overwrite any labels that have the same name. Finally, use a custom CSV file hosted on the internet to create the labels from.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels" -RemoveExistingLabels $false -CreateCsvLabelExports $false -LabelsToApplyCsvUri "https://example.com/csv/avm-github-labels.csv"
  
  #>
  
  #Requires -PSEdition Core
  
  [CmdletBinding()]
  param (
    [Parameter(Mandatory = $true)]
    [string]$RepositoryName,
  
    [Parameter(Mandatory = $false)]
    [bool]$RemoveExistingLabels = $true,
  
    [Parameter(Mandatory = $false)]
    [bool]$UpdateAndAddLabelsOnly = $true,
  
    [Parameter(Mandatory = $false)]
    [bool]$CreateCsvLabelExports = $true,
  
    [Parameter(Mandatory = $false)]
    [string]$OutputDirectory = (Get-Location),
  
    [Parameter(Mandatory = $false)]
    [int]$GitHubCliLimit = 999,
  
    [Parameter(Mandatory = $false)]
    [string]$LabelsToApplyCsvUri = "https://azure.github.io/Azure-Verified-Modules/governance/avm-standard-github-labels.csv",
  
    [Parameter(Mandatory = $false)]
    [bool]$NoUserPrompts = $false
  )
  
  # Check if the GitHub CLI is installed
  $GitHubCliInstalled = Get-Command gh -ErrorAction SilentlyContinue
  if ($null -eq $GitHubCliInstalled) {
    throw "The GitHub CLI is not installed. Please install the GitHub CLI and try again."
  }
  Write-Host "The GitHub CLI is installed..." -ForegroundColor Green
  
  # Check if GitHub CLI is authenticated
  $GitHubCliAuthenticated = gh auth status
  if ($LASTEXITCODE -ne 0) {
    Write-Host $GitHubCliAuthenticated -ForegroundColor Red
    throw "Not authenticated to GitHub. Please authenticate to GitHub using the GitHub CLI, `gh auth login`, and try again."
  }
  Write-Host "Authenticated to GitHub..." -ForegroundColor Green
  
  # Check if GitHub repository name is valid
  $GitHubRepositoryNameValid = $RepositoryName -match "^[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$"
  if ($false -eq $GitHubRepositoryNameValid) {
    throw "The GitHub repository name $RepositoryName is not valid. Please check the repository name and try again. The format must be <OrgName>/<RepoName>"
  }
  
  # List GitHub repository provided and check it exists
  $GitHubRepository = gh repo view $RepositoryName
  if ($LASTEXITCODE -ne 0) {
    Write-Host $GitHubRepository -ForegroundColor Red
    throw "The GitHub repository $RepositoryName does not exist. Please check the repository name and try again."
  }
  Write-Host "The GitHub repository $RepositoryName exists..." -ForegroundColor Green
  
  # PRE - Get the current GitHub repository labels and export to a CSV file in the current directory or where -OutputDirectory specifies if set to a valid directory path and the directory exists or can be created if it does not exist already
  if ($RemoveExistingLabels -or $UpdateAndAddLabelsOnly) {
    Write-Host "Getting the current GitHub repository (pre) labels for $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
  
    if ($null -ne $GitHubRepositoryLabels -and $CreateCsvLabelExports -eq $true) {
      $csvFileNamePathPre = "$OutputDirectory\$($RepositoryName.Replace('/', '_'))-Labels-Pre-$(Get-Date -Format FileDateTime).csv"
      Write-Host "Exporting the current GitHub repository (pre) labels for $RepositoryName to $csvFileNamePathPre" -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | Export-Csv -Path $csvFileNamePathPre -NoTypeInformation
    }
  }
  
  # Remove all pre-existing labels if -RemoveExistingLabels is set to $true and user confirms they want to remove all pre-existing labels
  if ($null -ne $GitHubRepositoryLabels) {
    $GitHubRepositoryLabelsJson = $GitHubRepositoryLabels | ConvertFrom-Json
    if ($RemoveExistingLabels -eq $true -and $NoUserPrompts -eq $false -and $UpdateAndAddLabelsOnly -eq $false) {
      $RemoveExistingLabelsConfirmation = Read-Host "Are you sure you want to remove all $($GitHubRepositoryLabelsJson.Count) pre-existing labels from $($RepositoryName)? (Y/N)"
      if ($RemoveExistingLabelsConfirmation -eq "Y") {
        Write-Host "Removing all pre-existing labels from $RepositoryName..." -ForegroundColor Yellow
        $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
          Write-Host "Removing label $($_.name) from $RepositoryName..." -ForegroundColor DarkRed
          gh label delete -R $RepositoryName $_.name --yes
        }
      }
    }
    if ($RemoveExistingLabels -eq $true -and $NoUserPrompts -eq $true -and $UpdateAndAddLabelsOnly -eq $false) {
      Write-Host "Removing all pre-existing labels from $RepositoryName..." -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
        Write-Host "Removing label $($_.name) from $RepositoryName..." -ForegroundColor DarkRed
        gh label delete -R $RepositoryName $_.name --yes
      }
    }
  }
  if ($null -eq $GitHubRepositoryLabels) {
    Write-Host "No pre-existing labels to remove or not selected to be removed from $RepositoryName..." -ForegroundColor Magenta
  }
  
  # Check LabelsToApplyCsvUri is valid and contains a CSV content
  Write-Host "Checking $LabelsToApplyCsvUri is valid..." -ForegroundColor Yellow
  $LabelsToApplyCsvUriValid = $LabelsToApplyCsvUri -match "^https?://"
  if ($false -eq $LabelsToApplyCsvUriValid) {
    throw "The LabelsToApplyCsvUri $LabelsToApplyCsvUri is not valid. Please check the URI and try again. The format must be a valid URI."
  }
  Write-Host "The LabelsToApplyCsvUri $LabelsToApplyCsvUri is valid..." -ForegroundColor Green
  
  # Create AVM lables from the AVM labels CSV file stored on the web using the convertfrom-csv cmdlet
  $avmLabelsCsv = Invoke-WebRequest -Uri $LabelsToApplyCsvUri | ConvertFrom-Csv
  
  # Check if the AVM labels CSV file contains the following columns: Name, Description, HEX
  $avmLabelsCsvColumns = $avmLabelsCsv | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name
  $avmLabelsCsvColumnsValid = $avmLabelsCsvColumns -contains "Name" -and $avmLabelsCsvColumns -contains "Description" -and $avmLabelsCsvColumns -contains "HEX"
  if ($false -eq $avmLabelsCsvColumnsValid) {
    throw "The labels CSV file does not contain the required columns: Name, Description, HEX. Please check the CSV file and try again. It contains the following columns: $avmLabelsCsvColumns"
  }
  Write-Host "The labels CSV file contains the required columns: Name, Description, HEX" -ForegroundColor Green
  
  # Create the AVM labels in the GitHub repository
  Write-Host "Creating/Updating the $($avmLabelsCsv.Count) AVM labels in $RepositoryName..." -ForegroundColor Yellow
  $avmLabelsCsv | ForEach-Object {
    if ($GitHubRepositoryLabelsJson.name -contains $_.name) {
      Write-Host "The label $($_.name) already exists in $RepositoryName. Updating the label to ensure description and color are consitent..." -ForegroundColor Magenta
      gh label create -R $RepositoryName "$($_.name)" -c $_.HEX -d $($_.Description) --force
    }
    else {
      Write-Host "The label $($_.name) does not exist in $RepositoryName. Creating label $($_.name) in $RepositoryName..." -ForegroundColor Cyan
      gh label create -R $RepositoryName "$($_.Name)" -c $_.HEX -d $($_.Description) --force
    }
  }
  
  # POST - Get the current GitHub repository labels and export to a CSV file in the current directory or where -OutputDirectory specifies if set to a valid directory path and the directory exists or can be created if it does not exist already
  if ($CreateCsvLabelExports -eq $true) {
    Write-Host "Getting the current GitHub repository (post) labels for $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
  
    if ($null -ne $GitHubRepositoryLabels) {
      $csvFileNamePathPre = "$OutputDirectory\$($RepositoryName.Replace('/', '_'))-Labels-Post-$(Get-Date -Format FileDateTime).csv"
      Write-Host "Exporting the current GitHub repository (post) labels for $RepositoryName to $csvFileNamePathPre" -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | Export-Csv -Path $csvFileNamePathPre -NoTypeInformation
    }
  }
  
  # If -RemoveExistingLabels is set to $true and user confirms they want to remove all pre-existing labels check that only the avm labels exist in the repository
  if ($RemoveExistingLabels -eq $true -and ($RemoveExistingLabelsConfirmation -eq "Y" -or $NoUserPrompts -eq $true) -and $UpdateAndAddLabelsOnly -eq $false) {
    Write-Host "Checking that only the AVM labels exist in $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
    $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
      if ($avmLabelsCsv.Name -notcontains $_.name) {
        throw "The label $($_.name) exists in $RepositoryName but is not in the CSV file."
      }
    }
    Write-Host "Only the CSV labels exist in $RepositoryName..." -ForegroundColor Green
  }
  
  Write-Host "The CSV labels have been created/updated in $RepositoryName..." -ForegroundColor Green
  



See origin...

ID: BCPNFR15 - Category: Contribution/Support - AVM Module Issue template file

Module owners MUST add an entry to the AVM Module Issue template file in the BRM repository (here). When the module is deprecated, this entry MUST be removed from the file.

Note

Through this approach, the AVM core team will allow raising a bug or feature request for a module, only after the module gets merged to the BRM repository.

The module name entry MUST be added to the dropdown list with id module-name-dropdown as an option, in alphabetical order.

Important

Module owners MUST ensure that the module name is added in alphabetical order, to simplify selecting the right module name when raising an AVM module issue.

Example - AVM Module Issue template module name entry for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

- type: dropdown
  id: module-name-dropdown
  attributes:
    label: Module Name
    description: Which existing AVM module is this issue related to?
    options:
      ...
      - "avm/res/network/virtual-network"
      ...



Telemetry

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SFR3Deployment/Usage TelemetryMUSTOwnerInitial
2SFR4Telemetry Enablement FlexibilityMUSTOwnerInitial
3BCPFR4Telemetry EnablementMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

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 more details on this requirement, here.)

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.ps1 utility
  • Terraform: Executing the make docs command with the note and header ## Data Collection being placed in the module’s _footer.md beforehand
### 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

We will maintain a set of CSV files in the AVM Central Repo (Azure/Azure-Verified-Modules) with the required TelemetryId prefixes to enable checks to utilize this list to ensure the correct IDs are used. To see the formatted content of these CSV files with additional information, please visit the AVM Module Indexes page.

The value you need to use for your module is defined in the related module index. You can look it up on the index pages for Resource Modules, Pattern Modules and Utility Modules.

The ARM deployment name used for the telemetry MUST follow the pattern and MUST be no longer than 64 characters in length: 46d3xbcp.<res/ptn>.<(short) module name>.<version>.<uniqueness>

  • <res/ptn> == AVM Resource or Pattern Module
  • <(short) module name> == The AVM Module’s, possibly shortened, name including the resource provider and the resource type, without;
    • The prefixes: avm-res-
    • The prefixes: avm-ptn-
  • <version> == The AVM Module’s MAJOR.MINOR version (only) with . (periods) replaced with - (hyphens), to allow simpler splitting of the ARM deployment name
  • <uniqueness> == This section of the ARM deployment name is to be used to ensure uniqueness of the deployment name.
    • This is to cater for the following scenarios:
      • The module is deployed multiple times to the same:
        • Location/Region
        • Scope (Tenant, Management Group,Subscription, Resource Group)
Note

Due to the 64-character length limit of Azure deployment names, the <(short) module name> segment has a length limit of 36 characters, so if the module name is longer than that, it MUST be truncated to 36 characters. If any of the semantic version’s segments are longer than 1 character, it further restricts the number of characters that can be used for naming the module.

An example deployment name for the AVM Virtual Machine Resource Module would be: 46d3xbcp.res.compute-virtualmachine.1-2-3.eum3

An example deployment name for a shortened module name would be: 46d3xbcp.res.desktopvirtualization-appgroup.1-2-3.eum3

Tip

Terraform: Terraform uses a telemetry provider, the configuration of which is the same for every module and is included in the template repo.

General: See the language specific contribution guides for detailed guidance and sample code to use in AVM modules to achieve this requirement.

Terraform

To enable telemetry data collection for Terraform modules, the modtm telemetry provider MUST be used. This lightweight telemetry provider sends telemetry data to Azure Application Insights via a HTTP POST front end service.

The modtm telemetry provider is included in all Terraform modules and is enabled by default through the main.telemetry.tf file being automatically distributed from the template repo.

The modtm provider MUST be listed under the required_providers section in the module’s terraform.tf file using the following entry. This is also validated by the linter.

terraform {
  required_providers {
    # .. other required providers as needed
    modtm = {
      source = "Azure/modtm"
      version = "~> 0.3"
    }
  }
}



See origin...

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

Currently, no further requirements apply.




See origin...

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).

@description('Optional. Location for all resources.')
param location string = resourceGroup().location

@description('Optional. Enable/Disable usage telemetry for module.')
param enableTelemetry bool = true

#disable-next-line no-deployments-resources
resource avmTelemetry 'Microsoft.Resources/deployments@2025-04-01' = if (enableTelemetry) {
  name: take('46d3xbcp.res.compute-virtualmachine.${replace('-..--..-', '.', '-')}.${substring(uniqueString(deployment().name, location), 0, 4)}', 64)
  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
#IDTitleSeverityPersonaLifecycle
1SFR1Preview ServicesMUSTOwnerBAU
2SFR2WAF AlignedSHOULDOwnerBAU
3SFR5Availability ZonesMUSTOwnerInitial
4SFR6Data RedundancyMUSTOwnerInitial
5SNFR25Resource NamingMUSTOwnerInitial
6PMFR1Resource Group CreationMAYOwnerContributorBAU
7PMNFR1Module NamingMUSTOwnerInitial
8PMNFR2Use Resource Modules to Build a Pattern ModuleMUSTOwnerContributorBAU
9PMNFR3Use other Pattern Modules to Build a Pattern ModuleMUSTOwnerContributorBAU
10BCPFR1Cross-Referencing ModulesMAYOwnerContributorBAU
11BCPFR2Role Assignments Role Definition MappingMUSTOwnerContributorBAU
12BCPFR6Cross-Referencing Child-ModulesMUSTOwnerContributorBAU
13BCPNFR19User-defined types - NamingMUSTOwnerContributorBAU
14BCPNFR23Module compositionMUSTOwnerContributorBAU
15BCPNFR24Deterministic Deployment NamesMUSTOwnerContributorBAU
16BCPNFR5Role Assignments Role Definition Mapping LimitsSHOULDOwnerContributorBAU
17BCPNFR6Role Assignments Role Definition Mapping Compulsory RolesMUSTOwnerContributorBAU
18BCPNFR14VersioningMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

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., is 2021-05-01-preview
    • Otherwise the latest “non-preview” version of the API SHOULD be used

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”



See origin...

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:

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.




See origin...

ID: SFR5 - Category: Composition - Availability Zones

Modules that deploy zone-redundant resources MUST enable the spanning across as many zones as possible by default, typically all 3.

Modules that deploy zonal resources MUST provide the ability to specify a zone for the resources to be deployed/pinned to. However, they MUST NOT default to a particular zone by default, e.g. 1 in an effort to make the consumer aware of the zone they are selecting to suit their architecture requirements.

For both scenarios the modules MUST expose these configuration options via configurable parameters/variables.

Note

For information on the differences between zonal and zone-redundant services, see Availability zone service and regional support




See origin...

ID: SFR6 - Category: Composition - Data Redundancy

Modules that deploy resources or patterns that support data redundancy SHOULD enable this to the highest possible value by default, e.g. RA-GZRS. When a resource or pattern doesn’t provide the ability to specify data redundancy as a simple property, e.g. GRS etc., then the modules MUST provide the ability to enable data redundancy for the resources or pattern via parameters/variables.

For example, a Storage Account module can simply set the sku.name property to Standard_RAGZRS. Whereas a SQL DB or Cosmos DB module will need to expose more properties, via parameters/variables, to allow the specification of the regions to replicate data to as per the consumers requirements.

Note

For information on the data redundancy options in Azure, see Cross-region replication in Azure




See origin...

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.




See origin...

ID: PMFR1 - Category: Composition - Resource Group Creation

A Pattern Module MAY create Resource Group(s).




See origin...

ID: PMNFR1 - Category: Naming - Module Naming

Pattern Modules MUST follow the below naming conventions (all lower case).

Important

As part of the module proposal process, the module’s approved name is captured both in the module proposal issue AND the related module index page (backed by the corresponding CSV file).

Therefore, module owners don’t need to construct the module’s name themselves, instead they need use the name prescribed in the module proposal issue or in the related CSV file, at the time of approval.

Bicep Pattern Module Naming

  • Naming convention: avm/ptn/<hyphenated grouping/category name>/<hyphenated pattern module name>
  • Example: avm/ptn/compute/app-tier-vmss or avm/ptn/avd-lza/management-plane or avm/ptn/3-tier/web-app
  • Segments:
    • ptn defines this as a pattern module
    • <hyphenated grouping/category name> is a hierarchical grouping of pattern modules by category, with each word separated by dashes, such as:
      • project name, e.g., avd-lza,
      • primary resource provider, e.g., compute or network, or
      • architecture, e.g., 3-tier
    • <hyphenated pattern module name> is a term describing the module’s function, with each word separated by dashes, e.g., app-tier-vmss = Application Tier VMSS; management-plane = Azure Virtual Desktop Landing Zone Accelerator Management Plane

Terraform Pattern Module Naming

  • Naming convention:
    • avm-ptn-<pattern module name> (Module name for registry)
    • terraform-<provider>-avm-ptn-<pattern module name> (GitHub repository name to meet registry naming requirements)
  • Example: avm-ptn-apptiervmss or avm-ptn-avd-lza-managementplane
  • Segments:
    • <provider> is a legacy requirement of the Terraform registry. This must be set to azure
    • ptn defines this as a pattern module
    • <pattern module name> is a term describing the module’s function, e.g., apptiervmss = Application Tier VMSS; avd-lza-managementplane = Azure Virtual Desktop Landing Zone Accelerator Management Plane



See origin...

ID: PMNFR2 - Category: Composition - Use Resource Modules to Build a Pattern Module

A Pattern Module SHOULD be built from AVM Resources Modules to establish a standardized code base and improve maintainability. If a valid reason exists, a pattern module MAY contain native resources (“vanilla” code) where it’s necessary. A Pattern Module MUST NOT contain references to non-AVM modules.

Valid reasons for not using a Resource Module for a resource required by a Pattern Module include but are not limited to:

  • When using a Resource Module would result in hitting scaling limitations and/or would reduce the capabilities of the Pattern Module due to the limitations of Azure Resource Manager.
  • Developing a Pattern Module under time constraint, without having all required Resource Modules readily available.
Note

In the latter case, the Pattern Module SHOULD be updated to use the Resource Module when the required Resource Module becomes available, to avoid accumulating technical debt. Ideally, all required Resource Modules SHOULD be developed first, and then leveraged by the Pattern Module.




See origin...

ID: PMNFR3 - Category: Composition - Use other Pattern Modules to Build a Pattern Module

A Pattern Module MAY contain and be built using other AVM Pattern Modules. A Pattern Module MUST NOT contain references to non-AVM modules.




See origin...

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.




See origin...

ID: BCPFR2 - Category: Composition - Role Assignments Role Definition Mapping

Module owners MAY define common RBAC Role Definition names and IDs within a variable to allow consumers to define a RBAC Role Definition by their name rather than their ID, this should be self contained within the module themselves.

However, they MUST use only the official RBAC Role Definition name within the variable and nothing else.

To meet the requirements of BCPFR2, BCPNFR5 and BCPNFR6 you MUST use the below code sample in your AVM Modules to achieve this.

  @description('''Required. You can provide either the display name (note not all roles are supported, check module documentation) of the role definition, or its fully qualified ID in the following format: `/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11`.''')
  param roleDefinitionIdOrName string
  
  var builtInRbacRoleNames = {
    Owner: '/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635'
    Contributor: '/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c'
    Reader: '/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7'
    'Role Based Access Control Administrator (Preview)': '/providers/Microsoft.Authorization/roleDefinitions/f58310d9-a9f6-439a-9e8d-f62e7b41a168'
    'User Access Administrator': '/providers/Microsoft.Authorization/roleDefinitions/18d7d88d-d35e-4fb5-a5c3-7773c20a72d9'
    //Other RBAC Role Definitions Names & IDs can be added here as needed for your module
  }
  
  var roleDefinitionIdMappedResult = (contains(builtInRbacRoleNames, roleDefinitionIdOrName) ? builtInRbacRoleNames[roleDefinitionIdOrName] : roleDefinitionIdOrName)
  
  resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
    //Other properties removed for ease of reading
    properties: {
      roleDefinitionId: roleDefinitionIdMappedResult
      //Other properties removed for ease of reading
    }
  }
  



See origin...

ID: BCPFR6 - Cross-Referencing Child-Modules

Parent templates MUST reference all their direct child-templates to allow for an end-to-end deployment experience.
For example, the SQL server template must reference its child database module and encapsulate it in a loop to allow for the deployment of multiple databases.

@description('Optional. The databases to create in the server')
param databases databaseType[]?

resource server 'Microsoft.Sql/servers@(...)' = { (...) }

module server_databases 'database/main.bicep' = [for (database, index) in (databases ?? []): {
  name: '${uniqueString(server.id, location)}-Sql-DB-${index}'
  params: {
    serverName: server.name
    (...)
  }
}]



See origin...

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 outputs

Since 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 = { ... } // Correct



See origin...

ID: 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 .bicep file 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...



See origin...

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:

SegmentDescription
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:

  1. Deterministic — the same parent resource always produces the same hash, so repeated deployments overwrite rather than accumulate.
  2. 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:

ApproachDeterministic?Collision-free?Issue
deployment().nameChanges every deployment; hits 800-limit
utcNow() / timestampsChanges every deployment; hits 800-limit
subscription().id + resourceGroup().idSame hash for all resources in the same RG; collisions when deploying multiple instances
<parentResource>.id, locationRecommended — 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
    (...)
  }
}]



See origin...

ID: BCPNFR5 - Category: Composition - Role Assignments Role Definition Mapping Limits

As per BCPFR2, module owners MAY define common RBAC Role Definition names and IDs within a variable to allow consumers to define a RBAC Role Definition by their name rather than their ID.

Module owners SHOULD NOT map every RBAC Role Definition within this variable as it can cause the module to bloat in size and cause consumption issues later when stitched together with other modules due to the 4MB ARM Template size limit.

Therefore module owners SHOULD only map the most applicable and common RBAC Role Definition names for their module and SHOULD NOT exceed 15 RBAC Role Definitions in the variable.

Important

Remember if the RBAC Role Definition name is not included in the variable this does not mean it cannot be declared, used and assigned to an identity via an RBAC Role Assignment as part of a module, as any RBAC Role Definition can be specified via its ID without being in the variable.

Tip

Review the Bicep Contribution Guide’s ‘RBAC Role Definition Name Mapping’ section for a code sample to achieve this requirement.




See origin...

ID: BCPNFR6 - Category: Composition - Role Assignments Role Definition Mapping Compulsory Roles

Module owners MUST include the following roles in the variable for RBAC Role Definition names:

  • Owner - ID: 8e3af657-a8ff-443c-a75c-2fe8c4bcb635
  • Contributor - ID: b24988ac-6180-42a0-ab88-20f7382dd24c
  • Reader - ID: acdd72a7-3385-48ef-bd42-f606fba81ae7
  • User Access Administrator - ID: 18d7d88d-d35e-4fb5-a5c3-7773c20a72d9
  • Role Based Access Control Administrator (Preview) - ID: f58310d9-a9f6-439a-9e8d-f62e7b41a168
Tip

Review the Bicep Contribution Guide’s ‘RBAC Role Definition Name Mapping’ section for a code sample to achieve this requirement.




See origin...

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.1 for new modules, so that they can be released as v0.1.0.
  • 1.0 once the module owner signs off the module is stable enough for it’s first Major release of v1.0.0.
  • 0.x for all feature updates between the first release v0.1.0 and the first Major release of v1.0.0.



Inputs / Outputs

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR14Data TypesSHOULDOwnerContributorBAU
2SNFR22Parameters/Variables for Resource IDsMUSTOwnerContributorBAU
3SNFR26Output - Parameters - DecoratorsMUSTOwnerContributorBAU
4PMNFR5Parameter/Variable NamingSHOULDOwnerContributorBAU
5BCPNFR1Complex data types - GeneralMUSTOwnerContributorBAU
6BCPNFR9Inputs - DecoratorsMUSTOwnerContributorBAU
7BCPNFR18User-defined types - SpecificationMUSTOwnerContributorBAU
8BCPNFR19User-defined types - NamingMUSTOwnerContributorBAU
9BCPNFR20User-defined types - ExportMUSTOwnerContributorBAU
10BCPNFR21User-defined types - DecoratorsMUSTOwnerContributorBAU
11BCPNFR7Parameter Requirement TypesMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

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.




See origin...

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
  }
}



See origin...

ID: SNFR26 - Output-Parameters - Decorators

Output parameters MUST implement:

Output parameters
@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
}



See origin...

ID: PMNFR5 - Category: Inputs - Parameter/Variable Naming

Parameter/variable input names SHOULD contain the resource to which they pertain. E.g., virtualMachineSku/virtualmachine_sku




See origin...

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.




See origin...

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 string



See origin...

ID: 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.




See origin...

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 outputs

Since 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 = { ... } // Correct



See origin...

ID: 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.




See origin...

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')
}



See origin...

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 TypeDefinitionExample Description Decorator
RequiredThe 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...>')
ConditionalThe 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...>')
OptionalThe parameter value is not mandatory. The module provides a default value for the parameter.@description('Optional. <PARAMETER DESCRIPTION HERE...>')
GeneratedThe 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
#IDTitleSeverityPersonaLifecycle
1SNFR1Prescribed TestsMUSTOwnerContributorBAU
2SNFR2E2E TestingMUSTOwnerContributorBAU
3SNFR3AVM Compliance TestsMUSTOwnerContributorInitial
4SNFR4Unit TestsSHOULDOwnerContributorBAU
5SNFR5Upgrade TestsSHOULDOwnerContributorBAU
6SNFR6Static Analysis/Linting TestsMUSTOwnerContributorBAU
7SNFR7Idempotency TestsMUSTOwnerContributorBAU
8SNFR24Testing Child, Extension & Interface ResourcesMUSTOwnerContributorBAU
9BCPNFR10Test Bicep File NamingMUSTOwnerContributorBAU
10BCPNFR11Test ToolingMUSTOwnerContributorBAU
11BCPNFR12Deployment Test NamingMUSTOwnerContributorBAU
12BCPNFR13Test file metadataMUSTOwnerContributorBAU
13BCPNFR16Post-deployment testsMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

ID: SNFR1 - Category: Testing - Prescribed Tests

Modules MUST use the prescribed tooling and testing frameworks defined in the language specific specs.




See origin...

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

To see a directory and file structure for a module, see the language specific contribution guide.

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.
➕ 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.

Details for use in E2E tests

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>).




See origin...

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/avm-core-team-technical-bicep, OR @Azure/avm-core-team-technical-terraform.




See origin...

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.




See origin...

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.




See origin...

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.




See origin...

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.




See origin...

ID: SNFR24 - Category: Testing - Testing Child, Extension & Interface Resources

Module owners MUST test that child and extension resources and those Bicep or Terreform interface resources that are supported by their modules, are validated in E2E tests as per SNFR2 to ensure they deploy and are configured correctly.

These MAY be tested in a separate E2E test and DO NOT have to be tested in each E2E test.




See origin...

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.




See origin...

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.



See origin...

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.




See origin...

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.



See origin...

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 e2e test’s folder or any subfolder (e.g., e2e/max/myTest.tests.ps1 or e2e/max/tests/myTest.tests.ps1)

  • Implement an input parameter TestInputData in the following way:

    param (
        [Parameter(Mandatory = $false)]
        [hashtable] $TestInputData = @{}
    )

    Through this parameter you can make use of every output the main.test.bicep file 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.resourceId defined in the main.test.bicep file, the $TestInputData would 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
            }
        }
    }



Documentation

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR15Automatic Documentation GenerationMUSTOwnerContributorBAU
2SNFR16Examples/E2EMUSTOwnerContributorBAU
3BCPNFR2Module Documentation GenerationMUSTOwnerContributorBAU
4BCPNFR3Usage Example formatsMUSTOwnerContributorBAU
5BCPNFR4Parameter Input ExamplesMAYOwnerContributorBAU
➕ See Specifications for this category
See origin...

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.




See origin...

ID: SNFR16 - Category: Documentation - Examples/E2E

An examples/e2e directory MUST exist to provide named scenarios for module deployment.




See origin...

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



See origin...

ID: BCPNFR3 - Category: Documentation - Usage Example formats

Usage examples for Bicep modules MUST be provided in the following formats:

  • Bicep file (orchestration module style) - .bicep

    module <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.




See origin...

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 customerManagedKeyType

It 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
#IDTitleSeverityPersonaLifecycle
1SNFR17Semantic VersioningMUSTOwnerContributorBAU
2SNFR18Breaking ChangesSHOULDOwnerContributorBAU
3SNFR19Registries TargetedMUSTOwnerContributorBAU
4SNFR21Cross Language CollaborationSHOULDOwnerContributorBAU
5BCPNFR22Bicep Module ChangelogMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

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 Version
  • Y == Minor Version
  • Z == Patch Version

Module versioning before first Major version release 1.0.0

  • Initially modules MUST be released as version 0.1.0 and 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 version 1.0.0.

    Note

    Releasing as version 0.1.0 initially 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.0 is released, given a version number X.Y.Z:

    • X Major version MUST NOT be bumped.
    • Y Minor version MUST be bumped when introducing breaking changes (which would normally bump Major after 1.0.0 release) or feature updates (same as it will be after 1.0.0 release).
    • Z Patch version MUST be bumped when introducing non-breaking, backward compatible bug fixes (same as it will be after 1.0.0 release).



See origin...

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.




See origin...

ID: SNFR19 - Category: Publishing - Registries Targeted

Modules MUST be published to their respective language public registries.

Tip

See the language specific contribution guides for detailed guidance and sample code to use in AVM modules to achieve this requirement.




See origin...

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.




See origin...

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

- None

Each version’s entry:

  • MUST contain two sections: Changes and Breaking Changes. At least one of them must have a meaningful entry and sections must not be left empty. A - None may 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

- None

Each 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 ## Changes and ## Breaking Changes with 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 as tags for each module https://mcr.microsoft.com/v2/bicep/avm/(res|ptn|utl)/<namespace/modulename>/tags/list




Code Style

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1BCPNFR8Code Styling - lower camelCasingSHOULDOwnerContributorBAU
2BCPNFR17Code Styling - Type castingSHOULDOwnerContributorBAU
➕ See Specifications for this category
See origin...

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)




See origin...

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

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

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))
  }
}



Bicep Resource Module Specifications

Contribution / Support

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR8Module Owner(s) GitHubMUSTOwnerInitial
2SNFR20GitHub Teams OnlyMUSTOwnerInitial
3SNFR9AVM & PG Teams GitHub Repo PermissionsMUSTOwnerInitial
4SNFR10MIT LicensingMUSTOwnerInitial
5SNFR11Issues Response TimesMUSTOwnerContributorBAU
6SNFR12Versions SupportedMUSTOwnerBAU
7SNFR23GitHub Repo LabelsMUSTOwnerBAU
8PMNFR4Missing Resource Module(s)MUSTOwnerContributorBAU
9BCPNFR15AVM Module Issue template fileMUSTOwnerBAU
➕ See Specifications for this category
See origin...

ID: SNFR8 - Category: Contribution/Support - Module Owner(s) GitHub

A module MUST have an owner that is defined and managed by a GitHub Team in the Azure GitHub organization.

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

The names for the GitHub teams for each approved module are already defined in the respective Module Indexes. These teams MUST be created (and used) for each module.




See origin...

ID: SNFR20 - Category: Contribution/Support - GitHub Teams Only

All GitHub repositories that AVM module are published from and hosted within MUST only assign GitHub repository permissions to GitHub teams only.

Each module MUST have a GitHub team assigned for module owners. This team MUST be created in the Azure organization in GitHub.

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

Important

As part of the module proposal process, the name of the GitHub team for each approved module is already defined in the respective Module Indexes (or CSV file). This team MUST be created (and used) for each module.

Module owners don’t need to construct the name of the GitHub team for their module themselves, instead they need use the name prescribed in the related CSV file, at the time of approval.

For a direct link, see the list of related index pages:

The @Azure prefix in the last column of the tables linked above represents the “Azure” GitHub organization all AVM-related repositories exist in. DO NOT include this segment in the team’s name!

Naming Convention

The naming convention for the GitHub teams MUST follow the below pattern:

  • <hyphenated module name>-module-owners-bicep - to grant permissions for module owners on Bicep modules

Segments:

  • <hyphenated module name> == the AVM Module’s name, with each segment separated by dashes, i.e., avm-res-<resource provider>-<ARM resource type>
    • See RMNFR1 for AVM Resource Module Naming
    • See PMNFR1 for AVM Pattern Module Naming
  • module-owners == the role the GitHub Team is assigned to
  • <bicep == the language the module is written in

Examples:

  • avm-res-compute-virtualmachine-module-owners-bicep
Note

The naming convention for Bicep modules is slightly different than the naming convention for their respective GitHub teams.

Add Team Members

All officially documented module owner(s) MUST be added to the -module-owners- team. The -module-owners- team MUST NOT have any other members.

Unless explicitly requested and agreed, members of the AVM core team or any PG teams MUST NOT be added to the -module-owners- teams as permissions for them are granted through the teams described in SNFR9.

Grant permissions through team memberships

Note

In case of Bicep modules, permissions to the BRM repository (the repo of the Bicep Registry) are granted via assigning the -module-owners- teams to parent teams that already have the required level access configured. While it is the module owner’s responsibility to initiate the addition of their team to the respective parent, only the AVM core team can approve this parent-child relationship.

Module owners MUST create their -module-owners- team and as part of the provisioning process, they MUST request the addition of this team to its respective parent team (see the table below for details).

GitHub Team NameDescriptionPermissionsPermissions granted throughWhere to work?
<hyphenated module name>-module-owners-bicepAVM Bicep Module Owners - <module name>WriteAssignment to the avm-technical-reviewers-bicep parent team.Need to work in a fork.

Example - GitHub team required for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

  • avm-res-network-virtualnetwork-module-owners-bicep –> assign to the avm-technical-reviewers-bicep parent team.
Tip

Direct link to create a new GitHub team and assign it to its parent: Create new team

Fill in the values as follows:

  • Team name: Following the naming convention described above, use the value defined in the module indexes.
  • Description: Follow the guidance above (see the Description column in the table above).
  • Parent team: Follow the guidance above (see the Permissions granted through column in the table above).
  • Team visibility: Visible
  • Team notifications: Enabled

CODEOWNERS file

As part of the “initial Pull Request” (that publishes the first version of the module), module owners MUST add an entry to the CODEOWNERS file in the BRM repository (here).

Note

Through this approach, the AVM core team will grant review permission to module owners as part of the standard PR review process.

Every CODEOWNERS entry (line) MUST include the following segments separated by a single whitespace character:

  • Path of the module, relative to the repo’s root, e.g.: /avm/res/network/virtual-network/
  • The -module-owners-team, with the @Azure/ prefix, e.g., @Azure/avm-res-network-virtualnetwork-module-owners-bicep
  • The GitHub team of the AVM Bicep reviewers, with the @Azure/ prefix, i.e., @Azure/avm-module-reviewers-bicep

Example - CODEOWNERS entry for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

  • /avm/res/network/virtual-network/ @Azure/avm-res-network-virtualnetwork-module-owners-bicep @Azure/avm-module-reviewers-bicep

Terraform

Note

Access management for Terraform repositories now uses a single team, membership of which is managed using an internal entitlement management tool (Core Identity).

All module owners MUST request access to the avm-module-owners-terraform GitHub team via the Azure Verified Module Owners Terraform entitlement in Core Identity (Microsoft internal tool).




See origin...

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

Note

These required GitHub teams are already associated to the BRM repository and have the required permissions.

Terraform

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.




See origin...

ID: SNFR10 - Category: Contribution/Support - MIT Licensing

A module MUST be published with the MIT License in the Azure GitHub organization.




See origin...

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.




See origin...

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.




See origin...

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:

➕ AVM Standard GitHub Labels

These labels are available in a CSV file from here

NameDescriptionHEX
AZD 🧑‍💻These modules are requested/used by the AZD team.
E0BFFA
Needs: Attention 👋Reply has been added to issue, maintainer to review
E99695
Needs: Immediate Attention ‼️Immediate attention of module owner / AVM team is needed
FF0000
Needs: Author Feedback 👂Awaiting feedback from the issue/PR author
F18A07
Needs: External Changes ⚒️When an issue/PR requires changes that are outside of the control of the module. e.g. to an RP.
DE389D
Needs: More Evidence ⚖We are looking for more evidence to make a decision on this
F64872
Needs: Triage 🔍Maintainers need to triage still
FBCA04
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.
FF0019
Needs: Module Contributor 📣This module needs secondary owner(s) or contributor(s) to develop or maintain it
C95474
Needs: Core Team 🧞‍♂️This item needs the AVM Core Team to review it
DB4503
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
800080
Status: Do Not Merge ⛔Do not merge PRs with this label attached as they are not ready or aligned to future direction etc.
8B4513
Status: External Contribution 🌍This is being worked on by someone outside of the AVM module owners/contributors or AVM core team
D8FA2C
Status: Fixed ✅Auto label applied when issue fixed by merged PR
90EE90
Status: Help Wanted 🆘Extra attention is needed
FF4500
Status: In Triage 🔍Picked up for triaging by an AVM core team member
D4AF37
Status: In PR 👉This is when an issue is due to be fixed in an open PR
EDEDED
Status: Invalid ❌This doesn't seem right
E4E669
Status: Long Term ⏳We will do it, but will take a longer amount of time due to complexity/priorities
B60205
Status: No Recent Activity 💤When an issue/PR has not been modified for X amount of days
808080
Status: Won't Fix 💔This will not be worked on
FFFFFF
Status: Owners Identified 🤘This module has its owners identified
FBEF2A
Status: Module Available 🟢The module is published
C8E6C9
Status: Module Deprecated 🔴This is a request to deprecate a module
000000
Status: Module Orphaned 🟡The module has no owner and is therefore orphaned at this time
F4A460
Status: Ready For Repository Creation 📝This module is approved and the owner is ready for the repository to be created (Terraform)
136A41
Status: Repository Created 📄This module has had it's repository created and configured ready for owner contribution (Terraform)
27AB03
Status: Response Overdue 🚩When an issue/PR has not been responded to for X amount of days
850000
Status: Looking For Assistance 🦆This item is looking for anyone to help develop the code and submit a PR for resolution
03FCC2
Type: Bug 🐛Something isn't working
D73A4A
Type: CI 🚀This issue is related to the AVM CI
74CFB0
Type: Documentation 📄Improvements or additions to documentation
0075CA
Type: Duplicate 🤲This issue or pull request already exists
CFD3D7
Type: Feature Request ➕New feature or request
A2EEEF
Type: Hygiene 🧹things related to testing, issue triage etc.
17016A
Type: New Module Proposal 💡A new module for AVM is being proposed
ADD8E6
Type: Question/Feedback 🙋‍♀️Further information is requested or just some feedback
CB6BA2
Type: Security Bug 🔒This is a security bug
FFFF00
Type: AVM 🅰️ ✌️ ⓜ️This is an AVM related issue
F0FFFF
Language: Terraform 🌐This is related to the Terraform IaC language
7740B6
Language: Bicep 💪This is related to the Bicep IaC language
1D73B3
Class: Resource Module 📦This is a resource module
D3D3D3
Class: Pattern Module 📦This is a pattern module
A9A9A9
Class: Utility Module 📦This is a utility module
CAD1DE
Class: Child Module 📦This is a child module
5E5186

To help apply these to a module GitHub repository you can use the below PowerShell script:

➕ Set-AvmGitHubLabels.ps1

For most scenario this is the command you’ll need to call the below PowerShell script with, replacing the value for RepositoryName:

  Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -CreateCsvLabelExports $false -NoUserPrompts $true
```shell
# Linux / MacOs
# For Windows replace $PWD with your the local path or your repository
#
docker run -it -v $PWD:/repo -w /repo mcr.microsoft.com/powershell pwsh -Command '
    #Invoke-WebRequest -Uri "https://azure.github.io/Azure-Verified-Modules/scripts/Set-AvmGitHubLabels.ps1" -OutFile "Set-AvmGitHubLabels.ps1"
    $gh_version = "2.44.1"
    Invoke-WebRequest -Uri "https://github.com/cli/cli/releases/download/v2.44.1/gh_2.44.1_linux_amd64.tar.gz" -OutFile "gh_$($gh_version)_linux_amd64.tar.gz"
    apt-get update && apt-get install -y git
    tar -xzf "gh_$($gh_version)_linux_amd64.tar.gz"
    ls -lsa
    mv "gh_$($gh_version)_linux_amd64/bin/gh" /usr/local/bin/
    rm "gh_$($gh_version)_linux_amd64.tar.gz" && rm -rf "gh_$($gh_version)_linux_amd64"
    gh --version
    ls -lsa
    gh auth login
    $OrgProject = "Azure/terraform-azurerm-avm-res-kusto-cluster"
    gh auth status
    ./Set-AvmGitHubLabels.ps1 -RepositoryName $OrgProject -CreateCsvLabelExports $false -NoUserPrompts $true

  '
```

By default this script will only update and append labels on the repository specified. However, this can be changed by setting the parameter -UpdateAndAddLabelsOnly to $false, which will remove all the labels from the repository first and then apply the AVM labels from the CSV only.

Make sure you elevate your privilege to admin level or the labels will not be applied to your repository. Go to repos.opensource.microsoft.com/orgs/Azure/repos/ to request admin access before running the script.

Full Script:

These Set-AvmGitHubLabels.ps1 can be downloaded from here.

  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Coloured output required in this script")]
  
  <#
  .SYNOPSIS
    This script can be used to create the Azure Verified Modules (AVM) standard GitHub labels to a GitHub repository.
  
  .DESCRIPTION
    This script can be used to create the Azure Verified Modules (AVM) standard GitHub labels to a GitHub repository.
  
    By default, the script will remove all pre-existing labels and apply the AVM labels. However, this can be changed by using the -RemoveExistingLabels parameter and setting it to $false. The tool will also output the labels that exist in the repository before and after the script has run to a CSV file in the current directory, or a directory specified by the -OutputDirectory parameter.
  
    The AVM labels to be created are documented here: TBC
  
  .NOTES
    Please ensure you have specified the GitHub repositry correctly. The script will prompt you to confirm the repository name before proceeding.
  
  .COMPONENT
    You must have the GitHub CLI installed and be authenticated to a GitHub account with access to the repository you are applying the labels to before running this script.
  
  .LINK
    TBC
  
  .Parameter RepositoryName
    The name of the GitHub repository to apply the labels to.
  
  .Parameter RemoveExistingLabels
    If set to $true, the default value, the script will remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels. If set to $false, the script will not remove any pre-existing labels.
  
  .Parameter UpdateAndAddLabelsOnly
    If set to $true, the default value, the script will only update and add labels to the repository specified in -RepositoryName. If set to $false, the script will remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels.
  
  .Parameter OutputDirectory
    The directory to output the pre-existing and post-existing labels to in a CSV file. The default value is the current directory.
  
  .Parameter CreateCsvLabelExports
    If set to $true, the default value, the script will output the pre-existing and post-existing labels to a CSV file in the current directory, or a directory specified by the -OutputDirectory parameter. If set to $false, the script will not output the pre-existing and post-existing labels to a CSV file.
  
  .Parameter GitHubCliLimit
    The maximum number of labels to return from the GitHub CLI. The default value is 999.
  
  .Parameter LabelsToApplyCsvUri
    The URI to the CSV file containing the labels to apply to the GitHub repository. The default value is https://raw.githubusercontent.com/jtracey93/label-source/main/avm-github-labels.csv.
  
  .Parameter NoUserPrompts
    If set to $true, the default value, the script will not prompt the user to confirm they want to remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels. If set to $false, the script will prompt the user to confirm they want to remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels.
  
    This is useful for running the script in automation workflows
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and remove all pre-existing labels.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo"
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -RemoveExistingLabels $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and output the pre-existing and post-existing labels to the directory C:\GitHubLabels.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels"
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and output the pre-existing and post-existing labels to the directory C:\GitHubLabels and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels" -RemoveExistingLabels $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not create the pre-existing and post-existing labels CSV files and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -RemoveExistingLabels $false -CreateCsvLabelExports $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not create the pre-existing and post-existing labels CSV files and do not remove any pre-existing labels, just overwrite any labels that have the same name. Finally, use a custom CSV file hosted on the internet to create the labels from.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels" -RemoveExistingLabels $false -CreateCsvLabelExports $false -LabelsToApplyCsvUri "https://example.com/csv/avm-github-labels.csv"
  
  #>
  
  #Requires -PSEdition Core
  
  [CmdletBinding()]
  param (
    [Parameter(Mandatory = $true)]
    [string]$RepositoryName,
  
    [Parameter(Mandatory = $false)]
    [bool]$RemoveExistingLabels = $true,
  
    [Parameter(Mandatory = $false)]
    [bool]$UpdateAndAddLabelsOnly = $true,
  
    [Parameter(Mandatory = $false)]
    [bool]$CreateCsvLabelExports = $true,
  
    [Parameter(Mandatory = $false)]
    [string]$OutputDirectory = (Get-Location),
  
    [Parameter(Mandatory = $false)]
    [int]$GitHubCliLimit = 999,
  
    [Parameter(Mandatory = $false)]
    [string]$LabelsToApplyCsvUri = "https://azure.github.io/Azure-Verified-Modules/governance/avm-standard-github-labels.csv",
  
    [Parameter(Mandatory = $false)]
    [bool]$NoUserPrompts = $false
  )
  
  # Check if the GitHub CLI is installed
  $GitHubCliInstalled = Get-Command gh -ErrorAction SilentlyContinue
  if ($null -eq $GitHubCliInstalled) {
    throw "The GitHub CLI is not installed. Please install the GitHub CLI and try again."
  }
  Write-Host "The GitHub CLI is installed..." -ForegroundColor Green
  
  # Check if GitHub CLI is authenticated
  $GitHubCliAuthenticated = gh auth status
  if ($LASTEXITCODE -ne 0) {
    Write-Host $GitHubCliAuthenticated -ForegroundColor Red
    throw "Not authenticated to GitHub. Please authenticate to GitHub using the GitHub CLI, `gh auth login`, and try again."
  }
  Write-Host "Authenticated to GitHub..." -ForegroundColor Green
  
  # Check if GitHub repository name is valid
  $GitHubRepositoryNameValid = $RepositoryName -match "^[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$"
  if ($false -eq $GitHubRepositoryNameValid) {
    throw "The GitHub repository name $RepositoryName is not valid. Please check the repository name and try again. The format must be <OrgName>/<RepoName>"
  }
  
  # List GitHub repository provided and check it exists
  $GitHubRepository = gh repo view $RepositoryName
  if ($LASTEXITCODE -ne 0) {
    Write-Host $GitHubRepository -ForegroundColor Red
    throw "The GitHub repository $RepositoryName does not exist. Please check the repository name and try again."
  }
  Write-Host "The GitHub repository $RepositoryName exists..." -ForegroundColor Green
  
  # PRE - Get the current GitHub repository labels and export to a CSV file in the current directory or where -OutputDirectory specifies if set to a valid directory path and the directory exists or can be created if it does not exist already
  if ($RemoveExistingLabels -or $UpdateAndAddLabelsOnly) {
    Write-Host "Getting the current GitHub repository (pre) labels for $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
  
    if ($null -ne $GitHubRepositoryLabels -and $CreateCsvLabelExports -eq $true) {
      $csvFileNamePathPre = "$OutputDirectory\$($RepositoryName.Replace('/', '_'))-Labels-Pre-$(Get-Date -Format FileDateTime).csv"
      Write-Host "Exporting the current GitHub repository (pre) labels for $RepositoryName to $csvFileNamePathPre" -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | Export-Csv -Path $csvFileNamePathPre -NoTypeInformation
    }
  }
  
  # Remove all pre-existing labels if -RemoveExistingLabels is set to $true and user confirms they want to remove all pre-existing labels
  if ($null -ne $GitHubRepositoryLabels) {
    $GitHubRepositoryLabelsJson = $GitHubRepositoryLabels | ConvertFrom-Json
    if ($RemoveExistingLabels -eq $true -and $NoUserPrompts -eq $false -and $UpdateAndAddLabelsOnly -eq $false) {
      $RemoveExistingLabelsConfirmation = Read-Host "Are you sure you want to remove all $($GitHubRepositoryLabelsJson.Count) pre-existing labels from $($RepositoryName)? (Y/N)"
      if ($RemoveExistingLabelsConfirmation -eq "Y") {
        Write-Host "Removing all pre-existing labels from $RepositoryName..." -ForegroundColor Yellow
        $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
          Write-Host "Removing label $($_.name) from $RepositoryName..." -ForegroundColor DarkRed
          gh label delete -R $RepositoryName $_.name --yes
        }
      }
    }
    if ($RemoveExistingLabels -eq $true -and $NoUserPrompts -eq $true -and $UpdateAndAddLabelsOnly -eq $false) {
      Write-Host "Removing all pre-existing labels from $RepositoryName..." -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
        Write-Host "Removing label $($_.name) from $RepositoryName..." -ForegroundColor DarkRed
        gh label delete -R $RepositoryName $_.name --yes
      }
    }
  }
  if ($null -eq $GitHubRepositoryLabels) {
    Write-Host "No pre-existing labels to remove or not selected to be removed from $RepositoryName..." -ForegroundColor Magenta
  }
  
  # Check LabelsToApplyCsvUri is valid and contains a CSV content
  Write-Host "Checking $LabelsToApplyCsvUri is valid..." -ForegroundColor Yellow
  $LabelsToApplyCsvUriValid = $LabelsToApplyCsvUri -match "^https?://"
  if ($false -eq $LabelsToApplyCsvUriValid) {
    throw "The LabelsToApplyCsvUri $LabelsToApplyCsvUri is not valid. Please check the URI and try again. The format must be a valid URI."
  }
  Write-Host "The LabelsToApplyCsvUri $LabelsToApplyCsvUri is valid..." -ForegroundColor Green
  
  # Create AVM lables from the AVM labels CSV file stored on the web using the convertfrom-csv cmdlet
  $avmLabelsCsv = Invoke-WebRequest -Uri $LabelsToApplyCsvUri | ConvertFrom-Csv
  
  # Check if the AVM labels CSV file contains the following columns: Name, Description, HEX
  $avmLabelsCsvColumns = $avmLabelsCsv | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name
  $avmLabelsCsvColumnsValid = $avmLabelsCsvColumns -contains "Name" -and $avmLabelsCsvColumns -contains "Description" -and $avmLabelsCsvColumns -contains "HEX"
  if ($false -eq $avmLabelsCsvColumnsValid) {
    throw "The labels CSV file does not contain the required columns: Name, Description, HEX. Please check the CSV file and try again. It contains the following columns: $avmLabelsCsvColumns"
  }
  Write-Host "The labels CSV file contains the required columns: Name, Description, HEX" -ForegroundColor Green
  
  # Create the AVM labels in the GitHub repository
  Write-Host "Creating/Updating the $($avmLabelsCsv.Count) AVM labels in $RepositoryName..." -ForegroundColor Yellow
  $avmLabelsCsv | ForEach-Object {
    if ($GitHubRepositoryLabelsJson.name -contains $_.name) {
      Write-Host "The label $($_.name) already exists in $RepositoryName. Updating the label to ensure description and color are consitent..." -ForegroundColor Magenta
      gh label create -R $RepositoryName "$($_.name)" -c $_.HEX -d $($_.Description) --force
    }
    else {
      Write-Host "The label $($_.name) does not exist in $RepositoryName. Creating label $($_.name) in $RepositoryName..." -ForegroundColor Cyan
      gh label create -R $RepositoryName "$($_.Name)" -c $_.HEX -d $($_.Description) --force
    }
  }
  
  # POST - Get the current GitHub repository labels and export to a CSV file in the current directory or where -OutputDirectory specifies if set to a valid directory path and the directory exists or can be created if it does not exist already
  if ($CreateCsvLabelExports -eq $true) {
    Write-Host "Getting the current GitHub repository (post) labels for $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
  
    if ($null -ne $GitHubRepositoryLabels) {
      $csvFileNamePathPre = "$OutputDirectory\$($RepositoryName.Replace('/', '_'))-Labels-Post-$(Get-Date -Format FileDateTime).csv"
      Write-Host "Exporting the current GitHub repository (post) labels for $RepositoryName to $csvFileNamePathPre" -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | Export-Csv -Path $csvFileNamePathPre -NoTypeInformation
    }
  }
  
  # If -RemoveExistingLabels is set to $true and user confirms they want to remove all pre-existing labels check that only the avm labels exist in the repository
  if ($RemoveExistingLabels -eq $true -and ($RemoveExistingLabelsConfirmation -eq "Y" -or $NoUserPrompts -eq $true) -and $UpdateAndAddLabelsOnly -eq $false) {
    Write-Host "Checking that only the AVM labels exist in $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
    $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
      if ($avmLabelsCsv.Name -notcontains $_.name) {
        throw "The label $($_.name) exists in $RepositoryName but is not in the CSV file."
      }
    }
    Write-Host "Only the CSV labels exist in $RepositoryName..." -ForegroundColor Green
  }
  
  Write-Host "The CSV labels have been created/updated in $RepositoryName..." -ForegroundColor Green
  



See origin...

ID: PMNFR4 - Category: Hygiene - Missing Resource Module(s)

An item MUST be logged onto as an issue on the AVM Central Repo (Azure/Azure-Verified-Modules) if a Resource Module does not exist for resources deployed by the pattern module.

Exception

If the Resource Module adds no value, see Resource Module functional requirement ID: RMFR2.




See origin...

ID: BCPNFR15 - Category: Contribution/Support - AVM Module Issue template file

Module owners MUST add an entry to the AVM Module Issue template file in the BRM repository (here). When the module is deprecated, this entry MUST be removed from the file.

Note

Through this approach, the AVM core team will allow raising a bug or feature request for a module, only after the module gets merged to the BRM repository.

The module name entry MUST be added to the dropdown list with id module-name-dropdown as an option, in alphabetical order.

Important

Module owners MUST ensure that the module name is added in alphabetical order, to simplify selecting the right module name when raising an AVM module issue.

Example - AVM Module Issue template module name entry for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

- type: dropdown
  id: module-name-dropdown
  attributes:
    label: Module Name
    description: Which existing AVM module is this issue related to?
    options:
      ...
      - "avm/res/network/virtual-network"
      ...



Telemetry

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SFR3Deployment/Usage TelemetryMUSTOwnerInitial
2SFR4Telemetry Enablement FlexibilityMUSTOwnerInitial
3BCPFR4Telemetry EnablementMUSTOwnerContributorBAU
4BCPFR7Cross-Referencing ModulesMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

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 more details on this requirement, here.)

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.ps1 utility
  • Terraform: Executing the make docs command with the note and header ## Data Collection being placed in the module’s _footer.md beforehand
### 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

We will maintain a set of CSV files in the AVM Central Repo (Azure/Azure-Verified-Modules) with the required TelemetryId prefixes to enable checks to utilize this list to ensure the correct IDs are used. To see the formatted content of these CSV files with additional information, please visit the AVM Module Indexes page.

The value you need to use for your module is defined in the related module index. You can look it up on the index pages for Resource Modules, Pattern Modules and Utility Modules.

The ARM deployment name used for the telemetry MUST follow the pattern and MUST be no longer than 64 characters in length: 46d3xbcp.<res/ptn>.<(short) module name>.<version>.<uniqueness>

  • <res/ptn> == AVM Resource or Pattern Module
  • <(short) module name> == The AVM Module’s, possibly shortened, name including the resource provider and the resource type, without;
    • The prefixes: avm-res-
    • The prefixes: avm-ptn-
  • <version> == The AVM Module’s MAJOR.MINOR version (only) with . (periods) replaced with - (hyphens), to allow simpler splitting of the ARM deployment name
  • <uniqueness> == This section of the ARM deployment name is to be used to ensure uniqueness of the deployment name.
    • This is to cater for the following scenarios:
      • The module is deployed multiple times to the same:
        • Location/Region
        • Scope (Tenant, Management Group,Subscription, Resource Group)
Note

Due to the 64-character length limit of Azure deployment names, the <(short) module name> segment has a length limit of 36 characters, so if the module name is longer than that, it MUST be truncated to 36 characters. If any of the semantic version’s segments are longer than 1 character, it further restricts the number of characters that can be used for naming the module.

An example deployment name for the AVM Virtual Machine Resource Module would be: 46d3xbcp.res.compute-virtualmachine.1-2-3.eum3

An example deployment name for a shortened module name would be: 46d3xbcp.res.desktopvirtualization-appgroup.1-2-3.eum3

Tip

Terraform: Terraform uses a telemetry provider, the configuration of which is the same for every module and is included in the template repo.

General: See the language specific contribution guides for detailed guidance and sample code to use in AVM modules to achieve this requirement.

Terraform

To enable telemetry data collection for Terraform modules, the modtm telemetry provider MUST be used. This lightweight telemetry provider sends telemetry data to Azure Application Insights via a HTTP POST front end service.

The modtm telemetry provider is included in all Terraform modules and is enabled by default through the main.telemetry.tf file being automatically distributed from the template repo.

The modtm provider MUST be listed under the required_providers section in the module’s terraform.tf file using the following entry. This is also validated by the linter.

terraform {
  required_providers {
    # .. other required providers as needed
    modtm = {
      source = "Azure/modtm"
      version = "~> 0.3"
    }
  }
}



See origin...

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

Currently, no further requirements apply.




See origin...

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).

@description('Optional. Location for all resources.')
param location string = resourceGroup().location

@description('Optional. Enable/Disable usage telemetry for module.')
param enableTelemetry bool = true

#disable-next-line no-deployments-resources
resource avmTelemetry 'Microsoft.Resources/deployments@2025-04-01' = if (enableTelemetry) {
  name: take('46d3xbcp.res.compute-virtualmachine.${replace('-..--..-', '.', '-')}.${substring(uniqueString(deployment().name, location), 0, 4)}', 64)
  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'
        }
      }
    }
  }
}



See origin...

ID: BCPFR7 - Cross-Referencing published Modules

Resource modules, that reference other modules (child, utility, or other resource modules), MUST disable the telemetry on the referenced modules.

Note

This only applies to resource modules that reference other modules, such as:

  • other resource modules
  • utility modules
  • child-modules qualifying for publishing, i.e. having a version.json file in their directory and exposing the enableTelemetry input parameter

For pattern modules, SFR4 still applies.

A variable named enableReferencedModulesTelemetry is created in the main.bicep file of the module, that cross-references other published modules, and set to false. This variable is used to set the enableTelemetry parameter of cross-referenced modules.

var enableReferencedModulesTelemetry = false

// local referencing
module virtualNetwork_subnets 'subnet/main.bicep' = [
  for (subnet, index) in (subnets ?? []): {
    name: '${uniqueString(virtualNetwork.id, location)}-subnet-${index}'
    params: {
      (...)
      enableTelemetry: enableReferencedModulesTelemetry
    }
  }
]

// published module reference
module virtualNetwork_subnet 'br/public:avm/res/network/virtual-network/subnet:0.1.0' = {
  name: '${uniqueString(virtualNetwork.id, location)}-subnet-${index}'
    params: {
      (...)
      enableTelemetry: enableReferencedModulesTelemetry
    }
}



Naming / Composition

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SFR1Preview ServicesMUSTOwnerBAU
2SFR2WAF AlignedSHOULDOwnerBAU
3SFR5Availability ZonesMUSTOwnerInitial
4SFR6Data RedundancyMUSTOwnerInitial
5SNFR25Resource NamingMUSTOwnerInitial
6RMFR1Single Resource OnlyMUSTOwnerContributorBAU
7RMFR2No Resource Wrapper ModulesMUSTOwnerInitial
8RMFR3Resource GroupsMUSTOwnerContributorBAU
9RMFR4AVM Consistent Feature & Extension Resources Value AddMUSTOwnerContributorBAU
10RMFR5AVM Consistent Feature & Extension Resources Value Add Interfaces/SchemasMUSTOwnerContributorBAU
11RMFR8Dependency on child and other resourcesMUSTOwnerContributorBAU
12RMFR9End-of-life resource versionsSHOULDOwnerContributorBAU
13RMNFR1Module NamingMUSTOwnerInitial
14RMNFR3RP CollaborationSHOULDOwnerBAU
15BCPFR1Cross-Referencing ModulesMAYOwnerContributorBAU
16BCPFR2Role Assignments Role Definition MappingMUSTOwnerContributorBAU
17BCPFR6Cross-Referencing Child-ModulesMUSTOwnerContributorBAU
18BCPNFR19User-defined types - NamingMUSTOwnerContributorBAU
19BCPNFR23Module compositionMUSTOwnerContributorBAU
20BCPNFR24Deterministic Deployment NamesMUSTOwnerContributorBAU
21BCPNFR5Role Assignments Role Definition Mapping LimitsSHOULDOwnerContributorBAU
22BCPNFR6Role Assignments Role Definition Mapping Compulsory RolesMUSTOwnerContributorBAU
23BCPNFR14VersioningMUSTOwnerContributorBAU
24BCPRMNFR3Child resources structureMUSTOwnerContributorBAU
25BCPRMNFR4Multi-scope modulesMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

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., is 2021-05-01-preview
    • Otherwise the latest “non-preview” version of the API SHOULD be used

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”



See origin...

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:

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.




See origin...

ID: SFR5 - Category: Composition - Availability Zones

Modules that deploy zone-redundant resources MUST enable the spanning across as many zones as possible by default, typically all 3.

Modules that deploy zonal resources MUST provide the ability to specify a zone for the resources to be deployed/pinned to. However, they MUST NOT default to a particular zone by default, e.g. 1 in an effort to make the consumer aware of the zone they are selecting to suit their architecture requirements.

For both scenarios the modules MUST expose these configuration options via configurable parameters/variables.

Note

For information on the differences between zonal and zone-redundant services, see Availability zone service and regional support




See origin...

ID: SFR6 - Category: Composition - Data Redundancy

Modules that deploy resources or patterns that support data redundancy SHOULD enable this to the highest possible value by default, e.g. RA-GZRS. When a resource or pattern doesn’t provide the ability to specify data redundancy as a simple property, e.g. GRS etc., then the modules MUST provide the ability to enable data redundancy for the resources or pattern via parameters/variables.

For example, a Storage Account module can simply set the sku.name property to Standard_RAGZRS. Whereas a SQL DB or Cosmos DB module will need to expose more properties, via parameters/variables, to allow the specification of the regions to replicate data to as per the consumers requirements.

Note

For information on the data redundancy options in Azure, see Cross-region replication in Azure




See origin...

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.




See origin...

ID: RMFR1 - Category: Composition - Single Resource Only

A resource module MUST only deploy a single instance of the primary resource, e.g., one virtual machine per instance.

Multiple instances of the module MUST be used to scale out.




See origin...

ID: RMFR2 - Category: Composition - No Resource Wrapper Modules

A resource module MUST add value by including additional features on top of the primary resource.




See origin...

ID: RMFR3 - Category: Composition - Resource Groups

A resource module MUST NOT create a Resource Group for resources that require them.

In the case that a Resource Group is required, a module MUST have an input (scope or variable):

  • In Bicep the targetScope MUST be set to resourceGroup or not specified (which means default to resourceGroup scope).
  • For Terraform, the resource group is supplied via the parent_id variable defined by TFRMFR1. Terraform resource modules MUST NOT expose a resource_group_name (or resource_group_resource_id) variable.

Scopes will be covered further in the respective language specific specifications.




See origin...

ID: RMFR4 - Category: Composition - AVM Consistent Feature & Extension Resources Value Add

Resource modules support the following optional features/extension resources, as specified, if supported by the primary resource. The top-level variable/parameter names MUST be:

Optional Features/Extension ResourcesBicep Parameter NameTerraform Variable NameMUST/SHOULD
Diagnostic SettingsdiagnosticSettingsdiagnostic_settingsMUST
Role AssignmentsroleAssignmentsrole_assignmentsMUST
Resource LockslocklockMUST
TagstagstagsMUST
Managed Identities (System / User Assigned)managedIdentitiesmanaged_identitiesMUST
Private EndpointsprivateEndpointsprivate_endpointsMUST
Customer Managed KeyscustomerManagedKeycustomer_managed_keyMUST
Azure Monitor AlertsalertsalertsSHOULD

Resource modules MUST NOT deploy required/dependent resources for the optional features/extension resources specified above. For example, for Diagnostic Settings the resource module MUST NOT deploy the Log Analytics Workspace, this is expected to be already in existence from the perspective of the resource module deployed via another method/module etc.

Note

Please note that the implementation of Customer Managed Keys from an ARM API perspective is different across various RPs that implement Customer Managed Keys in their service. For that reason you may see differences between modules on how Customer Managed Keys are handled and implemented, but functionality will be as expected.

Module owners MAY choose to utilize cross repo dependencies for these “add-on” resources, or MAY chose to implement the code directly in their own repo/module. So long as the implementation and outputs are as per the specifications requirements, then this is acceptable.

Tip

Make sure to checkout the language specific specifications for more info on this:




See origin...

ID: RMFR5 - Category: Composition - AVM Consistent Feature & Extension Resources Value Add Interfaces/Schemas

Resource modules MUST implement a common interface, e.g. the input’s data structures and properties within them (objects/arrays/dictionaries/maps), for the optional features/extension resources:

See:




See origin...

ID: RMFR8 - Category: Composition - Dependency on child and other resources

A resource module MAY contain references to other resource modules, however MUST NOT contain references to non-AVM modules nor AVM pattern modules.

See BCPFR1 and TFFR1 for more information on this.




See origin...

ID: RMFR9 - Category: Composition - End-of-life resource versions

When a given version of an Azure resource used in a resource module reaches its end-of-life (EOL) and is no longer supported by Microsoft, the module owner SHOULD ensure that:

  1. The module is aligned with these changes and only includes supported versions of the resource. This is typically achieved through the allowed values in the parameter that specifies the resource SKU or type.
  2. The following notice is shown under the Notes section of the module’s readme.md. (If any related public announcement is available, it can also be linked to from the Notes section.):

    “Certain versions of this Azure resource reached their end of life. The latest version of this module only includes supported versions of the resource. All unsupported versions have been removed from the related parameters.”

  3. AND the related parameter’s description:

    “Certain versions of this Azure resource reached their end of life. The latest version of this module only includes supported versions of the resource. All unsupported versions have been removed from this parameter.”




See origin...

ID: RMNFR1 - Category: Naming - Module Naming

Resource modules MUST follow the below naming conventions (all lower case).

Important

As part of the module proposal process, the module’s approved name is captured both in the module proposal issue AND the related module index page (backed by the corresponding CSV file).

Therefore, module owners don’t need to construct the module’s name themselves, instead they need use the name prescribed in the module proposal issue or in the related CSV file, at the time of approval.

Note

We will maintain a set of CSV files in the AVM Central Repo (Azure/Azure-Verified-Modules) with the correct singular names for all resource types to enable checks to utilize this list to ensure repos are named correctly. To see the formatted content of these CSV files with additional information, please visit the AVM Module Indexes page.

This will be updated quarterly, or ad-hoc as new RPs/ Resources are created and highlighted via a check failure.

Bicep Resource Module Naming

  • Naming convention (module name for registry): avm/res/<hyphenated resource provider name>/<hyphenated ARM resource type>
  • Example: avm/res/compute/virtual-machine or avm/res/managed-identity/user-assigned-identity
  • Segments:
    • res defines this is a resource module
    • <hyphenated resource provider name> is the resource provider’s name after the Microsoft part, with each word starting with a capital letter separated by dashes, e.g., Microsoft.Compute = compute, Microsoft.ManagedIdentity = managed-identity.
    • <hyphenated ARM resource type> is the singular version of the word after the resource provider, with each word starting with a capital letter separated by dashes, e.g., Microsoft.Compute/virtualMachines = virtual-machine, BUT Microsoft.Network/trafficmanagerprofiles = trafficmanagerprofile - since trafficmanagerprofiles is all lower case as per the ARM API definition.

Bicep Child Module Naming

  • Naming convention (module name for registry):avm/res/<hyphenated resource provider name>/<hyphenated ARM resource type>/ <hyphenated child resource type/<hyphenated grandchild resource type>/<etc.>

  • Example: avm/res/network/virtual-network/subnet or avm/res/storage/storage-account/blob-service/container

  • Segments:

    • res defines this is a resource module
    • <hyphenated resource provider name> is the resource provider’s name after the Microsoft part, with each word starting with a capital letter separated by dashes, e.g., Microsoft.Network = network.
    • <hyphenated ARM resource type> is the singular version of the word after the resource provider, with each word starting with a capital letter separated by dashes, e.g., Microsoft.Network/virtualNetworks = virtual-network.
    • <hyphenated child resource type (to be repeated for grandchildren, etc.)> is the singular version of the word after the resource provider, with each word starting with a capital letter separated by dashes, e.g., Microsoft.Network/virtualNetworks/subnets = subnet or Microsoft.Storage/storageAccounts/blobServices/containers = blob-service/container.

Terraform Resource Module Naming

  • Naming convention:
    • avm-res-<resource provider>-<ARM resource type> (module name for registry)
    • terraform-<provider>-avm-res-<resource provider>-<ARM resource type> (GitHub repository name to meet registry naming requirements)
  • Example: avm-res-compute-virtualmachine or avm-res-managedidentity-userassignedidentity
  • Segments:
    • <provider> is a legacy requirement of the Terraform registry. This must be set to azure
    • res defines this is a resource module
    • <resource provider> is the resource provider’s name after the Microsoft part, e.g., Microsoft.Compute = compute.
    • <ARM resource type> is the singular version of the word after the resource provider, e.g., Microsoft.Compute/virtualMachines = virtualmachine



See origin...

ID: RMNFR3 - Category: Composition - RP Collaboration

Module owners (Microsoft FTEs) SHOULD reach out to the respective Resource Provider teams to build a partnership and collaboration on the modules creation, existence and long term maintenance.

Review this wiki page (Microsoft Internal) for more information.




See origin...

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.




See origin...

ID: BCPFR2 - Category: Composition - Role Assignments Role Definition Mapping

Module owners MAY define common RBAC Role Definition names and IDs within a variable to allow consumers to define a RBAC Role Definition by their name rather than their ID, this should be self contained within the module themselves.

However, they MUST use only the official RBAC Role Definition name within the variable and nothing else.

To meet the requirements of BCPFR2, BCPNFR5 and BCPNFR6 you MUST use the below code sample in your AVM Modules to achieve this.

  @description('''Required. You can provide either the display name (note not all roles are supported, check module documentation) of the role definition, or its fully qualified ID in the following format: `/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11`.''')
  param roleDefinitionIdOrName string
  
  var builtInRbacRoleNames = {
    Owner: '/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635'
    Contributor: '/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c'
    Reader: '/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7'
    'Role Based Access Control Administrator (Preview)': '/providers/Microsoft.Authorization/roleDefinitions/f58310d9-a9f6-439a-9e8d-f62e7b41a168'
    'User Access Administrator': '/providers/Microsoft.Authorization/roleDefinitions/18d7d88d-d35e-4fb5-a5c3-7773c20a72d9'
    //Other RBAC Role Definitions Names & IDs can be added here as needed for your module
  }
  
  var roleDefinitionIdMappedResult = (contains(builtInRbacRoleNames, roleDefinitionIdOrName) ? builtInRbacRoleNames[roleDefinitionIdOrName] : roleDefinitionIdOrName)
  
  resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
    //Other properties removed for ease of reading
    properties: {
      roleDefinitionId: roleDefinitionIdMappedResult
      //Other properties removed for ease of reading
    }
  }
  



See origin...

ID: BCPFR6 - Cross-Referencing Child-Modules

Parent templates MUST reference all their direct child-templates to allow for an end-to-end deployment experience.
For example, the SQL server template must reference its child database module and encapsulate it in a loop to allow for the deployment of multiple databases.

@description('Optional. The databases to create in the server')
param databases databaseType[]?

resource server 'Microsoft.Sql/servers@(...)' = { (...) }

module server_databases 'database/main.bicep' = [for (database, index) in (databases ?? []): {
  name: '${uniqueString(server.id, location)}-Sql-DB-${index}'
  params: {
    serverName: server.name
    (...)
  }
}]



See origin...

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 outputs

Since 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 = { ... } // Correct



See origin...

ID: 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 .bicep file 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...



See origin...

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:

SegmentDescription
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:

  1. Deterministic — the same parent resource always produces the same hash, so repeated deployments overwrite rather than accumulate.
  2. 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:

ApproachDeterministic?Collision-free?Issue
deployment().nameChanges every deployment; hits 800-limit
utcNow() / timestampsChanges every deployment; hits 800-limit
subscription().id + resourceGroup().idSame hash for all resources in the same RG; collisions when deploying multiple instances
<parentResource>.id, locationRecommended — 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
    (...)
  }
}]



See origin...

ID: BCPNFR5 - Category: Composition - Role Assignments Role Definition Mapping Limits

As per BCPFR2, module owners MAY define common RBAC Role Definition names and IDs within a variable to allow consumers to define a RBAC Role Definition by their name rather than their ID.

Module owners SHOULD NOT map every RBAC Role Definition within this variable as it can cause the module to bloat in size and cause consumption issues later when stitched together with other modules due to the 4MB ARM Template size limit.

Therefore module owners SHOULD only map the most applicable and common RBAC Role Definition names for their module and SHOULD NOT exceed 15 RBAC Role Definitions in the variable.

Important

Remember if the RBAC Role Definition name is not included in the variable this does not mean it cannot be declared, used and assigned to an identity via an RBAC Role Assignment as part of a module, as any RBAC Role Definition can be specified via its ID without being in the variable.

Tip

Review the Bicep Contribution Guide’s ‘RBAC Role Definition Name Mapping’ section for a code sample to achieve this requirement.




See origin...

ID: BCPNFR6 - Category: Composition - Role Assignments Role Definition Mapping Compulsory Roles

Module owners MUST include the following roles in the variable for RBAC Role Definition names:

  • Owner - ID: 8e3af657-a8ff-443c-a75c-2fe8c4bcb635
  • Contributor - ID: b24988ac-6180-42a0-ab88-20f7382dd24c
  • Reader - ID: acdd72a7-3385-48ef-bd42-f606fba81ae7
  • User Access Administrator - ID: 18d7d88d-d35e-4fb5-a5c3-7773c20a72d9
  • Role Based Access Control Administrator (Preview) - ID: f58310d9-a9f6-439a-9e8d-f62e7b41a168
Tip

Review the Bicep Contribution Guide’s ‘RBAC Role Definition Name Mapping’ section for a code sample to achieve this requirement.




See origin...

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.1 for new modules, so that they can be released as v0.1.0.
  • 1.0 once the module owner signs off the module is stable enough for it’s first Major release of v1.0.0.
  • 0.x for all feature updates between the first release v0.1.0 and the first Major release of v1.0.0.



See origin...

ID: BCPRMNFR3 - Implementing child resources

Child resource modules MUST be stored in a subfolder of their parent resource module and named after the child resource’s singular name (ref), so that the path to the child resource folder is consistent with the hierarchy of its resource type.
For example, Microsoft.Sql/servers may have dedicated child resources of type Microsoft.Sql/servers/databases. Hence, the SQL server database child module is stored in a database subfolder of the server parent folder.

sql
└─ server [module]
  └─ database [child-module/resource]

In this folder, we recommend to place the child resource-template alongside a ReadMe & compiled JSON (to be generated via the default Set-AVMModule utility) and optionally further nest additional folders for its child resources.

There are several reasons to structure a module in this way. For example:

  • It allows a separation of concerns where each module can focus on its own properties and logic, while delegating most of a child-resource’s logic to its separate child module
  • It’s consistent with the provider namespace structure and makes modules easier to understand not only because they’re more aligned with set structure, but also are aligned with one another
  • As each module is its own ‘deployment’, it reduces limitations around nested loops
  • It enables module owners to publish child-modules as separate modules to the public registry, allowing consumers to make use of them directly [Ref child module publishing guidelines for details].
Note

In full transparency: The drawbacks of these additional deployments is an extended deployment period & a contribution to the 800 deployments limit. However, for AVM resource modules it was agreed that the advantages listed above outweigh these limitations.




See origin...

ID: BCPRMNFR4 - Implementing multi-scope modules

Several resource types in Azure (e.g., role-assignments, budgets, etc.) may be deployed to more than one scope (e.g., subscription, management-group, etc.).
In AVM, such modules can be implemented in one of two ways:

  1. As pattern modules with one ‘orchestrating’ parent module using scoped sub-modules based on the input parameters provided

Note: Only the parent module is published. I.e., it is not possible to target e.g., the resource-group scoped sub-module directly.

  1. As resource modules where each scope is implemented as a child-module of a non-published parent.

Note: Each child module is published, but not the parent. I.e., it is possible to target e.g., the resource-group scoped sub-module directly.

Tip

It is highly recommended to publish multi-scoped modules as resource modules as the solution provides the best user experience.

Considerations when published as a pattern module

Example: avm/ptn/authorization/role-assignment

Note

The following instructions consider all deployment scopes. Your module may only deploy to a subset of the same and you should map the conventions to your case.

To successfully implement a multi-scoped module as a pattern modules you have to adhere to the following convention:

  • The parent module MUST be implemented in the highest scope the resource provider supports (e.g., management-group)
  • The parent module MUST have one sub-module for each scope that the resource provider supports (e.g., management-group, subscription & resource-group)
  • Each sub-module MUST be implemented for the scope it is intended
  • The parent module MUST invoke each sub-module in the scope it is written for, using input parameters needed to target set scope (e.g., a subscription-id to invoke a module for set scope)
  • The parent module MUST have test cases to validate each sub-module
  • The parent module is the one that is versioned, published and maintains a changelog

The full folder structure may look like

📄main.bicep                 [Orchestrating module]
📄main.json                  [ARM JSON file of the module]
📄version.json               [Version file of the module]
📄README.md                  [Readme of the module]
📄CHANGELOG.md               [The changelog of the module]
┣ 📂modules
┃ ┣ 📄management-group.bicep [Sub-module deploying to the mgmt-group scope (if applicable)]
┃ ┣ 📄subscription.bicep     [Sub-module deploying to the subscription scope (if applicable)]
┃ ┗ 📄resource-group.bicep   [Sub-module deploying to the resource-group scope (if applicable)]
┗ 📂tests/e2e
  ┣ 📂 mg.defaults
  ┃ ┗ 📄main.test.bicep      [deploys parent template]
  ┣ 📂 mg.waf-aligned
  ┃ ┗ 📄main.test.bicep      [deploys parent template]
  ┣ 📂 sub.defaults
  ┃ ┗ 📄main.test.bicep      [deploys parent template with `subscriptionId` param]
  ┣ 📂 sub.waf-aligned
  ┃ ┗ 📄main.test.bicep      [deploys parent template with `subscriptionId` param]
  ┣ 📂 rg.defaults
  ┃ ┗ 📄main.test.bicep      [deploys parent template with `subscriptionId` & `resourceGroupName` params]
  ┗ 📂 rg.waf-aligned
    ┗ 📄main.test.bicep      [deploys parent template with `subscriptionId` & `resourceGroupName` params]
Warning

Even if a consumer wants to deploy to one of the sub-scopes (e.g., subscription), the module must be deployed via its parent (e.g., management-group). This can be confusing for consumers at first and should be considered when implementing the solution.

Example: To use a role-assignment pattern module (which would be written for all scopes, with the parent targeting the management-group scope) to deploy role assignments to a resource group, a user would need to invoke New-AzManagementGroupDeployment and provide the parameters for both the subscription & resource-group to target. I.e., the user must have permissions to deploy to each scope.

Considerations when published as a resource module

Example: avm/res/authorization/role-assignment

Note

The following instructions consider all deployment scopes. Your module may only deploy to a subset of the same and you should map the conventions to your case.

To successfully implement a multi-scoped module as a resource modules you have to adhere to the following convention:

  • The parent folder MUST contain a

    • main.bicep file
    • main.json file
    • README.md file
    • tests/e2e folder
    • One folder per each scope the resource provider can deploy to (either mg-scope, sub-scope or rg-scope).
  • Each child-module folder MUST be implemented as a proper child module, with a

    • main.bicep
    • main.json
    • version.json
    • README.md
    • CHANGELOG.md

    file. Each child-module is maintained and versioned independently of the others.

  • The parent main.bicep MUST contain the following information

    metadata name = '<Module Name> (Multi-Scope)'
    metadata description = '''
    This module's child-modules deploy a <Placeholder> at a Management Group (mg-scope), Subscription (sub-scope) or   Resource Group (rg-scope) scope.
    
    > While this template is **not** published, you can find the actual published modules in the subfolders
    > - `mg-scope`
    > - `sub-scope`
    > - `rg-scope`
    '''
    targetScope = 'managementGroup'

    updated with your module’s specifics

  • The tests/e2e folder MUST contain one instance of the require test cases per each scope, and MAY contain any additional test you see fit. In each case, the scope MUST be a prefix for the folder name. Each test case MUST reference the corresponding child module directly.

The full folder structure may look like

📄main.bicep                [Skeleton module with disclaimer referring to the child-modules]
📄main.json                 [ARM JSON file of the module]
📄README.md                 [The baseline readme, surfacing the metadata of the main.bicep file]
┣ 📂mg-scope
┃ ┣📄main.bicep             [Module deploying to mg-scope]
┃ ┣📄main.json              [ARM JSON file of the module]
┃ ┣📄README.md              [Readme of the module]
┃ ┣📄version.json           [Version file of the module]
┃ ┗📄CHANGELOG.md           [The changelog of the module]
┣ 📂sub-scope
┃ ┣📄main.bicep             [Module deploying to sub-scope]
┃ ┣📄main.json              [ARM JSON file of the module]
┃ ┣📄README.md              [Readme of the module]
┃ ┣📄version.json           [Version file of the module]
┃ ┗📄CHANGELOG.md           [The changelog of the module]
┣ 📂rg-scope
┃ ┣📄main.bicep             [Module deploying to rg-scope]
┃ ┣📄main.json              [ARM JSON file of the module]
┃ ┣📄README.md              [Readme of the module]
┃ ┣📄version.json           [Version file of the module]
┃ ┗📄CHANGELOG.md           [The changelog of the module]
┗ 📂tests/e2e
  ┣ 📂mg-scope.defaults
  ┃ ┗📄main.test.bicep      [references the 'mg-scope' child module template: '../../../mg-scope/main.bicep']
  ┣ 📂mg-scope.waf-aligned
  ┃ ┗📄main.test.bicep      [references the 'mg-scope' child module template: '../../../mg-scope/main.bicep']
  ┣ 📂mg-scope.max
  ┃ ┗📄main.test.bicep      [references the 'mg-scope' child module template: '../../../mg-scope/main.bicep']
  ┣ 📂sub-scope.defaults
  ┃ ┗📄main.test.bicep      [references the 'sub-scope' child module template: '../../../sub-scope/main.bicep']
  ┣ 📂sub-scope.waf-aligned
  ┃ ┗📄main.test.bicep      [references the 'sub-scope' child module template: '../../../sub-scope/main.bicep']
  ┣ 📂rg-scope.defaults
  ┃ ┗📄main.test.bicep      [references the 'rg-scope' child module template: '../../../rg-scope/main.bicep']
  ┗ 📂rg-scope.waf-aligned
    ┗📄main.test.bicep      [references the 'rg-scope' child module template: '../../../rg-scope/main.bicep']
Important

Because each child-module is published on its own, you must ensure that each is registered in the MAR-file before the modules can be published. The MAR-file can only be accessed by Microsoft FTEs.

Please highlight the nature of your module in the issue when proposing it to AVM.




Inputs / Outputs

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR14Data TypesSHOULDOwnerContributorBAU
2SNFR22Parameters/Variables for Resource IDsMUSTOwnerContributorBAU
3SNFR26Output - Parameters - DecoratorsMUSTOwnerContributorBAU
4RMFR6Parameter/Variable NamingMUSTOwnerContributorBAU
5RMFR7Minimum Required OutputsMUSTOwnerContributorBAU
6RMNFR2Parameter/Variable NamingMUSTOwnerContributorBAU
7BCPNFR1Complex data types - GeneralMUSTOwnerContributorBAU
8BCPNFR9Inputs - DecoratorsMUSTOwnerContributorBAU
9BCPNFR18User-defined types - SpecificationMUSTOwnerContributorBAU
10BCPNFR19User-defined types - NamingMUSTOwnerContributorBAU
11BCPNFR20User-defined types - ExportMUSTOwnerContributorBAU
12BCPNFR21User-defined types - DecoratorsMUSTOwnerContributorBAU
13BCPNFR7Parameter Requirement TypesMUSTOwnerContributorBAU
14BCPRMNFR2User-defined types - AVM-Common-TypesMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

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.




See origin...

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
  }
}



See origin...

ID: SNFR26 - Output-Parameters - Decorators

Output parameters MUST implement:

Output parameters
@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
}



See origin...

ID: RMFR6 - Category: Inputs - Parameter/Variable Naming

Parameters/variables that pertain to the primary resource MUST NOT use the resource type in the name.

e.g., use sku, vs. virtualMachineSku/virtualmachine_sku

Another example for where RPs contain some of their name within a property, leave the property unchanged. E.g. Key Vault has a property called keySize, it is fine to leave as this and not remove the key part from the property/parameter name.




See origin...

ID: RMFR7 - Category: Outputs - Minimum Required Outputs

Module owners MUST output the following outputs as a minimum in their modules:

OutputBicep Output NameTerraform Output Name
Resource Namenamename
Resource IDresourceIdresource_id
System Assigned Managed Identity Principal ID (if supported by module)systemAssignedMIPrincipalIdsystem_assigned_mi_principal_id
Tip

Module owners MAY also have to provide additional outputs depending on the IaC language, please check the language specific specs:




See origin...

ID: RMNFR2 - Category: Inputs - Parameter/Variable Naming

A resource module MUST use the following standard inputs:

  • name (no default)
  • location (if supported by the resource and not a global resource, then use Resource Group location, if resource supports Resource Groups, otherwise no default)



See origin...

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.




See origin...

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 string



See origin...

ID: 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.




See origin...

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 outputs

Since 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 = { ... } // Correct



See origin...

ID: 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.




See origin...

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')
}



See origin...

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 TypeDefinitionExample Description Decorator
RequiredThe 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...>')
ConditionalThe 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...>')
OptionalThe parameter value is not mandatory. The module provides a default value for the parameter.@description('Optional. <PARAMETER DESCRIPTION HERE...>')
GeneratedThe 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...>')



See origin...

ID: BCPRMNFR2 - User-defined types - AVM-Common-Types

When implementing any of the Bicep interface variants you MUST import their User-defined type (UDT) via the published AVM-Common-Types module.

When doing so, each type MUST be imported separately, right above the parameter or output that uses it.

import { roleAssignmentType } from 'br/public:avm/utl/types/avm-common-types:*.*.*'
@description('Optional. Array of role assignments to create.')
param roleAssignments roleAssignmentType[]?
import { diagnosticSettingFullType } from 'br/public:avm/utl/types/avm-common-types:*.*.*'
@description('Optional. The diagnostic settings of the service.')
param diagnosticSettings diagnosticSettingFullType[]?

Importing them individually as opposed to one common block has several benefits such as

  • Individual versioning of types
  • If you must update the version for one type, you’re not exposed to unexpected changes to other types
Important

The import (...) block MUST not be added in between a parameter’s definition and its metadata. Doing so breaks the metadata’s binding to the parameter in question.

Finally, you should check for version updates regularly to ensure the resource module stays consistent with the specs. If the used AVM-Common-Types runs stale, the CI may eventually fail the module’s static tests.




Testing

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR1Prescribed TestsMUSTOwnerContributorBAU
2SNFR2E2E TestingMUSTOwnerContributorBAU
3SNFR3AVM Compliance TestsMUSTOwnerContributorInitial
4SNFR4Unit TestsSHOULDOwnerContributorBAU
5SNFR5Upgrade TestsSHOULDOwnerContributorBAU
6SNFR6Static Analysis/Linting TestsMUSTOwnerContributorBAU
7SNFR7Idempotency TestsMUSTOwnerContributorBAU
8SNFR24Testing Child, Extension & Interface ResourcesMUSTOwnerContributorBAU
9BCPNFR10Test Bicep File NamingMUSTOwnerContributorBAU
10BCPNFR11Test ToolingMUSTOwnerContributorBAU
11BCPNFR12Deployment Test NamingMUSTOwnerContributorBAU
12BCPNFR13Test file metadataMUSTOwnerContributorBAU
13BCPNFR16Post-deployment testsMUSTOwnerContributorBAU
14BCPRMNFR1Expected Test DirectoriesMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

ID: SNFR1 - Category: Testing - Prescribed Tests

Modules MUST use the prescribed tooling and testing frameworks defined in the language specific specs.




See origin...

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

To see a directory and file structure for a module, see the language specific contribution guide.

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.
➕ 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.

Details for use in E2E tests

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>).




See origin...

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/avm-core-team-technical-bicep, OR @Azure/avm-core-team-technical-terraform.




See origin...

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.




See origin...

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.




See origin...

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.




See origin...

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.




See origin...

ID: SNFR24 - Category: Testing - Testing Child, Extension & Interface Resources

Module owners MUST test that child and extension resources and those Bicep or Terreform interface resources that are supported by their modules, are validated in E2E tests as per SNFR2 to ensure they deploy and are configured correctly.

These MAY be tested in a separate E2E test and DO NOT have to be tested in each E2E test.




See origin...

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.




See origin...

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.



See origin...

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.




See origin...

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.



See origin...

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 e2e test’s folder or any subfolder (e.g., e2e/max/myTest.tests.ps1 or e2e/max/tests/myTest.tests.ps1)

  • Implement an input parameter TestInputData in the following way:

    param (
        [Parameter(Mandatory = $false)]
        [hashtable] $TestInputData = @{}
    )

    Through this parameter you can make use of every output the main.test.bicep file 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.resourceId defined in the main.test.bicep file, the $TestInputData would 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
            }
        }
    }



See origin...

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
#IDTitleSeverityPersonaLifecycle
1SNFR15Automatic Documentation GenerationMUSTOwnerContributorBAU
2SNFR16Examples/E2EMUSTOwnerContributorBAU
3BCPNFR2Module Documentation GenerationMUSTOwnerContributorBAU
4BCPNFR3Usage Example formatsMUSTOwnerContributorBAU
5BCPNFR4Parameter Input ExamplesMAYOwnerContributorBAU
➕ See Specifications for this category
See origin...

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.




See origin...

ID: SNFR16 - Category: Documentation - Examples/E2E

An examples/e2e directory MUST exist to provide named scenarios for module deployment.




See origin...

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



See origin...

ID: BCPNFR3 - Category: Documentation - Usage Example formats

Usage examples for Bicep modules MUST be provided in the following formats:

  • Bicep file (orchestration module style) - .bicep

    module <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.




See origin...

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 customerManagedKeyType

It 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
#IDTitleSeverityPersonaLifecycle
1SNFR17Semantic VersioningMUSTOwnerContributorBAU
2SNFR18Breaking ChangesSHOULDOwnerContributorBAU
3SNFR19Registries TargetedMUSTOwnerContributorBAU
4SNFR21Cross Language CollaborationSHOULDOwnerContributorBAU
5BCPNFR22Bicep Module ChangelogMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

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 Version
  • Y == Minor Version
  • Z == Patch Version

Module versioning before first Major version release 1.0.0

  • Initially modules MUST be released as version 0.1.0 and 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 version 1.0.0.

    Note

    Releasing as version 0.1.0 initially 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.0 is released, given a version number X.Y.Z:

    • X Major version MUST NOT be bumped.
    • Y Minor version MUST be bumped when introducing breaking changes (which would normally bump Major after 1.0.0 release) or feature updates (same as it will be after 1.0.0 release).
    • Z Patch version MUST be bumped when introducing non-breaking, backward compatible bug fixes (same as it will be after 1.0.0 release).



See origin...

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.




See origin...

ID: SNFR19 - Category: Publishing - Registries Targeted

Modules MUST be published to their respective language public registries.

Tip

See the language specific contribution guides for detailed guidance and sample code to use in AVM modules to achieve this requirement.




See origin...

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.




See origin...

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

- None

Each version’s entry:

  • MUST contain two sections: Changes and Breaking Changes. At least one of them must have a meaningful entry and sections must not be left empty. A - None may 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

- None

Each 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 ## Changes and ## Breaking Changes with 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 as tags for each module https://mcr.microsoft.com/v2/bicep/avm/(res|ptn|utl)/<namespace/modulename>/tags/list




Code Style

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1BCPNFR8Code Styling - lower camelCasingSHOULDOwnerContributorBAU
2BCPNFR17Code Styling - Type castingSHOULDOwnerContributorBAU
➕ See Specifications for this category
See origin...

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)




See origin...

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

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

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))
  }
}



Bicep Utility Module Specifications

Contribution / Support

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR8Module Owner(s) GitHubMUSTOwnerInitial
2SNFR20GitHub Teams OnlyMUSTOwnerInitial
3SNFR9AVM & PG Teams GitHub Repo PermissionsMUSTOwnerInitial
4SNFR10MIT LicensingMUSTOwnerInitial
5SNFR11Issues Response TimesMUSTOwnerContributorBAU
6SNFR12Versions SupportedMUSTOwnerBAU
7SNFR23GitHub Repo LabelsMUSTOwnerBAU
8BCPNFR15AVM Module Issue template fileMUSTOwnerBAU
➕ See Specifications for this category
See origin...

ID: SNFR8 - Category: Contribution/Support - Module Owner(s) GitHub

A module MUST have an owner that is defined and managed by a GitHub Team in the Azure GitHub organization.

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

The names for the GitHub teams for each approved module are already defined in the respective Module Indexes. These teams MUST be created (and used) for each module.




See origin...

ID: SNFR20 - Category: Contribution/Support - GitHub Teams Only

All GitHub repositories that AVM module are published from and hosted within MUST only assign GitHub repository permissions to GitHub teams only.

Each module MUST have a GitHub team assigned for module owners. This team MUST be created in the Azure organization in GitHub.

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

Important

As part of the module proposal process, the name of the GitHub team for each approved module is already defined in the respective Module Indexes (or CSV file). This team MUST be created (and used) for each module.

Module owners don’t need to construct the name of the GitHub team for their module themselves, instead they need use the name prescribed in the related CSV file, at the time of approval.

For a direct link, see the list of related index pages:

The @Azure prefix in the last column of the tables linked above represents the “Azure” GitHub organization all AVM-related repositories exist in. DO NOT include this segment in the team’s name!

Naming Convention

The naming convention for the GitHub teams MUST follow the below pattern:

  • <hyphenated module name>-module-owners-bicep - to grant permissions for module owners on Bicep modules

Segments:

  • <hyphenated module name> == the AVM Module’s name, with each segment separated by dashes, i.e., avm-res-<resource provider>-<ARM resource type>
    • See RMNFR1 for AVM Resource Module Naming
    • See PMNFR1 for AVM Pattern Module Naming
  • module-owners == the role the GitHub Team is assigned to
  • <bicep == the language the module is written in

Examples:

  • avm-res-compute-virtualmachine-module-owners-bicep
Note

The naming convention for Bicep modules is slightly different than the naming convention for their respective GitHub teams.

Add Team Members

All officially documented module owner(s) MUST be added to the -module-owners- team. The -module-owners- team MUST NOT have any other members.

Unless explicitly requested and agreed, members of the AVM core team or any PG teams MUST NOT be added to the -module-owners- teams as permissions for them are granted through the teams described in SNFR9.

Grant permissions through team memberships

Note

In case of Bicep modules, permissions to the BRM repository (the repo of the Bicep Registry) are granted via assigning the -module-owners- teams to parent teams that already have the required level access configured. While it is the module owner’s responsibility to initiate the addition of their team to the respective parent, only the AVM core team can approve this parent-child relationship.

Module owners MUST create their -module-owners- team and as part of the provisioning process, they MUST request the addition of this team to its respective parent team (see the table below for details).

GitHub Team NameDescriptionPermissionsPermissions granted throughWhere to work?
<hyphenated module name>-module-owners-bicepAVM Bicep Module Owners - <module name>WriteAssignment to the avm-technical-reviewers-bicep parent team.Need to work in a fork.

Example - GitHub team required for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

  • avm-res-network-virtualnetwork-module-owners-bicep –> assign to the avm-technical-reviewers-bicep parent team.
Tip

Direct link to create a new GitHub team and assign it to its parent: Create new team

Fill in the values as follows:

  • Team name: Following the naming convention described above, use the value defined in the module indexes.
  • Description: Follow the guidance above (see the Description column in the table above).
  • Parent team: Follow the guidance above (see the Permissions granted through column in the table above).
  • Team visibility: Visible
  • Team notifications: Enabled

CODEOWNERS file

As part of the “initial Pull Request” (that publishes the first version of the module), module owners MUST add an entry to the CODEOWNERS file in the BRM repository (here).

Note

Through this approach, the AVM core team will grant review permission to module owners as part of the standard PR review process.

Every CODEOWNERS entry (line) MUST include the following segments separated by a single whitespace character:

  • Path of the module, relative to the repo’s root, e.g.: /avm/res/network/virtual-network/
  • The -module-owners-team, with the @Azure/ prefix, e.g., @Azure/avm-res-network-virtualnetwork-module-owners-bicep
  • The GitHub team of the AVM Bicep reviewers, with the @Azure/ prefix, i.e., @Azure/avm-module-reviewers-bicep

Example - CODEOWNERS entry for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

  • /avm/res/network/virtual-network/ @Azure/avm-res-network-virtualnetwork-module-owners-bicep @Azure/avm-module-reviewers-bicep

Terraform

Note

Access management for Terraform repositories now uses a single team, membership of which is managed using an internal entitlement management tool (Core Identity).

All module owners MUST request access to the avm-module-owners-terraform GitHub team via the Azure Verified Module Owners Terraform entitlement in Core Identity (Microsoft internal tool).




See origin...

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

Note

These required GitHub teams are already associated to the BRM repository and have the required permissions.

Terraform

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.




See origin...

ID: SNFR10 - Category: Contribution/Support - MIT Licensing

A module MUST be published with the MIT License in the Azure GitHub organization.




See origin...

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.




See origin...

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.




See origin...

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:

➕ AVM Standard GitHub Labels

These labels are available in a CSV file from here

NameDescriptionHEX
AZD 🧑‍💻These modules are requested/used by the AZD team.
E0BFFA
Needs: Attention 👋Reply has been added to issue, maintainer to review
E99695
Needs: Immediate Attention ‼️Immediate attention of module owner / AVM team is needed
FF0000
Needs: Author Feedback 👂Awaiting feedback from the issue/PR author
F18A07
Needs: External Changes ⚒️When an issue/PR requires changes that are outside of the control of the module. e.g. to an RP.
DE389D
Needs: More Evidence ⚖We are looking for more evidence to make a decision on this
F64872
Needs: Triage 🔍Maintainers need to triage still
FBCA04
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.
FF0019
Needs: Module Contributor 📣This module needs secondary owner(s) or contributor(s) to develop or maintain it
C95474
Needs: Core Team 🧞‍♂️This item needs the AVM Core Team to review it
DB4503
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
800080
Status: Do Not Merge ⛔Do not merge PRs with this label attached as they are not ready or aligned to future direction etc.
8B4513
Status: External Contribution 🌍This is being worked on by someone outside of the AVM module owners/contributors or AVM core team
D8FA2C
Status: Fixed ✅Auto label applied when issue fixed by merged PR
90EE90
Status: Help Wanted 🆘Extra attention is needed
FF4500
Status: In Triage 🔍Picked up for triaging by an AVM core team member
D4AF37
Status: In PR 👉This is when an issue is due to be fixed in an open PR
EDEDED
Status: Invalid ❌This doesn't seem right
E4E669
Status: Long Term ⏳We will do it, but will take a longer amount of time due to complexity/priorities
B60205
Status: No Recent Activity 💤When an issue/PR has not been modified for X amount of days
808080
Status: Won't Fix 💔This will not be worked on
FFFFFF
Status: Owners Identified 🤘This module has its owners identified
FBEF2A
Status: Module Available 🟢The module is published
C8E6C9
Status: Module Deprecated 🔴This is a request to deprecate a module
000000
Status: Module Orphaned 🟡The module has no owner and is therefore orphaned at this time
F4A460
Status: Ready For Repository Creation 📝This module is approved and the owner is ready for the repository to be created (Terraform)
136A41
Status: Repository Created 📄This module has had it's repository created and configured ready for owner contribution (Terraform)
27AB03
Status: Response Overdue 🚩When an issue/PR has not been responded to for X amount of days
850000
Status: Looking For Assistance 🦆This item is looking for anyone to help develop the code and submit a PR for resolution
03FCC2
Type: Bug 🐛Something isn't working
D73A4A
Type: CI 🚀This issue is related to the AVM CI
74CFB0
Type: Documentation 📄Improvements or additions to documentation
0075CA
Type: Duplicate 🤲This issue or pull request already exists
CFD3D7
Type: Feature Request ➕New feature or request
A2EEEF
Type: Hygiene 🧹things related to testing, issue triage etc.
17016A
Type: New Module Proposal 💡A new module for AVM is being proposed
ADD8E6
Type: Question/Feedback 🙋‍♀️Further information is requested or just some feedback
CB6BA2
Type: Security Bug 🔒This is a security bug
FFFF00
Type: AVM 🅰️ ✌️ ⓜ️This is an AVM related issue
F0FFFF
Language: Terraform 🌐This is related to the Terraform IaC language
7740B6
Language: Bicep 💪This is related to the Bicep IaC language
1D73B3
Class: Resource Module 📦This is a resource module
D3D3D3
Class: Pattern Module 📦This is a pattern module
A9A9A9
Class: Utility Module 📦This is a utility module
CAD1DE
Class: Child Module 📦This is a child module
5E5186

To help apply these to a module GitHub repository you can use the below PowerShell script:

➕ Set-AvmGitHubLabels.ps1

For most scenario this is the command you’ll need to call the below PowerShell script with, replacing the value for RepositoryName:

  Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -CreateCsvLabelExports $false -NoUserPrompts $true
```shell
# Linux / MacOs
# For Windows replace $PWD with your the local path or your repository
#
docker run -it -v $PWD:/repo -w /repo mcr.microsoft.com/powershell pwsh -Command '
    #Invoke-WebRequest -Uri "https://azure.github.io/Azure-Verified-Modules/scripts/Set-AvmGitHubLabels.ps1" -OutFile "Set-AvmGitHubLabels.ps1"
    $gh_version = "2.44.1"
    Invoke-WebRequest -Uri "https://github.com/cli/cli/releases/download/v2.44.1/gh_2.44.1_linux_amd64.tar.gz" -OutFile "gh_$($gh_version)_linux_amd64.tar.gz"
    apt-get update && apt-get install -y git
    tar -xzf "gh_$($gh_version)_linux_amd64.tar.gz"
    ls -lsa
    mv "gh_$($gh_version)_linux_amd64/bin/gh" /usr/local/bin/
    rm "gh_$($gh_version)_linux_amd64.tar.gz" && rm -rf "gh_$($gh_version)_linux_amd64"
    gh --version
    ls -lsa
    gh auth login
    $OrgProject = "Azure/terraform-azurerm-avm-res-kusto-cluster"
    gh auth status
    ./Set-AvmGitHubLabels.ps1 -RepositoryName $OrgProject -CreateCsvLabelExports $false -NoUserPrompts $true

  '
```

By default this script will only update and append labels on the repository specified. However, this can be changed by setting the parameter -UpdateAndAddLabelsOnly to $false, which will remove all the labels from the repository first and then apply the AVM labels from the CSV only.

Make sure you elevate your privilege to admin level or the labels will not be applied to your repository. Go to repos.opensource.microsoft.com/orgs/Azure/repos/ to request admin access before running the script.

Full Script:

These Set-AvmGitHubLabels.ps1 can be downloaded from here.

  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Coloured output required in this script")]
  
  <#
  .SYNOPSIS
    This script can be used to create the Azure Verified Modules (AVM) standard GitHub labels to a GitHub repository.
  
  .DESCRIPTION
    This script can be used to create the Azure Verified Modules (AVM) standard GitHub labels to a GitHub repository.
  
    By default, the script will remove all pre-existing labels and apply the AVM labels. However, this can be changed by using the -RemoveExistingLabels parameter and setting it to $false. The tool will also output the labels that exist in the repository before and after the script has run to a CSV file in the current directory, or a directory specified by the -OutputDirectory parameter.
  
    The AVM labels to be created are documented here: TBC
  
  .NOTES
    Please ensure you have specified the GitHub repositry correctly. The script will prompt you to confirm the repository name before proceeding.
  
  .COMPONENT
    You must have the GitHub CLI installed and be authenticated to a GitHub account with access to the repository you are applying the labels to before running this script.
  
  .LINK
    TBC
  
  .Parameter RepositoryName
    The name of the GitHub repository to apply the labels to.
  
  .Parameter RemoveExistingLabels
    If set to $true, the default value, the script will remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels. If set to $false, the script will not remove any pre-existing labels.
  
  .Parameter UpdateAndAddLabelsOnly
    If set to $true, the default value, the script will only update and add labels to the repository specified in -RepositoryName. If set to $false, the script will remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels.
  
  .Parameter OutputDirectory
    The directory to output the pre-existing and post-existing labels to in a CSV file. The default value is the current directory.
  
  .Parameter CreateCsvLabelExports
    If set to $true, the default value, the script will output the pre-existing and post-existing labels to a CSV file in the current directory, or a directory specified by the -OutputDirectory parameter. If set to $false, the script will not output the pre-existing and post-existing labels to a CSV file.
  
  .Parameter GitHubCliLimit
    The maximum number of labels to return from the GitHub CLI. The default value is 999.
  
  .Parameter LabelsToApplyCsvUri
    The URI to the CSV file containing the labels to apply to the GitHub repository. The default value is https://raw.githubusercontent.com/jtracey93/label-source/main/avm-github-labels.csv.
  
  .Parameter NoUserPrompts
    If set to $true, the default value, the script will not prompt the user to confirm they want to remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels. If set to $false, the script will prompt the user to confirm they want to remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels.
  
    This is useful for running the script in automation workflows
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and remove all pre-existing labels.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo"
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -RemoveExistingLabels $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and output the pre-existing and post-existing labels to the directory C:\GitHubLabels.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels"
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and output the pre-existing and post-existing labels to the directory C:\GitHubLabels and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels" -RemoveExistingLabels $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not create the pre-existing and post-existing labels CSV files and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -RemoveExistingLabels $false -CreateCsvLabelExports $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not create the pre-existing and post-existing labels CSV files and do not remove any pre-existing labels, just overwrite any labels that have the same name. Finally, use a custom CSV file hosted on the internet to create the labels from.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels" -RemoveExistingLabels $false -CreateCsvLabelExports $false -LabelsToApplyCsvUri "https://example.com/csv/avm-github-labels.csv"
  
  #>
  
  #Requires -PSEdition Core
  
  [CmdletBinding()]
  param (
    [Parameter(Mandatory = $true)]
    [string]$RepositoryName,
  
    [Parameter(Mandatory = $false)]
    [bool]$RemoveExistingLabels = $true,
  
    [Parameter(Mandatory = $false)]
    [bool]$UpdateAndAddLabelsOnly = $true,
  
    [Parameter(Mandatory = $false)]
    [bool]$CreateCsvLabelExports = $true,
  
    [Parameter(Mandatory = $false)]
    [string]$OutputDirectory = (Get-Location),
  
    [Parameter(Mandatory = $false)]
    [int]$GitHubCliLimit = 999,
  
    [Parameter(Mandatory = $false)]
    [string]$LabelsToApplyCsvUri = "https://azure.github.io/Azure-Verified-Modules/governance/avm-standard-github-labels.csv",
  
    [Parameter(Mandatory = $false)]
    [bool]$NoUserPrompts = $false
  )
  
  # Check if the GitHub CLI is installed
  $GitHubCliInstalled = Get-Command gh -ErrorAction SilentlyContinue
  if ($null -eq $GitHubCliInstalled) {
    throw "The GitHub CLI is not installed. Please install the GitHub CLI and try again."
  }
  Write-Host "The GitHub CLI is installed..." -ForegroundColor Green
  
  # Check if GitHub CLI is authenticated
  $GitHubCliAuthenticated = gh auth status
  if ($LASTEXITCODE -ne 0) {
    Write-Host $GitHubCliAuthenticated -ForegroundColor Red
    throw "Not authenticated to GitHub. Please authenticate to GitHub using the GitHub CLI, `gh auth login`, and try again."
  }
  Write-Host "Authenticated to GitHub..." -ForegroundColor Green
  
  # Check if GitHub repository name is valid
  $GitHubRepositoryNameValid = $RepositoryName -match "^[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$"
  if ($false -eq $GitHubRepositoryNameValid) {
    throw "The GitHub repository name $RepositoryName is not valid. Please check the repository name and try again. The format must be <OrgName>/<RepoName>"
  }
  
  # List GitHub repository provided and check it exists
  $GitHubRepository = gh repo view $RepositoryName
  if ($LASTEXITCODE -ne 0) {
    Write-Host $GitHubRepository -ForegroundColor Red
    throw "The GitHub repository $RepositoryName does not exist. Please check the repository name and try again."
  }
  Write-Host "The GitHub repository $RepositoryName exists..." -ForegroundColor Green
  
  # PRE - Get the current GitHub repository labels and export to a CSV file in the current directory or where -OutputDirectory specifies if set to a valid directory path and the directory exists or can be created if it does not exist already
  if ($RemoveExistingLabels -or $UpdateAndAddLabelsOnly) {
    Write-Host "Getting the current GitHub repository (pre) labels for $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
  
    if ($null -ne $GitHubRepositoryLabels -and $CreateCsvLabelExports -eq $true) {
      $csvFileNamePathPre = "$OutputDirectory\$($RepositoryName.Replace('/', '_'))-Labels-Pre-$(Get-Date -Format FileDateTime).csv"
      Write-Host "Exporting the current GitHub repository (pre) labels for $RepositoryName to $csvFileNamePathPre" -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | Export-Csv -Path $csvFileNamePathPre -NoTypeInformation
    }
  }
  
  # Remove all pre-existing labels if -RemoveExistingLabels is set to $true and user confirms they want to remove all pre-existing labels
  if ($null -ne $GitHubRepositoryLabels) {
    $GitHubRepositoryLabelsJson = $GitHubRepositoryLabels | ConvertFrom-Json
    if ($RemoveExistingLabels -eq $true -and $NoUserPrompts -eq $false -and $UpdateAndAddLabelsOnly -eq $false) {
      $RemoveExistingLabelsConfirmation = Read-Host "Are you sure you want to remove all $($GitHubRepositoryLabelsJson.Count) pre-existing labels from $($RepositoryName)? (Y/N)"
      if ($RemoveExistingLabelsConfirmation -eq "Y") {
        Write-Host "Removing all pre-existing labels from $RepositoryName..." -ForegroundColor Yellow
        $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
          Write-Host "Removing label $($_.name) from $RepositoryName..." -ForegroundColor DarkRed
          gh label delete -R $RepositoryName $_.name --yes
        }
      }
    }
    if ($RemoveExistingLabels -eq $true -and $NoUserPrompts -eq $true -and $UpdateAndAddLabelsOnly -eq $false) {
      Write-Host "Removing all pre-existing labels from $RepositoryName..." -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
        Write-Host "Removing label $($_.name) from $RepositoryName..." -ForegroundColor DarkRed
        gh label delete -R $RepositoryName $_.name --yes
      }
    }
  }
  if ($null -eq $GitHubRepositoryLabels) {
    Write-Host "No pre-existing labels to remove or not selected to be removed from $RepositoryName..." -ForegroundColor Magenta
  }
  
  # Check LabelsToApplyCsvUri is valid and contains a CSV content
  Write-Host "Checking $LabelsToApplyCsvUri is valid..." -ForegroundColor Yellow
  $LabelsToApplyCsvUriValid = $LabelsToApplyCsvUri -match "^https?://"
  if ($false -eq $LabelsToApplyCsvUriValid) {
    throw "The LabelsToApplyCsvUri $LabelsToApplyCsvUri is not valid. Please check the URI and try again. The format must be a valid URI."
  }
  Write-Host "The LabelsToApplyCsvUri $LabelsToApplyCsvUri is valid..." -ForegroundColor Green
  
  # Create AVM lables from the AVM labels CSV file stored on the web using the convertfrom-csv cmdlet
  $avmLabelsCsv = Invoke-WebRequest -Uri $LabelsToApplyCsvUri | ConvertFrom-Csv
  
  # Check if the AVM labels CSV file contains the following columns: Name, Description, HEX
  $avmLabelsCsvColumns = $avmLabelsCsv | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name
  $avmLabelsCsvColumnsValid = $avmLabelsCsvColumns -contains "Name" -and $avmLabelsCsvColumns -contains "Description" -and $avmLabelsCsvColumns -contains "HEX"
  if ($false -eq $avmLabelsCsvColumnsValid) {
    throw "The labels CSV file does not contain the required columns: Name, Description, HEX. Please check the CSV file and try again. It contains the following columns: $avmLabelsCsvColumns"
  }
  Write-Host "The labels CSV file contains the required columns: Name, Description, HEX" -ForegroundColor Green
  
  # Create the AVM labels in the GitHub repository
  Write-Host "Creating/Updating the $($avmLabelsCsv.Count) AVM labels in $RepositoryName..." -ForegroundColor Yellow
  $avmLabelsCsv | ForEach-Object {
    if ($GitHubRepositoryLabelsJson.name -contains $_.name) {
      Write-Host "The label $($_.name) already exists in $RepositoryName. Updating the label to ensure description and color are consitent..." -ForegroundColor Magenta
      gh label create -R $RepositoryName "$($_.name)" -c $_.HEX -d $($_.Description) --force
    }
    else {
      Write-Host "The label $($_.name) does not exist in $RepositoryName. Creating label $($_.name) in $RepositoryName..." -ForegroundColor Cyan
      gh label create -R $RepositoryName "$($_.Name)" -c $_.HEX -d $($_.Description) --force
    }
  }
  
  # POST - Get the current GitHub repository labels and export to a CSV file in the current directory or where -OutputDirectory specifies if set to a valid directory path and the directory exists or can be created if it does not exist already
  if ($CreateCsvLabelExports -eq $true) {
    Write-Host "Getting the current GitHub repository (post) labels for $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
  
    if ($null -ne $GitHubRepositoryLabels) {
      $csvFileNamePathPre = "$OutputDirectory\$($RepositoryName.Replace('/', '_'))-Labels-Post-$(Get-Date -Format FileDateTime).csv"
      Write-Host "Exporting the current GitHub repository (post) labels for $RepositoryName to $csvFileNamePathPre" -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | Export-Csv -Path $csvFileNamePathPre -NoTypeInformation
    }
  }
  
  # If -RemoveExistingLabels is set to $true and user confirms they want to remove all pre-existing labels check that only the avm labels exist in the repository
  if ($RemoveExistingLabels -eq $true -and ($RemoveExistingLabelsConfirmation -eq "Y" -or $NoUserPrompts -eq $true) -and $UpdateAndAddLabelsOnly -eq $false) {
    Write-Host "Checking that only the AVM labels exist in $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
    $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
      if ($avmLabelsCsv.Name -notcontains $_.name) {
        throw "The label $($_.name) exists in $RepositoryName but is not in the CSV file."
      }
    }
    Write-Host "Only the CSV labels exist in $RepositoryName..." -ForegroundColor Green
  }
  
  Write-Host "The CSV labels have been created/updated in $RepositoryName..." -ForegroundColor Green
  



See origin...

ID: BCPNFR15 - Category: Contribution/Support - AVM Module Issue template file

Module owners MUST add an entry to the AVM Module Issue template file in the BRM repository (here). When the module is deprecated, this entry MUST be removed from the file.

Note

Through this approach, the AVM core team will allow raising a bug or feature request for a module, only after the module gets merged to the BRM repository.

The module name entry MUST be added to the dropdown list with id module-name-dropdown as an option, in alphabetical order.

Important

Module owners MUST ensure that the module name is added in alphabetical order, to simplify selecting the right module name when raising an AVM module issue.

Example - AVM Module Issue template module name entry for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

- type: dropdown
  id: module-name-dropdown
  attributes:
    label: Module Name
    description: Which existing AVM module is this issue related to?
    options:
      ...
      - "avm/res/network/virtual-network"
      ...



Telemetry

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SFR3Deployment/Usage TelemetryMUSTOwnerInitial
2SFR4Telemetry Enablement FlexibilityMUSTOwnerInitial
3BCPFR4Telemetry EnablementMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

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 more details on this requirement, here.)

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.ps1 utility
  • Terraform: Executing the make docs command with the note and header ## Data Collection being placed in the module’s _footer.md beforehand
### 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

We will maintain a set of CSV files in the AVM Central Repo (Azure/Azure-Verified-Modules) with the required TelemetryId prefixes to enable checks to utilize this list to ensure the correct IDs are used. To see the formatted content of these CSV files with additional information, please visit the AVM Module Indexes page.

The value you need to use for your module is defined in the related module index. You can look it up on the index pages for Resource Modules, Pattern Modules and Utility Modules.

The ARM deployment name used for the telemetry MUST follow the pattern and MUST be no longer than 64 characters in length: 46d3xbcp.<res/ptn>.<(short) module name>.<version>.<uniqueness>

  • <res/ptn> == AVM Resource or Pattern Module
  • <(short) module name> == The AVM Module’s, possibly shortened, name including the resource provider and the resource type, without;
    • The prefixes: avm-res-
    • The prefixes: avm-ptn-
  • <version> == The AVM Module’s MAJOR.MINOR version (only) with . (periods) replaced with - (hyphens), to allow simpler splitting of the ARM deployment name
  • <uniqueness> == This section of the ARM deployment name is to be used to ensure uniqueness of the deployment name.
    • This is to cater for the following scenarios:
      • The module is deployed multiple times to the same:
        • Location/Region
        • Scope (Tenant, Management Group,Subscription, Resource Group)
Note

Due to the 64-character length limit of Azure deployment names, the <(short) module name> segment has a length limit of 36 characters, so if the module name is longer than that, it MUST be truncated to 36 characters. If any of the semantic version’s segments are longer than 1 character, it further restricts the number of characters that can be used for naming the module.

An example deployment name for the AVM Virtual Machine Resource Module would be: 46d3xbcp.res.compute-virtualmachine.1-2-3.eum3

An example deployment name for a shortened module name would be: 46d3xbcp.res.desktopvirtualization-appgroup.1-2-3.eum3

Tip

Terraform: Terraform uses a telemetry provider, the configuration of which is the same for every module and is included in the template repo.

General: See the language specific contribution guides for detailed guidance and sample code to use in AVM modules to achieve this requirement.

Terraform

To enable telemetry data collection for Terraform modules, the modtm telemetry provider MUST be used. This lightweight telemetry provider sends telemetry data to Azure Application Insights via a HTTP POST front end service.

The modtm telemetry provider is included in all Terraform modules and is enabled by default through the main.telemetry.tf file being automatically distributed from the template repo.

The modtm provider MUST be listed under the required_providers section in the module’s terraform.tf file using the following entry. This is also validated by the linter.

terraform {
  required_providers {
    # .. other required providers as needed
    modtm = {
      source = "Azure/modtm"
      version = "~> 0.3"
    }
  }
}



See origin...

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

Currently, no further requirements apply.




See origin...

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).

@description('Optional. Location for all resources.')
param location string = resourceGroup().location

@description('Optional. Enable/Disable usage telemetry for module.')
param enableTelemetry bool = true

#disable-next-line no-deployments-resources
resource avmTelemetry 'Microsoft.Resources/deployments@2025-04-01' = if (enableTelemetry) {
  name: take('46d3xbcp.res.compute-virtualmachine.${replace('-..--..-', '.', '-')}.${substring(uniqueString(deployment().name, location), 0, 4)}', 64)
  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
#IDTitleSeverityPersonaLifecycle
1SFR1Preview ServicesMUSTOwnerBAU
2SFR2WAF AlignedSHOULDOwnerBAU
3SNFR25Resource NamingMUSTOwnerInitial
4UMNFR1Module NamingMUSTOwnerInitial
5BCPFR1Cross-Referencing ModulesMAYOwnerContributorBAU
6BCPNFR19User-defined types - NamingMUSTOwnerContributorBAU
7BCPNFR23Module compositionMUSTOwnerContributorBAU
8BCPNFR24Deterministic Deployment NamesMUSTOwnerContributorBAU
9BCPNFR14VersioningMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

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., is 2021-05-01-preview
    • Otherwise the latest “non-preview” version of the API SHOULD be used

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”



See origin...

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:

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.




See origin...

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.




See origin...

ID: UMNFR1 - Category: Naming - Module Naming

Utility Modules MUST follow the below naming conventions (all lower case).

Important

As part of the module proposal process, the module’s approved name is captured both in the module proposal issue AND the related module index page (backed by the corresponding CSV file).

Therefore, module owners don’t need to construct the module’s name themselves, instead they need use the name prescribed in the module proposal issue or in the related CSV file, at the time of approval.

Bicep Utility Module Naming

  • Naming convention: avm/utl/<hyphenated grouping/category name>/<hyphenated utility module name>
  • Example: avm/utl/general/get-environment or avm/utl/types/avm-common-types
  • Segments:
    • utl defines 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: general or types
    • <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-finder or avm-utl-naming
  • Segments:
    • <provider> is a legacy requirement of the Terraform registry. For AVM Terraform utility modules this MUST be set to azure (for example Azure/avm-utl-naming/azure). Older utility modules may still use the azurerm or azuread segments.
    • utl defines 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.



See origin...

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.




See origin...

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 outputs

Since 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 = { ... } // Correct



See origin...

ID: 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 .bicep file 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...



See origin...

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:

SegmentDescription
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:

  1. Deterministic — the same parent resource always produces the same hash, so repeated deployments overwrite rather than accumulate.
  2. 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:

ApproachDeterministic?Collision-free?Issue
deployment().nameChanges every deployment; hits 800-limit
utcNow() / timestampsChanges every deployment; hits 800-limit
subscription().id + resourceGroup().idSame hash for all resources in the same RG; collisions when deploying multiple instances
<parentResource>.id, locationRecommended — 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
    (...)
  }
}]



See origin...

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.1 for new modules, so that they can be released as v0.1.0.
  • 1.0 once the module owner signs off the module is stable enough for it’s first Major release of v1.0.0.
  • 0.x for all feature updates between the first release v0.1.0 and the first Major release of v1.0.0.



Inputs / Outputs

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR14Data TypesSHOULDOwnerContributorBAU
2SNFR22Parameters/Variables for Resource IDsMUSTOwnerContributorBAU
3SNFR26Output - Parameters - DecoratorsMUSTOwnerContributorBAU
4BCPNFR1Complex data types - GeneralMUSTOwnerContributorBAU
5BCPNFR9Inputs - DecoratorsMUSTOwnerContributorBAU
6BCPNFR18User-defined types - SpecificationMUSTOwnerContributorBAU
7BCPNFR19User-defined types - NamingMUSTOwnerContributorBAU
8BCPNFR20User-defined types - ExportMUSTOwnerContributorBAU
9BCPNFR21User-defined types - DecoratorsMUSTOwnerContributorBAU
10BCPNFR7Parameter Requirement TypesMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

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.




See origin...

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
  }
}



See origin...

ID: SNFR26 - Output-Parameters - Decorators

Output parameters MUST implement:

Output parameters
@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
}



See origin...

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.




See origin...

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 string



See origin...

ID: 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.




See origin...

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 outputs

Since 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 = { ... } // Correct



See origin...

ID: 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.




See origin...

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')
}



See origin...

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 TypeDefinitionExample Description Decorator
RequiredThe 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...>')
ConditionalThe 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...>')
OptionalThe parameter value is not mandatory. The module provides a default value for the parameter.@description('Optional. <PARAMETER DESCRIPTION HERE...>')
GeneratedThe 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
#IDTitleSeverityPersonaLifecycle
1SNFR1Prescribed TestsMUSTOwnerContributorBAU
2SNFR2E2E TestingMUSTOwnerContributorBAU
3SNFR3AVM Compliance TestsMUSTOwnerContributorInitial
4SNFR4Unit TestsSHOULDOwnerContributorBAU
5SNFR5Upgrade TestsSHOULDOwnerContributorBAU
6SNFR6Static Analysis/Linting TestsMUSTOwnerContributorBAU
7SNFR7Idempotency TestsMUSTOwnerContributorBAU
8BCPNFR10Test Bicep File NamingMUSTOwnerContributorBAU
9BCPNFR11Test ToolingMUSTOwnerContributorBAU
10BCPNFR12Deployment Test NamingMUSTOwnerContributorBAU
11BCPNFR13Test file metadataMUSTOwnerContributorBAU
12BCPNFR16Post-deployment testsMUSTOwnerContributorBAU
13BCPRMNFR1Expected Test DirectoriesMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

ID: SNFR1 - Category: Testing - Prescribed Tests

Modules MUST use the prescribed tooling and testing frameworks defined in the language specific specs.




See origin...

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

To see a directory and file structure for a module, see the language specific contribution guide.

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.
➕ 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.

Details for use in E2E tests

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>).




See origin...

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/avm-core-team-technical-bicep, OR @Azure/avm-core-team-technical-terraform.




See origin...

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.




See origin...

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.




See origin...

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.




See origin...

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.




See origin...

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.




See origin...

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.



See origin...

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.




See origin...

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.



See origin...

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 e2e test’s folder or any subfolder (e.g., e2e/max/myTest.tests.ps1 or e2e/max/tests/myTest.tests.ps1)

  • Implement an input parameter TestInputData in the following way:

    param (
        [Parameter(Mandatory = $false)]
        [hashtable] $TestInputData = @{}
    )

    Through this parameter you can make use of every output the main.test.bicep file 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.resourceId defined in the main.test.bicep file, the $TestInputData would 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
            }
        }
    }



See origin...

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
#IDTitleSeverityPersonaLifecycle
1SNFR15Automatic Documentation GenerationMUSTOwnerContributorBAU
2SNFR16Examples/E2EMUSTOwnerContributorBAU
3BCPNFR2Module Documentation GenerationMUSTOwnerContributorBAU
4BCPNFR3Usage Example formatsMUSTOwnerContributorBAU
5BCPNFR4Parameter Input ExamplesMAYOwnerContributorBAU
➕ See Specifications for this category
See origin...

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.




See origin...

ID: SNFR16 - Category: Documentation - Examples/E2E

An examples/e2e directory MUST exist to provide named scenarios for module deployment.




See origin...

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



See origin...

ID: BCPNFR3 - Category: Documentation - Usage Example formats

Usage examples for Bicep modules MUST be provided in the following formats:

  • Bicep file (orchestration module style) - .bicep

    module <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.




See origin...

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 customerManagedKeyType

It 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
#IDTitleSeverityPersonaLifecycle
1SNFR17Semantic VersioningMUSTOwnerContributorBAU
2SNFR18Breaking ChangesSHOULDOwnerContributorBAU
3SNFR19Registries TargetedMUSTOwnerContributorBAU
4SNFR21Cross Language CollaborationSHOULDOwnerContributorBAU
5BCPNFR22Bicep Module ChangelogMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

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 Version
  • Y == Minor Version
  • Z == Patch Version

Module versioning before first Major version release 1.0.0

  • Initially modules MUST be released as version 0.1.0 and 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 version 1.0.0.

    Note

    Releasing as version 0.1.0 initially 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.0 is released, given a version number X.Y.Z:

    • X Major version MUST NOT be bumped.
    • Y Minor version MUST be bumped when introducing breaking changes (which would normally bump Major after 1.0.0 release) or feature updates (same as it will be after 1.0.0 release).
    • Z Patch version MUST be bumped when introducing non-breaking, backward compatible bug fixes (same as it will be after 1.0.0 release).



See origin...

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.




See origin...

ID: SNFR19 - Category: Publishing - Registries Targeted

Modules MUST be published to their respective language public registries.

Tip

See the language specific contribution guides for detailed guidance and sample code to use in AVM modules to achieve this requirement.




See origin...

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.




See origin...

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

- None

Each version’s entry:

  • MUST contain two sections: Changes and Breaking Changes. At least one of them must have a meaningful entry and sections must not be left empty. A - None may 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

- None

Each 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 ## Changes and ## Breaking Changes with 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 as tags for each module https://mcr.microsoft.com/v2/bicep/avm/(res|ptn|utl)/<namespace/modulename>/tags/list




Code Style

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1BCPNFR8Code Styling - lower camelCasingSHOULDOwnerContributorBAU
2BCPNFR17Code Styling - Type castingSHOULDOwnerContributorBAU
➕ See Specifications for this category
See origin...

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)




See origin...

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

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

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))
  }
}



Terraform Specifications

Specifications by Category and Module Classification

CategoryResourcePatternUtility
Contribution/Support988
Telemetry222
Naming/Composition20149
CodeStyle303030
Inputs/Outputs1298
Testing10109
Documentation444
Release/Publishing444
Summary918174

How to propose changes to the specifications?

Important

Any updates to existing or new specifications for Terraform must be submitted as a draft for review by Azure Terraform PG/Engineering(@Azure/terraform-avm) and AVM core team(@Azure/avm-core-team).

Why AVM Terraform modules favor AzAPI

AVM Terraform modules MUST use the AzAPI provider. The AzureRM provider is only permitted under the narrow exception described in TFFR3 (resources that have no AzAPI equivalent, e.g. some data-plane resources).

This decision is intentional and is driven by the following factors:

  • Built-in retries and error handling. AzAPI exposes first-class retry and timeouts blocks, including regex-based error matching, which lets modules handle transient failures (for example, scope locks being removed or eventual-consistency errors) deterministically and without external workarounds.
  • Pre-flight validation. AzAPI performs ARM API pre-flight checks at plan time, surfacing many configuration errors before an apply is attempted. This produces faster feedback loops and fewer partially-deployed resources.
  • Day-zero access to the latest Azure features. Because AzAPI talks directly to the Azure Resource Manager REST API, modules can adopt new resource types, properties and API versions as soon as they ship in Azure — without waiting for an AzureRM provider release.
  • Alignment with Bicep and ARM. AzAPI uses the same resource type identifiers (e.g. Microsoft.KeyVault/vaults@2023-07-01) and the same property shape as Bicep and ARM templates. This makes it dramatically easier to translate documentation, samples and Bicep modules into Terraform, and keeps Bicep and Terraform AVM modules conceptually aligned.
  • Close partnership with the Azure engineering teams. AzAPI is built and maintained in close collaboration with the Azure Resource Provider engineering teams. Issues in AzAPI can be triaged directly against the underlying ARM behavior, and the AVM team works directly with the AzAPI engineering team on roadmap and breaking changes.
  • Consistency across the AVM ecosystem. Standardizing on AzAPI means every AVM Terraform module uses the same patterns for identity, diagnostic settings, role assignments, locks and private endpoints — primarily through the Azure/avm-utl-interfaces/azure utility module — which simplifies authoring, review and consumer experience.

What changed recently?

See what specifications changed in the last 30 days...

#IDLast Modified (UTC)Git HistoryLast Commit
1TFRMNFR12026-05-29 10:34:42All Commits5807d20
2PMNFR12026-05-26 15:12:56All Commitsb7412ee
3RMNFR12026-05-26 15:12:56All Commitsb7412ee
4SNFR122026-05-26 15:12:56All Commitsb7412ee
5SNFR22026-05-26 15:12:56All Commitsb7412ee
6SNFR262026-05-26 15:12:56All Commitsb7412ee
7UMNFR12026-05-26 15:12:56All Commitsb7412ee
8TFRMFR12026-05-26 15:12:56All Commitsb7412ee
9TFRMNFR22026-05-26 15:12:56All Commitsb7412ee
10TFFR12026-05-26 15:12:56All Commitsb7412ee
11TFFR22026-05-26 15:12:56All Commitsb7412ee
12TFFR32026-05-26 15:12:56All Commitsb7412ee
13TFFR42026-05-26 15:12:56All Commitsb7412ee
14TFFR52026-05-26 15:12:56All Commitsb7412ee
15TFFR62026-05-26 15:12:56All Commitsb7412ee
16TFFR72026-05-26 15:12:56All Commitsb7412ee
17TFNFR112026-05-26 15:12:56All Commitsb7412ee
18TFNFR122026-05-26 15:12:56All Commitsb7412ee
19TFNFR172026-05-26 15:12:56All Commitsb7412ee
20TFNFR182026-05-26 15:12:56All Commitsb7412ee
21TFNFR212026-05-26 15:12:56All Commitsb7412ee
22TFNFR232026-05-26 15:12:56All Commitsb7412ee
23TFNFR252026-05-26 15:12:56All Commitsb7412ee
24TFNFR262026-05-26 15:12:56All Commitsb7412ee
25TFNFR272026-05-26 15:12:56All Commitsb7412ee
26TFNFR292026-05-26 15:12:56All Commitsb7412ee
27TFNFR342026-05-26 15:12:56All Commitsb7412ee
28TFNFR362026-05-26 15:12:56All Commitsb7412ee
29TFNFR382026-05-26 15:12:56All Commitsb7412ee
30TFNFR392026-05-26 15:12:56All Commitsb7412ee
31TFNFR52026-05-26 15:12:56All Commitsb7412ee
32TFNFR72026-05-26 15:12:56All Commitsb7412ee
33TFNFR82026-05-26 15:12:56All Commitsb7412ee

Subsections of Terraform

Terraform Interfaces

This chapter details the interfaces/schemas for the AVM Resource Modules features/extension resources as referenced in RMFR4 and RMFR5.

Diagnostic Settings

Important

Allowed values for logs and metric categories or category groups MUST NOT be specified to keep the module implementation evergreen for any new categories or category groups added by RPs, without module owners having to update a list of allowed values and cut a new release of their module.

  variable "diagnostic_settings" {
    type = map(object({
      name = optional(string, null)
      logs = optional(set(object({
        category       = optional(string, null)
        category_group = optional(string, null)
        enabled        = optional(bool, true)
        retention_policy = optional(object({
          days    = optional(number, 0)
          enabled = optional(bool, false)
        }), {})
      })), [])
      metrics = optional(set(object({
        category = optional(string, null)
        enabled  = optional(bool, true)
        retention_policy = optional(object({
          days    = optional(number, 0)
          enabled = optional(bool, false)
        }), {})
      })), [])
      log_analytics_destination_type           = optional(string, "Dedicated")
      workspace_resource_id                    = optional(string, null)
      storage_account_resource_id              = optional(string, null)
      event_hub_authorization_rule_resource_id = optional(string, null)
      event_hub_name                           = optional(string, null)
      marketplace_partner_resource_id          = optional(string, null)
    }))
    default  = {}
    nullable = false
  
    validation {
      condition     = alltrue([for _, v in var.diagnostic_settings : contains(["Dedicated", "AzureDiagnostics"], v.log_analytics_destination_type)])
      error_message = "Log analytics destination type must be one of: 'Dedicated', 'AzureDiagnostics'."
    }
    validation {
      condition = alltrue([
        for _, v in var.diagnostic_settings : alltrue([
          for l in v.logs : (l.category != null) != (l.category_group != null)
        ])
      ])
      error_message = "Each log entry must set exactly one of `category` or `category_group`."
    }
    validation {
      condition = alltrue(
        [
          for _, v in var.diagnostic_settings :
          v.workspace_resource_id != null || v.storage_account_resource_id != null || v.event_hub_authorization_rule_resource_id != null || v.marketplace_partner_resource_id != null
        ]
      )
      error_message = "At least one of `workspace_resource_id`, `storage_account_resource_id`, `marketplace_partner_resource_id`, or `event_hub_authorization_rule_resource_id`, must be set."
    }
    validation {
      condition = alltrue([
        for _, v in var.diagnostic_settings :
        v.workspace_resource_id == null || can(provider::azapi::parse_resource_id("Microsoft.OperationalInsights/workspaces", v.workspace_resource_id))
      ])
      error_message = "Each `workspace_resource_id` must be a valid Log Analytics workspace resource ID, or null."
    }
    validation {
      condition = alltrue([
        for _, v in var.diagnostic_settings :
        v.storage_account_resource_id == null || can(provider::azapi::parse_resource_id("Microsoft.Storage/storageAccounts", v.storage_account_resource_id))
      ])
      error_message = "Each `storage_account_resource_id` must be a valid storage account resource ID, or null."
    }
    validation {
      condition = alltrue([
        for _, v in var.diagnostic_settings :
        v.event_hub_authorization_rule_resource_id == null || can(provider::azapi::parse_resource_id("Microsoft.EventHub/namespaces/authorizationRules", v.event_hub_authorization_rule_resource_id))
      ])
      error_message = "Each `event_hub_authorization_rule_resource_id` must be a valid Event Hub namespace authorization rule resource ID, or null."
    }
    description = <<DESCRIPTION
  A map of diagnostic settings to create on the resource. The map key is deliberately arbitrary to avoid issues where map keys maybe unknown at plan time.
  
  - `name` - (Optional) The name of the diagnostic setting. One will be generated if not set, however this will not be unique if you want to create multiple diagnostic setting resources.
  - `logs` - (Optional) A set of log entries to send to the destination. Each entry has the following attributes:
    - `category` - (Optional) The name of a specific log category to enable. Mutually exclusive with `category_group`.
    - `category_group` - (Optional) The name of a log category group to enable (for example, `allLogs` or `audit`). Mutually exclusive with `category`.
    - `enabled` - (Optional) Whether the log entry is enabled. Defaults to `true`.
    - `retention_policy` - (Optional) The retention policy for the log entry.
      - `days` - (Optional) The retention period in days. Defaults to `0` (retain indefinitely).
      - `enabled` - (Optional) Whether the retention policy is enabled. Defaults to `false`.
  - `metrics` - (Optional) A set of metric entries to send to the destination. Each entry has the following attributes:
    - `category` - (Optional) The name of the metric category to enable.
    - `enabled` - (Optional) Whether the metric entry is enabled. Defaults to `true`.
    - `retention_policy` - (Optional) The retention policy for the metric entry, with the same `days` and `enabled` attributes as `logs.retention_policy`.
  - `log_analytics_destination_type` - (Optional) The destination type for the diagnostic setting. Possible values are `Dedicated` and `AzureDiagnostics`. Defaults to `Dedicated`.
  - `workspace_resource_id` - (Optional) The resource ID of the log analytics workspace to send logs and metrics to.
  - `storage_account_resource_id` - (Optional) The resource ID of the storage account to send logs and metrics to.
  - `event_hub_authorization_rule_resource_id` - (Optional) The resource ID of the event hub authorization rule to send logs and metrics to.
  - `event_hub_name` - (Optional) The name of the event hub. If none is specified, the default event hub will be selected.
  - `marketplace_partner_resource_id` - (Optional) The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs.
  DESCRIPTION
  }
  
  module "avm_interfaces" {
    source  = "Azure/avm-utl-interfaces/azure"
    version = "0.6.0" # check latest version at the time of use
  
    diagnostic_settings_v2    = var.diagnostic_settings
    diagnostic_settings_scope = azapi_resource.this.id
  }
  
  # Sample resource
  resource "azapi_resource" "diagnostic_settings" {
    for_each = module.avm_interfaces.diagnostic_settings_azapi_v2
  
    type      = each.value.type
    name      = each.value.name
    parent_id = each.value.parent_id
    body      = each.value.body
  }
  
  diagnostic_settings = {
    diag_setting_1 = {
      name = "diagSetting1"
      logs = [
        {
          category_group = "allLogs"
          enabled        = true
        }
      ]
      metrics = [
        {
          category = "AllMetrics"
          enabled  = true
        }
      ]
      log_analytics_destination_type           = "Dedicated"
      workspace_resource_id                    = "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}"
      storage_account_resource_id              = "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{storageAccountName}"
      event_hub_authorization_rule_resource_id = "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.EventHub/namespaces/{namespaceName}/eventhubs/{eventHubName}/authorizationrules/{authorizationRuleName}"
      event_hub_name                           = "{eventHubName}"
      marketplace_partner_resource_id          = "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{partnerResourceProvider}/{partnerResourceType}/{partnerResourceName}"
    }
  }
  
Note

In the provided example for Diagnostic Settings, both logs and metrics are enabled for the associated resource. However, it is IMPORTANT to note that certain resources may not support both diagnostic setting types/categories. In such cases, the resource configuration MUST be modified accordingly to ensure proper functionality and compliance with system requirements.

Role Assignments

  variable "role_assignments" {
    type = map(object({
      name                                   = optional(string, null)
      role_definition_id_or_name             = string
      principal_id                           = string
      description                            = optional(string, null)
      skip_service_principal_aad_check       = optional(bool, false)
      condition                              = optional(string, null)
      condition_version                      = optional(string, null)
      delegated_managed_identity_resource_id = optional(string, null)
      principal_type                         = optional(string, null)
    }))
    default     = {}
    nullable    = false
    description = <<DESCRIPTION
  A map of role assignments to create on the <RESOURCE>. The map key is deliberately arbitrary to avoid issues where map keys maybe unknown at plan time.
  
  - `name` - (Optional) The name of the role assignment. If not set, a random UUID will be generated. Changing this forces the creation of a new resource.
  - `role_definition_id_or_name` - The ID or name of the role definition to assign to the principal.
  - `principal_id` - The ID of the principal to assign the role to.
  - `description` - (Optional) The description of the role assignment.
  - `skip_service_principal_aad_check` - (Optional) If set to true, skips the Azure Active Directory check for the service principal in the tenant. Defaults to false.
  - `condition` - (Optional) The condition which will be used to scope the role assignment.
  - `condition_version` - (Optional) The version of the condition syntax. Leave as `null` if you are not using a condition, if you are then valid values are '2.0'.
  - `delegated_managed_identity_resource_id` - (Optional) The delegated Azure Resource Id which contains a Managed Identity. Changing this forces a new resource to be created. This field is only used in cross-tenant scenario.
  - `principal_type` - (Optional) The type of the `principal_id`. Possible values are `User`, `Group` and `ServicePrincipal`. It is necessary to explicitly set this attribute when creating role assignments if the principal creating the assignment is constrained by ABAC rules that filters on the PrincipalType attribute.
  
  > Note: only set `skip_service_principal_aad_check` to true if you are assigning a role to a service principal.
  DESCRIPTION
  
    validation {
      condition = alltrue([
        for _, v in var.role_assignments :
        v.delegated_managed_identity_resource_id == null || can(provider::azapi::parse_resource_id("Microsoft.ManagedIdentity/userAssignedIdentities", v.delegated_managed_identity_resource_id))
      ])
      error_message = "Each `role_assignments[*].delegated_managed_identity_resource_id` must be a valid user-assigned managed identity resource ID, or null."
    }
  }
  
  module "avm_interfaces" {
    source  = "Azure/avm-utl-interfaces/azure"
    version = "0.6.0" # check latest version at the time of use
  
    role_assignments                 = var.role_assignments
    role_assignment_definition_scope = azapi_resource.this.id
  }
  
  # Example resource declaration
  resource "azapi_resource" "role_assignments" {
    for_each = module.avm_interfaces.role_assignments_azapi
  
    type      = each.value.type
    name      = each.value.name
    parent_id = each.value.parent_id
    body      = each.value.body
    retry = {
      error_message_regex  = ["ScopeLocked"] # retry if a lock is in place on the scope and has only just been removed
      interval_seconds     = 15
      max_interval_seconds = 60
    }
  
    timeouts {
      delete = "5m"
    }
  }
  
  role_assignments = {
    role_assignment_1 = {
      role_definition_id_or_name             = "Contributor"
      principal_id                           = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
      skip_service_principal_aad_check       = true
    },
    role_assignment_2 = {
      role_definition_id_or_name             = "Storage Blob Data Reader"
      principal_id                           = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
      description                            = "Example role assignment 2 of reader role"
      skip_service_principal_aad_check       = false
      condition                              = "@Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase 'foo_storage_container'"
      condition_version                      = "2.0"
    }
  }
  

Details on child, extension and cross-referenced resources:

  • Modules MUST support Role Assignments on child, extension and cross-referenced resources as well as the primary resource via parameters/variables

Resource Locks

  variable "lock" {
    type = object({
      kind = string
      name = optional(string, null)
    })
    default     = null
    description = <<DESCRIPTION
  Controls the Resource Lock configuration for this resource. The following properties can be specified:
  
  - `kind` - (Required) The type of lock. Possible values are `\"CanNotDelete\"` and `\"ReadOnly\"`.
  - `name` - (Optional) The name of the lock. If not specified, a name will be generated based on the `kind` value. Changing this forces the creation of a new resource.
  DESCRIPTION
  
    validation {
      condition     = var.lock != null ? contains(["CanNotDelete", "ReadOnly"], var.lock.kind) : true
      error_message = "Lock kind must be either `\"CanNotDelete\"` or `\"ReadOnly\"`."
    }
  }
  
  module "avm_interfaces" {
    source  = "Azure/avm-utl-interfaces/azure"
    version = "0.6.0" # check latest version at the time of use
  
    lock       = var.lock
    lock_scope = azapi_resource.this.id
  }
  
  # Example resource implementation
  resource "azapi_resource" "lock" {
    count = var.lock != null ? 1 : 0
  
    type      = module.avm_interfaces.lock_azapi.type
    name      = module.avm_interfaces.lock_azapi.name
    parent_id = module.avm_interfaces.lock_azapi.parent_id
    body      = module.avm_interfaces.lock_azapi.body
  }
  
  lock = {
    name = "lock-{resourcename}" # optional
    type = "CanNotDelete"
  }
  

Details on child and extension resources:

  • Locks SHOULD be able to be set for child resources of the primary resource in resource modules

Details on cross-referenced resources:

  • Locks MUST be automatically applied to cross-referenced resources if the primary resource has a lock applied.
    • This MUST also be able to be turned off for each of the cross-referenced resources by the module consumer via a parameter/variable if they desire

An example of this is a Key Vault module that has a Private Endpoints enabled. If a lock is applied to the Key Vault via the lock parameter/variable then the lock should also be applied to the Private Endpoint automatically, unless the privateEndpointLock/private_endpoint_lock (example name) parameter/variable is set to None

Important

In Terraform, locks become part of the resource graph and suitable depends_on values should be set. Note that, during a destroy operation, Terraform will remove the locks before removing the resource itself, reducing the usefulness of the lock somewhat. Also note, due to eventual consistency in Azure, use of locks can cause destroy operations to fail as the lock may not have been fully removed by the time the destroy operation is executed.

Tags

  variable "tags" {
    type     = map(string)
    default  = null
    description = "(Optional) Tags of the resource."
  }
  
  tags = {
    key           = "value"
    "another-key" = "another-value"
    integers      = 123
  }
  

Details on child, extension and cross-referenced resources:

  • Tags MUST be automatically applied to child, extension and cross-referenced resources, if tags are applied to the primary resource.
    • By default, all tags set for the primary resource will automatically be passed down to child, extension and cross-referenced resources.
    • This MUST be able to be overridden by the module consumer so they can specify alternate tags for child, extension and cross-referenced resources, if they desire via a parameter/variable
      • If overridden by the module consumer, no merge/union of tags will take place from the primary resource and only the tags specified for the child, extension and cross-referenced resources will be applied

Managed Identities

  variable "managed_identities" {
    type = object({
      system_assigned            = optional(bool, false)
      user_assigned_resource_ids = optional(set(string), [])
    })
    default     = {}
    nullable    = false
    description = <<DESCRIPTION
  Controls the Managed Identity configuration on this resource. The following properties can be specified:
  
  - `system_assigned` - (Optional) Specifies if the System Assigned Managed Identity should be enabled.
  - `user_assigned_resource_ids` - (Optional) Specifies a list of User Assigned Managed Identity resource IDs to be assigned to this resource.
  DESCRIPTION
  
    validation {
      condition = alltrue([
        for id in var.managed_identities.user_assigned_resource_ids :
        can(provider::azapi::parse_resource_id("Microsoft.ManagedIdentity/userAssignedIdentities", id))
      ])
      error_message = "Each entry in `managed_identities.user_assigned_resource_ids` must be a valid user-assigned managed identity resource ID."
    }
  }
  
  module "avm_interfaces" {
    source  = "Azure/avm-utl-interfaces/azure"
    version = "0.6.0" # check latest version at the time of use
  
    managed_identities = var.managed_identities
  }
  
  # Example identity block on the parent azapi_resource. The avm_interfaces
  # module returns a single object with the correct `type` and `identity_ids`
  # values, including the case when no identity is configured (in which case
  # the for_each is empty and no identity block is rendered).
  #
  # Note: AzAPI accepts a single `identity` block. The dynamic block below
  # renders zero or one block depending on whether a managed identity is
  # configured. The same pattern works for resources that only support
  # SystemAssigned or only UserAssigned identities.
  resource "azapi_resource" "this" {
    # ...other arguments...
  
    dynamic "identity" {
      for_each = module.avm_interfaces.managed_identities_azapi != null ? [module.avm_interfaces.managed_identities_azapi] : []
      content {
        type         = identity.value.type
        identity_ids = identity.value.identity_ids
      }
    }
  }
  
  managed_identities = {
    system_assigned = true
    user_assigned_resource_ids = [
      "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}",
      "/subscriptions/{subscriptionId2}/resourceGroups/{resourceGroupName2}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName2}"
    ]
  }
  

Reason for differences in User Assigned data type in languages:

  • We do not forsee the Managed Identity Resource Provider team to ever add additional properties within the empty object ({}) value required on the input of a User Assigned Managed Identity.
  • In Bicep we therefore have removed the need for this to be declared and just converted it to a simple array of Resource IDs
  • However, in Terraform we have left it as a object/map as this simplifies for_each and other loop mechanisms and provides more consistency in plan, apply, destroy operations
    • Especially when adding, removing or changing the order of the User Assigned Managed Identities as they are declared

Private Endpoints

  # In this example we only support one service, e.g. Key Vault.
  # If your service has multiple private endpoint services, then expose the service name.
  
  variable "private_endpoints_manage_dns_zone_group" {
    type        = bool
    default     = true
    nullable    = false
    description = "Whether to manage private DNS zone groups with this module. If set to false, you must manage private DNS zone groups externally, e.g. using Azure Policy."
  }
  
  variable "private_endpoints" {
    type = map(object({
      name = optional(string, null)
      role_assignments = optional(map(object({
        name                                   = optional(string, null)
        role_definition_id_or_name             = string
        principal_id                           = string
        description                            = optional(string, null)
        skip_service_principal_aad_check       = optional(bool, false)
        condition                              = optional(string, null)
        condition_version                      = optional(string, null)
        delegated_managed_identity_resource_id = optional(string, null)
        principal_type                         = optional(string, null)
      })), {})
      lock = optional(object({
        kind = string
        name = optional(string, null)
      }), null)
      tags                                    = optional(map(string), null)
      subnet_resource_id                      = string
      subresource_name                        = optional(string, null) # only required if the parent resource exposes more than one private endpoint sub-resource
      private_dns_zone_group_name             = optional(string, "default")
      private_dns_zone_resource_ids           = optional(set(string), [])
      application_security_group_associations = optional(map(string), {})
      private_service_connection_name         = optional(string, null)
      network_interface_name                  = optional(string, null)
      location                                = optional(string, null)
      resource_group_name                     = optional(string, null)
      ip_configurations = optional(map(object({
        name               = string
        private_ip_address = string
        member_name        = optional(string)
      })), {})
    }))
    default     = {}
    nullable    = false
    description = <<DESCRIPTION
  A map of private endpoints to create on the Key Vault. The map key is deliberately arbitrary to avoid issues where map keys maybe unknown at plan time.
  
  - `name` - (Optional) The name of the private endpoint. One will be generated if not set.
  - `role_assignments` - (Optional) A map of role assignments to create on the private endpoint. The map key is deliberately arbitrary to avoid issues where map keys maybe unknown at plan time. See `var.role_assignments` for more information.
    - `name` - (Optional) The name of the role assignment. If not set, a random UUID will be generated. Changing this forces the creation of a new resource.
    - `role_definition_id_or_name` - The ID or name of the role definition to assign to the principal.
    - `principal_id` - The ID of the principal to assign the role to.
    - `description` - (Optional) The description of the role assignment.
    - `skip_service_principal_aad_check` - (Optional) If set to true, skips the Azure Active Directory check for the service principal in the tenant. Defaults to false.
    - `condition` - (Optional) The condition which will be used to scope the role assignment.
    - `condition_version` - (Optional) The version of the condition syntax. Leave as `null` if you are not using a condition, if you are then valid values are '2.0'.
    - `delegated_managed_identity_resource_id` - (Optional) The delegated Azure Resource Id which contains a Managed Identity. Changing this forces a new resource to be created. This field is only used in cross-tenant scenario.
    - `principal_type` - (Optional) The type of the `principal_id`. Possible values are `User`, `Group` and `ServicePrincipal`. It is necessary to explicitly set this attribute when creating role assignments if the principal creating the assignment is constrained by ABAC rules that filters on the PrincipalType attribute.
  - `lock` - (Optional) The lock level to apply to the private endpoint. Default is `None`. Possible values are `None`, `CanNotDelete`, and `ReadOnly`.
    - `kind` - (Required) The type of lock. Possible values are `\"CanNotDelete\"` and `\"ReadOnly\"`.
    - `name` - (Optional) The name of the lock. If not specified, a name will be generated based on the `kind` value. Changing this forces the creation of a new resource.
  - `tags` - (Optional) A mapping of tags to assign to the private endpoint.
  - `subnet_resource_id` - The resource ID of the subnet to deploy the private endpoint in.
  - `subresource_name` (Optional) - The name of the sub resource for the private endpoint.
  - `private_dns_zone_group_name` - (Optional) The name of the private DNS zone group. One will be generated if not set.
  - `private_dns_zone_resource_ids` - (Optional) A set of resource IDs of private DNS zones to associate with the private endpoint. If not set, no zone groups will be created and the private endpoint will not be associated with any private DNS zones. DNS records must be managed external to this module.
  - `application_security_group_resource_ids` - (Optional) A map of resource IDs of application security groups to associate with the private endpoint. The map key is deliberately arbitrary to avoid issues where map keys maybe unknown at plan time.
  - `private_service_connection_name` - (Optional) The name of the private service connection. One will be generated if not set.
  - `network_interface_name` - (Optional) The name of the network interface. One will be generated if not set.
  - `location` - (Optional) The Azure location where the resources will be deployed. Defaults to the location of the resource group.
  - `resource_group_name` - (Optional) The resource group resource ID where the private endpoint resources will be deployed. Defaults to the resource group of the parent resource.
  - `ip_configurations` - (Optional) A map of IP configurations to create on the private endpoint. If not specified the platform will create one. The map key is deliberately arbitrary to avoid issues where map keys maybe unknown at plan time.
    - `name` - The name of the IP configuration.
    - `private_ip_address` - The private IP address of the IP configuration.
    - `member_name` - (Optional) The private IP configuration member name.
  DESCRIPTION
  
    validation {
      condition = alltrue([
        for _, v in var.private_endpoints :
        can(provider::azapi::parse_resource_id("Microsoft.Network/virtualNetworks/subnets", v.subnet_resource_id))
      ])
      error_message = "Each `private_endpoints[*].subnet_resource_id` must be a valid subnet resource ID."
    }
    validation {
      condition = alltrue(flatten([
        for _, v in var.private_endpoints : [
          for id in v.private_dns_zone_resource_ids :
          can(provider::azapi::parse_resource_id("Microsoft.Network/privateDnsZones", id))
        ]
      ]))
      error_message = "Each entry in `private_endpoints[*].private_dns_zone_resource_ids` must be a valid private DNS zone resource ID."
    }
    validation {
      condition = alltrue(flatten([
        for _, v in var.private_endpoints : [
          for _, asg in v.application_security_group_associations :
          can(provider::azapi::parse_resource_id("Microsoft.Network/applicationSecurityGroups", asg))
        ]
      ]))
      error_message = "Each value in `private_endpoints[*].application_security_group_associations` must be a valid application security group resource ID."
    }
    validation {
      condition = alltrue(flatten([
        for _, v in var.private_endpoints : [
          for _, ra in v.role_assignments :
          ra.delegated_managed_identity_resource_id == null || can(provider::azapi::parse_resource_id("Microsoft.ManagedIdentity/userAssignedIdentities", ra.delegated_managed_identity_resource_id))
        ]
      ]))
      error_message = "Each `private_endpoints[*].role_assignments[*].delegated_managed_identity_resource_id` must be a valid user-assigned managed identity resource ID, or null."
    }
  }
  
  module "avm_interfaces" {
    source  = "Azure/avm-utl-interfaces/azure"
    version = "0.6.0" # check latest version at the time of use
  
    private_endpoints                = var.private_endpoints
    private_endpoints_scope          = azapi_resource.this.id
    role_assignment_definition_scope = azapi_resource.this.id
  }
  
  resource "azapi_resource" "private_endpoints" {
    for_each = module.avm_interfaces.private_endpoints_azapi
  
    location  = azapi_resource.this.location
    name      = each.value.name
    parent_id = coalesce(var.private_endpoints[each.key].resource_group_name, azapi_resource.this.parent_id)
    type      = each.value.type
    body      = each.value.body
    retry = {
      error_message_regex = ["ScopeLocked"] # This will retry if a lock is in place on the resource group, and has only just been removed
    }
  
    timeouts {
      delete = "5m"
    }
  }
  
  resource "azapi_resource" "private_endpoint_locks" {
    for_each = module.avm_interfaces.lock_private_endpoint_azapi
  
    name      = each.value.name
    parent_id = azapi_resource.private_endpoints[each.value.pe_key].id
    type      = each.value.type
    body      = each.value.body
  
    depends_on = [
      azapi_resource.private_dns_zone_groups,
      azapi_resource.private_endpoint_role_assignments
    ]
  }
  
  resource "azapi_resource" "private_dns_zone_groups" {
    for_each = module.avm_interfaces.private_dns_zone_groups_azapi
  
    name      = each.value.name
    parent_id = azapi_resource.private_endpoints[each.key].id
    type      = each.value.type
    body      = each.value.body
    retry = {
      error_message_regex  = ["ScopeLocked"] # This will retry if a lock is in place on the resource group, and has only just been removed
      interval_seconds     = 15
      max_interval_seconds = 60
    }
  
    timeouts {
      delete = "5m"
    }
  }
  
  resource "azapi_resource" "private_endpoint_role_assignments" {
    for_each = module.avm_interfaces.role_assignments_private_endpoint_azapi
  
    name      = each.value.name
    parent_id = azapi_resource.private_endpoints[each.value.pe_key].id
    type      = each.value.type
    body      = each.value.body
    retry = {
      error_message_regex  = ["ScopeLocked"]
      interval_seconds     = 15
      max_interval_seconds = 60
    }
  
    timeouts {
      delete = "5m"
    }
  }
  
  private_endpoints = {
    pe1 = {
      role_assignments   = {} # see interfaces/role assignments
      lock               = {} # see interfaces/resource locks
      tags               = {} # see interfaces/tags
      subnet_resource_id = "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/virtualNetworks/{vnetName}/subnets/{subnetName}"
      private_dns_zone_resource_ids = [
        "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/privateDnsZones/{dnsZoneName}"
      ]
      application_security_group_associations = {
        asg1 = "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/applicationSecurityGroups/{asgName}"
      }
      network_interface_name = "nic1"
      ip_configurations = {
        ipconfig1 = {
          name               = "ipconfig1"
          group_id           = "vault"
          member_name        = "default"
          private_ip_address = "10.0.0.7"
        }
      }
    }
  }
  

Notes:

  • The properties defined in the schema above are the minimum amount of properties expected to be exposed for Private Endpoints in AVM Resource Modules.
    • A module owner MAY chose to expose additional properties of the Private Endpoint resource.
      • However, module owners considering this SHOULD contact the AVM core team first to consult on how the property should be exposed to avoid future breaking changes to the schema that may be enforced upon them.
  • Module owners MAY chose to define a list of allowed value for the ‘service’ (a.k.a. groupIds) property.
    • However, they should do so with caution as should a new service appear for their resource module, a new release will need to be cut to add this new service to the allowed values.
      • Whereas not specifying allowed values will allow flexibility from day 0 without the need for any changes and releases to be made.

Customer Managed Keys

  variable "customer_managed_key" {
    type = object({
      key_vault_resource_id  = string
      key_name               = string
      key_version            = optional(string, null)
      user_assigned_identity = optional(object({
        resource_id = string
      }), null)
    })
    default = null
  
    validation {
      condition     = var.customer_managed_key == null || can(provider::azapi::parse_resource_id("Microsoft.KeyVault/vaults", var.customer_managed_key.key_vault_resource_id))
      error_message = "`customer_managed_key.key_vault_resource_id` must be a valid Azure Key Vault resource ID."
    }
    validation {
      condition     = var.customer_managed_key == null || var.customer_managed_key.user_assigned_identity == null || can(provider::azapi::parse_resource_id("Microsoft.ManagedIdentity/userAssignedIdentities", var.customer_managed_key.user_assigned_identity.resource_id))
      error_message = "`customer_managed_key.user_assigned_identity.resource_id` must be a valid user-assigned managed identity resource ID."
    }
  }
  
  customer_managed_key = {
    key_vault_resource_id: "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.KeyVault/vaults/{keyVaultName}"
    key_name: "{keyName}"
    key_version: "{keyVersion}"
    user_assigned_identity_resource_id: "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{uamiName}"
  }
  

Azure Monitor Alerts

Note

This interface is a SHOULD instead of a MUST and therefore the AVM core team have not mandated a interface schema to use.

AzAPI resource types

Important

The keys of the resource_types object are module-specific. Each module MUST declare one optional(string, "...") field per azapi_resource (or equivalent AzAPI resource) it owns, defaulting each field to the latest tested API version. See TFFR6.

  # The `resource_types` variable is module-specific: the field set below is
  # illustrative. Each module **MUST** declare one `optional(string, "...")`
  # field per `azapi_resource` (or equivalent AzAPI resource) it declares,
  # defaulting each field to the latest tested API version. Submodules
  # **MUST** follow the same pattern for the resources they own. The field
  # names **MUST** describe the resource being managed - never use generic
  # names like `this` or `resource`.
  variable "resource_types" {
    type = object({
      widget          = optional(string, "Microsoft.Example/widgets@2024-01-01")
      widget_setting  = optional(string, "Microsoft.Example/widgets/settings@2024-01-01")
      lock            = optional(string, "Microsoft.Authorization/locks@2020-05-01")
    })
    default     = {}
    nullable    = false
    description = <<DESCRIPTION
  Override the AzAPI `<provider>/<resource>@<api-version>` strings used by this module. Each key defaults to a tested value; supply only the keys you want to override. Useful when targeting a sovereign cloud with older API versions, or when opting into a newer preview API.
  
  The keys below are specific to this example module (which manages widgets). Each module names its keys after the resources it actually owns - for example, a Cosmos DB module would expose keys such as `database_account`, `sql_database`, and `sql_container`.
  
  - `widget`         - The primary widget managed by this module.
  - `widget_setting` - Settings attached to the widget.
  - `lock`           - Management lock applied to the widget and its private endpoints.
  DESCRIPTION
  }
  
  # Example resource implementation. The `type` of every `azapi_resource`
  # **MUST** come from `var.resource_types`, never a hard-coded string.
  resource "azapi_resource" "widget" {
    type      = var.resource_types.widget
    name      = var.name
    parent_id = var.parent_id
    body      = { /* ... */ }
  
    response_export_values = []
  }
  
  # Cascade the relevant subset of `resource_types` to every submodule the
  # parent module instantiates so that a single override at the parent level
  # propagates to every resource the module manages. The submodule names its
  # primary resource `setting` (because that is what it manages), while this
  # parent module exposes the same resource as `widget_setting`.
  module "setting" {
    source = "./modules/widget-setting"
  
    resource_types = {
      setting = var.resource_types.widget_setting
    }
  
    # ...other arguments...
  }
  
  resource_types = {
    # Pin the primary widget to a newer preview API version.
    widget = "Microsoft.Example/widgets@2025-06-01-preview"
  
    # Use an older API version that is available in a sovereign cloud.
    widget_setting = "Microsoft.Example/widgets/settings@2023-01-01"
  }
  

Notes:

  • Parent modules MUST cascade the relevant subset of resource_types to each submodule they instantiate (see TFRMNFR1). Submodules MUST declare their own resource_types variable using the same pattern.
  • Defaults MUST be a stable (non-preview) API version unless the module’s primary resource only ships a preview API.

AzAPI retry

  variable "retry" {
    type = object({
      error_message_regex  = optional(list(string))
      interval_seconds     = optional(number)
      max_interval_seconds = optional(number)
    })
    default     = null
    description = <<DESCRIPTION
  Retry configuration applied to every `azapi` resource managed by the module (root resource and all submodules). Defaults to `null` (no custom retry).
  
  - `error_message_regex`  - (Optional) A list of regex patterns matching error messages that trigger a retry.
  - `interval_seconds`     - (Optional) Initial interval between retries in seconds.
  - `max_interval_seconds` - (Optional) Maximum interval between retries in seconds.
  
  See <https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/resource#retry> for full semantics.
  DESCRIPTION
  }
  
  # Example resource implementation. `retry` is an attribute on `azapi_resource`,
  # so the variable is assigned directly. The same pattern applies to every
  # `azapi_resource` declared by the module, including those in submodules.
  resource "azapi_resource" "this" {
    type      = var.resource_types.this
    name      = var.name
    parent_id = var.parent_id
    body      = { /* ... */ }
  
    retry = var.retry
  
    response_export_values = []
  }
  
  # Cascade `retry` to every submodule the parent module instantiates so that a
  # single override at the parent level propagates everywhere.
  module "child" {
    source = "./modules/child"
  
    retry = var.retry
  
    # ...other arguments...
  }
  
  retry = {
    error_message_regex  = ["ScopeLocked", "RetryableError"]
    interval_seconds     = 15
    max_interval_seconds = 60
  }
  

Notes:

  • The retry variable MUST be applied to every azapi_resource (and equivalent AzAPI resources) declared by the module.
  • Parent modules MUST cascade retry to each submodule they instantiate (see TFFR7 and TFRMNFR1).
  • Module owners MAY ship module-level defaults when the resource it manages benefits from them. To do so, set the variable’s overall default to {} (not null) and provide per-field defaults inside the optional(...) wrappers. Consumers MUST still be able to override any individual field.
  # Module-level defaults example: a hypothetical module retries
  # on common transient replication errors and tunes the back-off interval. The
  # overall variable default is `{}` (not `null`) so the per-field defaults take
  # effect, and consumers can still override any individual field.
  variable "retry" {
    type = object({
      error_message_regex  = optional(list(string), ["AnotherOperationInProgress", "TooManyRequests"])
      interval_seconds     = optional(number, 30)
      max_interval_seconds = optional(number, 300)
    })
    default     = {}
    description = <<DESCRIPTION
  Retry configuration applied to every `azapi` resource managed by the module. This module ships defaults tuned for Storage Account replication; consumers **MAY** override any field.
  
  - `error_message_regex`  - (Optional) A list of regex patterns matching error messages that trigger a retry.
  - `interval_seconds`     - (Optional) Initial interval between retries in seconds.
  - `max_interval_seconds` - (Optional) Maximum interval between retries in seconds.
  DESCRIPTION
  }
  

AzAPI timeouts

  variable "timeouts" {
    type = object({
      create = optional(string)
      read   = optional(string)
      update = optional(string)
      delete = optional(string)
    })
    default     = null
    description = <<DESCRIPTION
  Default per-operation timeouts applied to every `azapi` resource managed by the module. Defaults to `null` (provider defaults). Each value is a Go duration string (e.g. `30m`, `1h`).
  
  - `create` - (Optional) Timeout for create operations.
  - `read`   - (Optional) Timeout for read operations.
  - `update` - (Optional) Timeout for update operations.
  - `delete` - (Optional) Timeout for delete operations.
  DESCRIPTION
  }
  
  # Example resource implementation. `timeouts` is a block on `azapi_resource`,
  # so a `dynamic "timeouts"` block is required to honour the variable's `null`
  # default. The same pattern applies to every `azapi_resource` declared by
  # the module, including those in submodules.
  resource "azapi_resource" "this" {
    type      = var.resource_types.this
    name      = var.name
    parent_id = var.parent_id
    body      = { /* ... */ }
  
    dynamic "timeouts" {
      for_each = var.timeouts == null ? [] : [var.timeouts]
      content {
        create = timeouts.value.create
        read   = timeouts.value.read
        update = timeouts.value.update
        delete = timeouts.value.delete
      }
    }
  
    response_export_values = []
  }
  
  # Cascade `timeouts` to every submodule the parent module instantiates so that
  # a single override at the parent level propagates everywhere.
  module "child" {
    source = "./modules/child"
  
    timeouts = var.timeouts
  
    # ...other arguments...
  }
  
  timeouts = {
    create = "30m"
    read   = "5m"
    update = "30m"
    delete = "1h"
  }
  

Notes:

  • timeouts is a block on azapi_resource (not an attribute), so a dynamic "timeouts" block is required to honor the variable’s null default.
  • The timeouts variable MUST be applied to every azapi_resource (and equivalent AzAPI resources) declared by the module.
  • Parent modules MUST cascade timeouts to each submodule they instantiate (see TFFR7 and TFRMNFR1). Submodules MAY additionally expose per-item overrides for cases where individual resources need different settings.
  • Module owners MAY ship module-level defaults when the resource it manages benefits from them (for example, longer create / delete timeouts for slow-provisioning resources). To do so, set the variable’s overall default to {} (not null) and provide per-field defaults inside the optional(...) wrappers. Consumers MUST still be able to override any individual field.
  # Module-level defaults example: a hypothetical SQL Database module ships
  # longer create / delete timeouts because provisioning and dropping large
  # databases can exceed the provider defaults. The overall variable default
  # is `{}` (not `null`) so the per-field defaults take effect, and consumers
  # can still override any individual field.
  variable "timeouts" {
    type = object({
      create = optional(string, "1h")
      read   = optional(string, "5m")
      update = optional(string, "1h")
      delete = optional(string, "45m")
    })
    default     = {}
    description = <<DESCRIPTION
  Default per-operation timeouts applied to every `azapi` resource managed by the module. This module ships defaults tuned for SQL Database provisioning latency; consumers **MAY** override any field.
  
  - `create` - (Optional) Timeout for create operations.
  - `read`   - (Optional) Timeout for read operations.
  - `update` - (Optional) Timeout for update operations.
  - `delete` - (Optional) Timeout for delete operations.
  DESCRIPTION
  }
  

Terraform Pattern Module Specifications

Contribution / Support

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR8Module Owner(s) GitHubMUSTOwnerInitial
2SNFR20GitHub Teams OnlyMUSTOwnerInitial
3SNFR9AVM & PG Teams GitHub Repo PermissionsMUSTOwnerInitial
4SNFR10MIT LicensingMUSTOwnerInitial
5SNFR11Issues Response TimesMUSTOwnerContributorBAU
6SNFR12Versions SupportedMUSTOwnerBAU
7SNFR23GitHub Repo LabelsMUSTOwnerBAU
8TFNFR3GitHub Repo Branch ProtectionMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

ID: SNFR8 - Category: Contribution/Support - Module Owner(s) GitHub

A module MUST have an owner that is defined and managed by a GitHub Team in the Azure GitHub organization.

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

The names for the GitHub teams for each approved module are already defined in the respective Module Indexes. These teams MUST be created (and used) for each module.




See origin...

ID: SNFR20 - Category: Contribution/Support - GitHub Teams Only

All GitHub repositories that AVM module are published from and hosted within MUST only assign GitHub repository permissions to GitHub teams only.

Each module MUST have a GitHub team assigned for module owners. This team MUST be created in the Azure organization in GitHub.

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

Important

As part of the module proposal process, the name of the GitHub team for each approved module is already defined in the respective Module Indexes (or CSV file). This team MUST be created (and used) for each module.

Module owners don’t need to construct the name of the GitHub team for their module themselves, instead they need use the name prescribed in the related CSV file, at the time of approval.

For a direct link, see the list of related index pages:

The @Azure prefix in the last column of the tables linked above represents the “Azure” GitHub organization all AVM-related repositories exist in. DO NOT include this segment in the team’s name!

Naming Convention

The naming convention for the GitHub teams MUST follow the below pattern:

  • <hyphenated module name>-module-owners-bicep - to grant permissions for module owners on Bicep modules

Segments:

  • <hyphenated module name> == the AVM Module’s name, with each segment separated by dashes, i.e., avm-res-<resource provider>-<ARM resource type>
    • See RMNFR1 for AVM Resource Module Naming
    • See PMNFR1 for AVM Pattern Module Naming
  • module-owners == the role the GitHub Team is assigned to
  • <bicep == the language the module is written in

Examples:

  • avm-res-compute-virtualmachine-module-owners-bicep
Note

The naming convention for Bicep modules is slightly different than the naming convention for their respective GitHub teams.

Add Team Members

All officially documented module owner(s) MUST be added to the -module-owners- team. The -module-owners- team MUST NOT have any other members.

Unless explicitly requested and agreed, members of the AVM core team or any PG teams MUST NOT be added to the -module-owners- teams as permissions for them are granted through the teams described in SNFR9.

Grant permissions through team memberships

Note

In case of Bicep modules, permissions to the BRM repository (the repo of the Bicep Registry) are granted via assigning the -module-owners- teams to parent teams that already have the required level access configured. While it is the module owner’s responsibility to initiate the addition of their team to the respective parent, only the AVM core team can approve this parent-child relationship.

Module owners MUST create their -module-owners- team and as part of the provisioning process, they MUST request the addition of this team to its respective parent team (see the table below for details).

GitHub Team NameDescriptionPermissionsPermissions granted throughWhere to work?
<hyphenated module name>-module-owners-bicepAVM Bicep Module Owners - <module name>WriteAssignment to the avm-technical-reviewers-bicep parent team.Need to work in a fork.

Example - GitHub team required for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

  • avm-res-network-virtualnetwork-module-owners-bicep –> assign to the avm-technical-reviewers-bicep parent team.
Tip

Direct link to create a new GitHub team and assign it to its parent: Create new team

Fill in the values as follows:

  • Team name: Following the naming convention described above, use the value defined in the module indexes.
  • Description: Follow the guidance above (see the Description column in the table above).
  • Parent team: Follow the guidance above (see the Permissions granted through column in the table above).
  • Team visibility: Visible
  • Team notifications: Enabled

CODEOWNERS file

As part of the “initial Pull Request” (that publishes the first version of the module), module owners MUST add an entry to the CODEOWNERS file in the BRM repository (here).

Note

Through this approach, the AVM core team will grant review permission to module owners as part of the standard PR review process.

Every CODEOWNERS entry (line) MUST include the following segments separated by a single whitespace character:

  • Path of the module, relative to the repo’s root, e.g.: /avm/res/network/virtual-network/
  • The -module-owners-team, with the @Azure/ prefix, e.g., @Azure/avm-res-network-virtualnetwork-module-owners-bicep
  • The GitHub team of the AVM Bicep reviewers, with the @Azure/ prefix, i.e., @Azure/avm-module-reviewers-bicep

Example - CODEOWNERS entry for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

  • /avm/res/network/virtual-network/ @Azure/avm-res-network-virtualnetwork-module-owners-bicep @Azure/avm-module-reviewers-bicep

Terraform

Note

Access management for Terraform repositories now uses a single team, membership of which is managed using an internal entitlement management tool (Core Identity).

All module owners MUST request access to the avm-module-owners-terraform GitHub team via the Azure Verified Module Owners Terraform entitlement in Core Identity (Microsoft internal tool).




See origin...

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

Note

These required GitHub teams are already associated to the BRM repository and have the required permissions.

Terraform

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.




See origin...

ID: SNFR10 - Category: Contribution/Support - MIT Licensing

A module MUST be published with the MIT License in the Azure GitHub organization.




See origin...

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.




See origin...

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.




See origin...

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:

➕ AVM Standard GitHub Labels

These labels are available in a CSV file from here

NameDescriptionHEX
AZD 🧑‍💻These modules are requested/used by the AZD team.
E0BFFA
Needs: Attention 👋Reply has been added to issue, maintainer to review
E99695
Needs: Immediate Attention ‼️Immediate attention of module owner / AVM team is needed
FF0000
Needs: Author Feedback 👂Awaiting feedback from the issue/PR author
F18A07
Needs: External Changes ⚒️When an issue/PR requires changes that are outside of the control of the module. e.g. to an RP.
DE389D
Needs: More Evidence ⚖We are looking for more evidence to make a decision on this
F64872
Needs: Triage 🔍Maintainers need to triage still
FBCA04
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.
FF0019
Needs: Module Contributor 📣This module needs secondary owner(s) or contributor(s) to develop or maintain it
C95474
Needs: Core Team 🧞‍♂️This item needs the AVM Core Team to review it
DB4503
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
800080
Status: Do Not Merge ⛔Do not merge PRs with this label attached as they are not ready or aligned to future direction etc.
8B4513
Status: External Contribution 🌍This is being worked on by someone outside of the AVM module owners/contributors or AVM core team
D8FA2C
Status: Fixed ✅Auto label applied when issue fixed by merged PR
90EE90
Status: Help Wanted 🆘Extra attention is needed
FF4500
Status: In Triage 🔍Picked up for triaging by an AVM core team member
D4AF37
Status: In PR 👉This is when an issue is due to be fixed in an open PR
EDEDED
Status: Invalid ❌This doesn't seem right
E4E669
Status: Long Term ⏳We will do it, but will take a longer amount of time due to complexity/priorities
B60205
Status: No Recent Activity 💤When an issue/PR has not been modified for X amount of days
808080
Status: Won't Fix 💔This will not be worked on
FFFFFF
Status: Owners Identified 🤘This module has its owners identified
FBEF2A
Status: Module Available 🟢The module is published
C8E6C9
Status: Module Deprecated 🔴This is a request to deprecate a module
000000
Status: Module Orphaned 🟡The module has no owner and is therefore orphaned at this time
F4A460
Status: Ready For Repository Creation 📝This module is approved and the owner is ready for the repository to be created (Terraform)
136A41
Status: Repository Created 📄This module has had it's repository created and configured ready for owner contribution (Terraform)
27AB03
Status: Response Overdue 🚩When an issue/PR has not been responded to for X amount of days
850000
Status: Looking For Assistance 🦆This item is looking for anyone to help develop the code and submit a PR for resolution
03FCC2
Type: Bug 🐛Something isn't working
D73A4A
Type: CI 🚀This issue is related to the AVM CI
74CFB0
Type: Documentation 📄Improvements or additions to documentation
0075CA
Type: Duplicate 🤲This issue or pull request already exists
CFD3D7
Type: Feature Request ➕New feature or request
A2EEEF
Type: Hygiene 🧹things related to testing, issue triage etc.
17016A
Type: New Module Proposal 💡A new module for AVM is being proposed
ADD8E6
Type: Question/Feedback 🙋‍♀️Further information is requested or just some feedback
CB6BA2
Type: Security Bug 🔒This is a security bug
FFFF00
Type: AVM 🅰️ ✌️ ⓜ️This is an AVM related issue
F0FFFF
Language: Terraform 🌐This is related to the Terraform IaC language
7740B6
Language: Bicep 💪This is related to the Bicep IaC language
1D73B3
Class: Resource Module 📦This is a resource module
D3D3D3
Class: Pattern Module 📦This is a pattern module
A9A9A9
Class: Utility Module 📦This is a utility module
CAD1DE
Class: Child Module 📦This is a child module
5E5186

To help apply these to a module GitHub repository you can use the below PowerShell script:

➕ Set-AvmGitHubLabels.ps1

For most scenario this is the command you’ll need to call the below PowerShell script with, replacing the value for RepositoryName:

  Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -CreateCsvLabelExports $false -NoUserPrompts $true
```shell
# Linux / MacOs
# For Windows replace $PWD with your the local path or your repository
#
docker run -it -v $PWD:/repo -w /repo mcr.microsoft.com/powershell pwsh -Command '
    #Invoke-WebRequest -Uri "https://azure.github.io/Azure-Verified-Modules/scripts/Set-AvmGitHubLabels.ps1" -OutFile "Set-AvmGitHubLabels.ps1"
    $gh_version = "2.44.1"
    Invoke-WebRequest -Uri "https://github.com/cli/cli/releases/download/v2.44.1/gh_2.44.1_linux_amd64.tar.gz" -OutFile "gh_$($gh_version)_linux_amd64.tar.gz"
    apt-get update && apt-get install -y git
    tar -xzf "gh_$($gh_version)_linux_amd64.tar.gz"
    ls -lsa
    mv "gh_$($gh_version)_linux_amd64/bin/gh" /usr/local/bin/
    rm "gh_$($gh_version)_linux_amd64.tar.gz" && rm -rf "gh_$($gh_version)_linux_amd64"
    gh --version
    ls -lsa
    gh auth login
    $OrgProject = "Azure/terraform-azurerm-avm-res-kusto-cluster"
    gh auth status
    ./Set-AvmGitHubLabels.ps1 -RepositoryName $OrgProject -CreateCsvLabelExports $false -NoUserPrompts $true

  '
```

By default this script will only update and append labels on the repository specified. However, this can be changed by setting the parameter -UpdateAndAddLabelsOnly to $false, which will remove all the labels from the repository first and then apply the AVM labels from the CSV only.

Make sure you elevate your privilege to admin level or the labels will not be applied to your repository. Go to repos.opensource.microsoft.com/orgs/Azure/repos/ to request admin access before running the script.

Full Script:

These Set-AvmGitHubLabels.ps1 can be downloaded from here.

  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Coloured output required in this script")]
  
  <#
  .SYNOPSIS
    This script can be used to create the Azure Verified Modules (AVM) standard GitHub labels to a GitHub repository.
  
  .DESCRIPTION
    This script can be used to create the Azure Verified Modules (AVM) standard GitHub labels to a GitHub repository.
  
    By default, the script will remove all pre-existing labels and apply the AVM labels. However, this can be changed by using the -RemoveExistingLabels parameter and setting it to $false. The tool will also output the labels that exist in the repository before and after the script has run to a CSV file in the current directory, or a directory specified by the -OutputDirectory parameter.
  
    The AVM labels to be created are documented here: TBC
  
  .NOTES
    Please ensure you have specified the GitHub repositry correctly. The script will prompt you to confirm the repository name before proceeding.
  
  .COMPONENT
    You must have the GitHub CLI installed and be authenticated to a GitHub account with access to the repository you are applying the labels to before running this script.
  
  .LINK
    TBC
  
  .Parameter RepositoryName
    The name of the GitHub repository to apply the labels to.
  
  .Parameter RemoveExistingLabels
    If set to $true, the default value, the script will remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels. If set to $false, the script will not remove any pre-existing labels.
  
  .Parameter UpdateAndAddLabelsOnly
    If set to $true, the default value, the script will only update and add labels to the repository specified in -RepositoryName. If set to $false, the script will remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels.
  
  .Parameter OutputDirectory
    The directory to output the pre-existing and post-existing labels to in a CSV file. The default value is the current directory.
  
  .Parameter CreateCsvLabelExports
    If set to $true, the default value, the script will output the pre-existing and post-existing labels to a CSV file in the current directory, or a directory specified by the -OutputDirectory parameter. If set to $false, the script will not output the pre-existing and post-existing labels to a CSV file.
  
  .Parameter GitHubCliLimit
    The maximum number of labels to return from the GitHub CLI. The default value is 999.
  
  .Parameter LabelsToApplyCsvUri
    The URI to the CSV file containing the labels to apply to the GitHub repository. The default value is https://raw.githubusercontent.com/jtracey93/label-source/main/avm-github-labels.csv.
  
  .Parameter NoUserPrompts
    If set to $true, the default value, the script will not prompt the user to confirm they want to remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels. If set to $false, the script will prompt the user to confirm they want to remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels.
  
    This is useful for running the script in automation workflows
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and remove all pre-existing labels.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo"
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -RemoveExistingLabels $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and output the pre-existing and post-existing labels to the directory C:\GitHubLabels.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels"
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and output the pre-existing and post-existing labels to the directory C:\GitHubLabels and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels" -RemoveExistingLabels $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not create the pre-existing and post-existing labels CSV files and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -RemoveExistingLabels $false -CreateCsvLabelExports $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not create the pre-existing and post-existing labels CSV files and do not remove any pre-existing labels, just overwrite any labels that have the same name. Finally, use a custom CSV file hosted on the internet to create the labels from.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels" -RemoveExistingLabels $false -CreateCsvLabelExports $false -LabelsToApplyCsvUri "https://example.com/csv/avm-github-labels.csv"
  
  #>
  
  #Requires -PSEdition Core
  
  [CmdletBinding()]
  param (
    [Parameter(Mandatory = $true)]
    [string]$RepositoryName,
  
    [Parameter(Mandatory = $false)]
    [bool]$RemoveExistingLabels = $true,
  
    [Parameter(Mandatory = $false)]
    [bool]$UpdateAndAddLabelsOnly = $true,
  
    [Parameter(Mandatory = $false)]
    [bool]$CreateCsvLabelExports = $true,
  
    [Parameter(Mandatory = $false)]
    [string]$OutputDirectory = (Get-Location),
  
    [Parameter(Mandatory = $false)]
    [int]$GitHubCliLimit = 999,
  
    [Parameter(Mandatory = $false)]
    [string]$LabelsToApplyCsvUri = "https://azure.github.io/Azure-Verified-Modules/governance/avm-standard-github-labels.csv",
  
    [Parameter(Mandatory = $false)]
    [bool]$NoUserPrompts = $false
  )
  
  # Check if the GitHub CLI is installed
  $GitHubCliInstalled = Get-Command gh -ErrorAction SilentlyContinue
  if ($null -eq $GitHubCliInstalled) {
    throw "The GitHub CLI is not installed. Please install the GitHub CLI and try again."
  }
  Write-Host "The GitHub CLI is installed..." -ForegroundColor Green
  
  # Check if GitHub CLI is authenticated
  $GitHubCliAuthenticated = gh auth status
  if ($LASTEXITCODE -ne 0) {
    Write-Host $GitHubCliAuthenticated -ForegroundColor Red
    throw "Not authenticated to GitHub. Please authenticate to GitHub using the GitHub CLI, `gh auth login`, and try again."
  }
  Write-Host "Authenticated to GitHub..." -ForegroundColor Green
  
  # Check if GitHub repository name is valid
  $GitHubRepositoryNameValid = $RepositoryName -match "^[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$"
  if ($false -eq $GitHubRepositoryNameValid) {
    throw "The GitHub repository name $RepositoryName is not valid. Please check the repository name and try again. The format must be <OrgName>/<RepoName>"
  }
  
  # List GitHub repository provided and check it exists
  $GitHubRepository = gh repo view $RepositoryName
  if ($LASTEXITCODE -ne 0) {
    Write-Host $GitHubRepository -ForegroundColor Red
    throw "The GitHub repository $RepositoryName does not exist. Please check the repository name and try again."
  }
  Write-Host "The GitHub repository $RepositoryName exists..." -ForegroundColor Green
  
  # PRE - Get the current GitHub repository labels and export to a CSV file in the current directory or where -OutputDirectory specifies if set to a valid directory path and the directory exists or can be created if it does not exist already
  if ($RemoveExistingLabels -or $UpdateAndAddLabelsOnly) {
    Write-Host "Getting the current GitHub repository (pre) labels for $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
  
    if ($null -ne $GitHubRepositoryLabels -and $CreateCsvLabelExports -eq $true) {
      $csvFileNamePathPre = "$OutputDirectory\$($RepositoryName.Replace('/', '_'))-Labels-Pre-$(Get-Date -Format FileDateTime).csv"
      Write-Host "Exporting the current GitHub repository (pre) labels for $RepositoryName to $csvFileNamePathPre" -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | Export-Csv -Path $csvFileNamePathPre -NoTypeInformation
    }
  }
  
  # Remove all pre-existing labels if -RemoveExistingLabels is set to $true and user confirms they want to remove all pre-existing labels
  if ($null -ne $GitHubRepositoryLabels) {
    $GitHubRepositoryLabelsJson = $GitHubRepositoryLabels | ConvertFrom-Json
    if ($RemoveExistingLabels -eq $true -and $NoUserPrompts -eq $false -and $UpdateAndAddLabelsOnly -eq $false) {
      $RemoveExistingLabelsConfirmation = Read-Host "Are you sure you want to remove all $($GitHubRepositoryLabelsJson.Count) pre-existing labels from $($RepositoryName)? (Y/N)"
      if ($RemoveExistingLabelsConfirmation -eq "Y") {
        Write-Host "Removing all pre-existing labels from $RepositoryName..." -ForegroundColor Yellow
        $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
          Write-Host "Removing label $($_.name) from $RepositoryName..." -ForegroundColor DarkRed
          gh label delete -R $RepositoryName $_.name --yes
        }
      }
    }
    if ($RemoveExistingLabels -eq $true -and $NoUserPrompts -eq $true -and $UpdateAndAddLabelsOnly -eq $false) {
      Write-Host "Removing all pre-existing labels from $RepositoryName..." -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
        Write-Host "Removing label $($_.name) from $RepositoryName..." -ForegroundColor DarkRed
        gh label delete -R $RepositoryName $_.name --yes
      }
    }
  }
  if ($null -eq $GitHubRepositoryLabels) {
    Write-Host "No pre-existing labels to remove or not selected to be removed from $RepositoryName..." -ForegroundColor Magenta
  }
  
  # Check LabelsToApplyCsvUri is valid and contains a CSV content
  Write-Host "Checking $LabelsToApplyCsvUri is valid..." -ForegroundColor Yellow
  $LabelsToApplyCsvUriValid = $LabelsToApplyCsvUri -match "^https?://"
  if ($false -eq $LabelsToApplyCsvUriValid) {
    throw "The LabelsToApplyCsvUri $LabelsToApplyCsvUri is not valid. Please check the URI and try again. The format must be a valid URI."
  }
  Write-Host "The LabelsToApplyCsvUri $LabelsToApplyCsvUri is valid..." -ForegroundColor Green
  
  # Create AVM lables from the AVM labels CSV file stored on the web using the convertfrom-csv cmdlet
  $avmLabelsCsv = Invoke-WebRequest -Uri $LabelsToApplyCsvUri | ConvertFrom-Csv
  
  # Check if the AVM labels CSV file contains the following columns: Name, Description, HEX
  $avmLabelsCsvColumns = $avmLabelsCsv | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name
  $avmLabelsCsvColumnsValid = $avmLabelsCsvColumns -contains "Name" -and $avmLabelsCsvColumns -contains "Description" -and $avmLabelsCsvColumns -contains "HEX"
  if ($false -eq $avmLabelsCsvColumnsValid) {
    throw "The labels CSV file does not contain the required columns: Name, Description, HEX. Please check the CSV file and try again. It contains the following columns: $avmLabelsCsvColumns"
  }
  Write-Host "The labels CSV file contains the required columns: Name, Description, HEX" -ForegroundColor Green
  
  # Create the AVM labels in the GitHub repository
  Write-Host "Creating/Updating the $($avmLabelsCsv.Count) AVM labels in $RepositoryName..." -ForegroundColor Yellow
  $avmLabelsCsv | ForEach-Object {
    if ($GitHubRepositoryLabelsJson.name -contains $_.name) {
      Write-Host "The label $($_.name) already exists in $RepositoryName. Updating the label to ensure description and color are consitent..." -ForegroundColor Magenta
      gh label create -R $RepositoryName "$($_.name)" -c $_.HEX -d $($_.Description) --force
    }
    else {
      Write-Host "The label $($_.name) does not exist in $RepositoryName. Creating label $($_.name) in $RepositoryName..." -ForegroundColor Cyan
      gh label create -R $RepositoryName "$($_.Name)" -c $_.HEX -d $($_.Description) --force
    }
  }
  
  # POST - Get the current GitHub repository labels and export to a CSV file in the current directory or where -OutputDirectory specifies if set to a valid directory path and the directory exists or can be created if it does not exist already
  if ($CreateCsvLabelExports -eq $true) {
    Write-Host "Getting the current GitHub repository (post) labels for $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
  
    if ($null -ne $GitHubRepositoryLabels) {
      $csvFileNamePathPre = "$OutputDirectory\$($RepositoryName.Replace('/', '_'))-Labels-Post-$(Get-Date -Format FileDateTime).csv"
      Write-Host "Exporting the current GitHub repository (post) labels for $RepositoryName to $csvFileNamePathPre" -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | Export-Csv -Path $csvFileNamePathPre -NoTypeInformation
    }
  }
  
  # If -RemoveExistingLabels is set to $true and user confirms they want to remove all pre-existing labels check that only the avm labels exist in the repository
  if ($RemoveExistingLabels -eq $true -and ($RemoveExistingLabelsConfirmation -eq "Y" -or $NoUserPrompts -eq $true) -and $UpdateAndAddLabelsOnly -eq $false) {
    Write-Host "Checking that only the AVM labels exist in $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
    $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
      if ($avmLabelsCsv.Name -notcontains $_.name) {
        throw "The label $($_.name) exists in $RepositoryName but is not in the CSV file."
      }
    }
    Write-Host "Only the CSV labels exist in $RepositoryName..." -ForegroundColor Green
  }
  
  Write-Host "The CSV labels have been created/updated in $RepositoryName..." -ForegroundColor Green
  



See origin...

ID: TFNFR3 - Category: Contribution/Support - GitHub Repo Branch Protection

Module owners MUST set a branch protection policy on their GitHub Repositories for AVM modules against their default branch, typically main, to do the following:

  1. Requires a Pull Request before merging
  2. Require approval of the most recent reviewable push
  3. Dismiss stale pull request approvals when new commits are pushed
  4. Require linear history
  5. Prevents force pushes
  6. Not allow deletions
  7. Require CODEOWNERS review
  8. Do not allow bypassing the above settings
  9. Above settings MUST also be enforced to administrators
Tip

If you use the template repository as mentioned in the contribution guide, the above will automatically be set.




Telemetry

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SFR3Deployment/Usage TelemetryMUSTOwnerInitial
2SFR4Telemetry Enablement FlexibilityMUSTOwnerInitial
➕ See Specifications for this category
See origin...

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 more details on this requirement, here.)

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.ps1 utility
  • Terraform: Executing the make docs command with the note and header ## Data Collection being placed in the module’s _footer.md beforehand
### 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

We will maintain a set of CSV files in the AVM Central Repo (Azure/Azure-Verified-Modules) with the required TelemetryId prefixes to enable checks to utilize this list to ensure the correct IDs are used. To see the formatted content of these CSV files with additional information, please visit the AVM Module Indexes page.

The value you need to use for your module is defined in the related module index. You can look it up on the index pages for Resource Modules, Pattern Modules and Utility Modules.

The ARM deployment name used for the telemetry MUST follow the pattern and MUST be no longer than 64 characters in length: 46d3xbcp.<res/ptn>.<(short) module name>.<version>.<uniqueness>

  • <res/ptn> == AVM Resource or Pattern Module
  • <(short) module name> == The AVM Module’s, possibly shortened, name including the resource provider and the resource type, without;
    • The prefixes: avm-res-
    • The prefixes: avm-ptn-
  • <version> == The AVM Module’s MAJOR.MINOR version (only) with . (periods) replaced with - (hyphens), to allow simpler splitting of the ARM deployment name
  • <uniqueness> == This section of the ARM deployment name is to be used to ensure uniqueness of the deployment name.
    • This is to cater for the following scenarios:
      • The module is deployed multiple times to the same:
        • Location/Region
        • Scope (Tenant, Management Group,Subscription, Resource Group)
Note

Due to the 64-character length limit of Azure deployment names, the <(short) module name> segment has a length limit of 36 characters, so if the module name is longer than that, it MUST be truncated to 36 characters. If any of the semantic version’s segments are longer than 1 character, it further restricts the number of characters that can be used for naming the module.

An example deployment name for the AVM Virtual Machine Resource Module would be: 46d3xbcp.res.compute-virtualmachine.1-2-3.eum3

An example deployment name for a shortened module name would be: 46d3xbcp.res.desktopvirtualization-appgroup.1-2-3.eum3

Tip

Terraform: Terraform uses a telemetry provider, the configuration of which is the same for every module and is included in the template repo.

General: See the language specific contribution guides for detailed guidance and sample code to use in AVM modules to achieve this requirement.

Terraform

To enable telemetry data collection for Terraform modules, the modtm telemetry provider MUST be used. This lightweight telemetry provider sends telemetry data to Azure Application Insights via a HTTP POST front end service.

The modtm telemetry provider is included in all Terraform modules and is enabled by default through the main.telemetry.tf file being automatically distributed from the template repo.

The modtm provider MUST be listed under the required_providers section in the module’s terraform.tf file using the following entry. This is also validated by the linter.

terraform {
  required_providers {
    # .. other required providers as needed
    modtm = {
      source = "Azure/modtm"
      version = "~> 0.3"
    }
  }
}



See origin...

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

Currently, no further requirements apply.




Naming / Composition

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SFR1Preview ServicesMUSTOwnerBAU
2SFR2WAF AlignedSHOULDOwnerBAU
3SFR5Availability ZonesMUSTOwnerInitial
4SFR6Data RedundancyMUSTOwnerInitial
5SNFR25Resource NamingMUSTOwnerInitial
6PMFR1Resource Group CreationMAYOwnerContributorBAU
7PMNFR1Module NamingMUSTOwnerInitial
8PMNFR2Use Resource Modules to Build a Pattern ModuleMUSTOwnerContributorBAU
9PMNFR3Use other Pattern Modules to Build a Pattern ModuleMUSTOwnerContributorBAU
10TFFR1Cross-Referencing ModulesMUSTOwnerContributorBAU
11TFFR3Providers - Permitted VersionsMUSTOwnerContributorBAU
12TFFR4AzAPI - response_export_valuesMUSTOwnerContributorBAU
13TFFR5AzAPI - replace_triggers_refsMUSTOwnerContributorBAU
14TFNFR4Lower snake_casingMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

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., is 2021-05-01-preview
    • Otherwise the latest “non-preview” version of the API SHOULD be used

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”



See origin...

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:

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.




See origin...

ID: SFR5 - Category: Composition - Availability Zones

Modules that deploy zone-redundant resources MUST enable the spanning across as many zones as possible by default, typically all 3.

Modules that deploy zonal resources MUST provide the ability to specify a zone for the resources to be deployed/pinned to. However, they MUST NOT default to a particular zone by default, e.g. 1 in an effort to make the consumer aware of the zone they are selecting to suit their architecture requirements.

For both scenarios the modules MUST expose these configuration options via configurable parameters/variables.

Note

For information on the differences between zonal and zone-redundant services, see Availability zone service and regional support




See origin...

ID: SFR6 - Category: Composition - Data Redundancy

Modules that deploy resources or patterns that support data redundancy SHOULD enable this to the highest possible value by default, e.g. RA-GZRS. When a resource or pattern doesn’t provide the ability to specify data redundancy as a simple property, e.g. GRS etc., then the modules MUST provide the ability to enable data redundancy for the resources or pattern via parameters/variables.

For example, a Storage Account module can simply set the sku.name property to Standard_RAGZRS. Whereas a SQL DB or Cosmos DB module will need to expose more properties, via parameters/variables, to allow the specification of the regions to replicate data to as per the consumers requirements.

Note

For information on the data redundancy options in Azure, see Cross-region replication in Azure




See origin...

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.




See origin...

ID: PMFR1 - Category: Composition - Resource Group Creation

A Pattern Module MAY create Resource Group(s).




See origin...

ID: PMNFR1 - Category: Naming - Module Naming

Pattern Modules MUST follow the below naming conventions (all lower case).

Important

As part of the module proposal process, the module’s approved name is captured both in the module proposal issue AND the related module index page (backed by the corresponding CSV file).

Therefore, module owners don’t need to construct the module’s name themselves, instead they need use the name prescribed in the module proposal issue or in the related CSV file, at the time of approval.

Bicep Pattern Module Naming

  • Naming convention: avm/ptn/<hyphenated grouping/category name>/<hyphenated pattern module name>
  • Example: avm/ptn/compute/app-tier-vmss or avm/ptn/avd-lza/management-plane or avm/ptn/3-tier/web-app
  • Segments:
    • ptn defines this as a pattern module
    • <hyphenated grouping/category name> is a hierarchical grouping of pattern modules by category, with each word separated by dashes, such as:
      • project name, e.g., avd-lza,
      • primary resource provider, e.g., compute or network, or
      • architecture, e.g., 3-tier
    • <hyphenated pattern module name> is a term describing the module’s function, with each word separated by dashes, e.g., app-tier-vmss = Application Tier VMSS; management-plane = Azure Virtual Desktop Landing Zone Accelerator Management Plane

Terraform Pattern Module Naming

  • Naming convention:
    • avm-ptn-<pattern module name> (Module name for registry)
    • terraform-<provider>-avm-ptn-<pattern module name> (GitHub repository name to meet registry naming requirements)
  • Example: avm-ptn-apptiervmss or avm-ptn-avd-lza-managementplane
  • Segments:
    • <provider> is a legacy requirement of the Terraform registry. This must be set to azure
    • ptn defines this as a pattern module
    • <pattern module name> is a term describing the module’s function, e.g., apptiervmss = Application Tier VMSS; avd-lza-managementplane = Azure Virtual Desktop Landing Zone Accelerator Management Plane



See origin...

ID: PMNFR2 - Category: Composition - Use Resource Modules to Build a Pattern Module

A Pattern Module SHOULD be built from AVM Resources Modules to establish a standardized code base and improve maintainability. If a valid reason exists, a pattern module MAY contain native resources (“vanilla” code) where it’s necessary. A Pattern Module MUST NOT contain references to non-AVM modules.

Valid reasons for not using a Resource Module for a resource required by a Pattern Module include but are not limited to:

  • When using a Resource Module would result in hitting scaling limitations and/or would reduce the capabilities of the Pattern Module due to the limitations of Azure Resource Manager.
  • Developing a Pattern Module under time constraint, without having all required Resource Modules readily available.
Note

In the latter case, the Pattern Module SHOULD be updated to use the Resource Module when the required Resource Module becomes available, to avoid accumulating technical debt. Ideally, all required Resource Modules SHOULD be developed first, and then leveraged by the Pattern Module.




See origin...

ID: PMNFR3 - Category: Composition - Use other Pattern Modules to Build a Pattern Module

A Pattern Module MAY contain and be built using other AVM Pattern Modules. A Pattern Module MUST NOT contain references to non-AVM modules.




See origin...

ID: TFFR1 - Category: Composition - Cross-Referencing Modules

Module owners MAY cross-references other modules to build either Resource or Pattern modules. However, they MUST be referenced only by a HashiCorp Terraform registry reference to a pinned version e.g.,

module "other-module" {
  source  = "Azure/xxx/azure"
  version = "1.2.3"
}

They MUST NOT use git reference to a module.

module "other-module" {
  source = "git::https://xxx.yyy/xxx.git"
}
module "other-module" {
  source = "github.com/xxx/yyy"
}

Modules MUST NOT contain references to non-AVM modules.

Tip

See Module Sources for more information.




See origin...

ID: TFFR3 - Category: Providers - Permitted Versions

Authors MUST only use the following Azure providers, and versions, in their modules:

providermin versionmax version
Azure/azapi>= 2.0< 3.0

The AzureRM provider MUST NOT be used, except where the narrow exception below applies.

Exception — AzureRM for resources with no AzAPI equivalent

An AVM Terraform module MAY declare the AzureRM provider only for resources whose functionality is genuinely unavailable through any AzAPI resource — that is, where there is no equivalent in azapi_resource, azapi_data_plane_resource, azapi_resource_action, or azapi_update_resource. In practice this is limited to a small set of edge cases, most commonly data-plane operations such as Key Vault secrets and certificates, Storage blobs, and a handful of resources whose azurerm_* implementation calls non-ARM APIs.

Where this exception applies the module MUST:

  • Pin the AzureRM provider to ~> 4.0 in required_providers.

  • Use AzAPI for every resource that has an AzAPI equivalent. AzureRM MUST NOT be used as a convenience alternative to AzAPI.

  • Document the exception in the module’s README.md, listing each azurerm_* resource used, the data-plane / non-ARM API it wraps, why no AzAPI equivalent exists today, and the upstream AzAPI issue or PR tracking the eventual replacement.

  • Replace each azurerm_* resource with its AzAPI equivalent as soon as one becomes available, in the next module release after the AzAPI capability ships.

  • Add the following TFLint exclusion (only required because the AzureRM provider is otherwise blocked by AVM tooling):

    rule "provider_azurerm_disallowed" {
      enabled = false
    }

This exception MUST NOT be used to:

  • Avoid migrating an existing AzureRM resource that does have an AzAPI equivalent.
  • Reduce author effort where the AzAPI body schema is more verbose than the AzureRM resource.
  • Side-step any other AzAPI-specific spec (for example TFFR4, TFFR5, TFFR6, or TFFR7) — those rules continue to apply to every AzAPI resource the module declares, regardless of whether the module also uses AzureRM under this exception.

Authors MUST use the required_providers block in their module to enforce the provider versions.

The following is an example.

terraform {
  required_providers {
    # Include one or both providers, as needed
    azapi = {
      source  = "Azure/azapi"
      version = "~> 2.9"
    }
  }
}



See origin...

ID: TFFR4 - Category: Composition - AzAPI - response_export_values

Authors MUST specify the response_export_values argument when using the AzAPI provider:

resource "azapi_resource" "example" {
  type      = "Microsoft.Example/resourceType@2021-01-01"
  name      = "example-resource"
  location  = "West US"
  response_export_values = [] # must be specified, even if empty
  body = {
    properties = {
      exampleProperty = "exampleValue"
    }
  }
}

If you require read-only properties to be returned from the resource, you SHOULD include them as follows:

resource "azapi_resource" "example" {
  type      = "Microsoft.Example/resourceType@2021-01-01"
  name      = "example-resource"
  location  = "West US"
  # Example as a list:
  response_export_values = ["properties.readOnlyProperty"]
  # Example as a map:
  # response_export_values = {
  #   read_only_property = "properties.readOnlyProperty"
  # }
  body = {
    properties = {
      exampleProperty = "exampleValue"
    }
  }
}

output "read_only_property" {
  # Example if response_export_values is a list:
  value = azapi_resource.example.output.properties.readOnlyProperty
  # Example if response_export_values is a map:
  # value = azapi_resource.example.output.read_only_property
}



See origin...

ID: TFFR5 - Category: Composition - AzAPI - replace_triggers_refs

Authors MUST specify the replace_triggers_refs argument when using the AzAPI provider.
The values should contain the body paths that would cause the resource to be replaced when they change.
You do not need to include name, or location, as these already trigger replacement.

This is to ensure that changes to properties that require replacement of the resource are handled correctly by Terraform.

resource "azapi_resource" "example" {
  type      = "Microsoft.Example/resourceType@2021-01-01"
  name      = "example-resource"
  location  = "West US"
  replace_triggers_refs = [
    "properties.exampleProperty"
  ] # must be specified, even if empty
  body = {
    properties = {
      exampleProperty = "exampleValue"
    }
  }
}



See origin...

ID: TFNFR4 - Category: Composition - Code Styling - lower snake_casing

Module owners MUST use lower snake_casing for naming the following:

  • Locals
  • Variables
  • Outputs
  • Resources (symbolic names)
  • Modules (symbolic names)

For example: snake_casing_example (every word in lowercase, with each word separated by an underscore _)




Code Style

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1TFNFR6Resource & Data OrderSHOULDOwnerContributorBAU
2TFNFR7Count & for_each UseMUSTOwnerContributorBAU
3TFNFR8Resource & Data Block OrdersSHOULDOwnerContributorBAU
4TFNFR9Module Block OrderSHOULDOwnerContributorBAU
5TFNFR10No Double Quotes in ignore_changesMUSTOwnerContributorBAU
6TFNFR11Null Comparison ToggleSHOULDOwnerContributorBAU
7TFNFR12Dynamic for Optional Nested ObjectsMUSTOwnerContributorBAU
8TFNFR13Default Values with coalesce/trySHOULDOwnerContributorBAU
9TFNFR16Variable Naming RulesSHOULDOwnerContributorBAU
10TFNFR17Variables with DescriptionsSHOULDOwnerContributorBAU
11TFNFR18Variables with TypesMUSTOwnerContributorBAU
12TFNFR19Sensitive Data VariablesSHOULDOwnerContributorBAU
13TFNFR20Non-Nullable Defaults for collection valuesSHOULDOwnerContributorBAU
14TFNFR21Discourage Nullability by DefaultMUSTOwnerContributorBAU
15TFNFR22Avoid sensitive = falseMUSTOwnerContributorBAU
16TFNFR23Sensitive Default Value ConditionsMUSTOwnerContributorBAU
17TFNFR24Handling Deprecated VariablesMUSTOwnerContributorBAU
18TFNFR25Verified Modules RequirementsMUSTOwnerContributorBAU
19TFNFR26Providers in required_providersMUSTOwnerContributorBAU
20TFNFR27Provider Declarations in ModulesMUSTOwnerContributorBAU
21TFNFR29Sensitive Data OutputsMUSTOwnerContributorBAU
22TFNFR30Handling Deprecated OutputsMUSTOwnerContributorBAU
23TFNFR31locals.tf for Locals OnlyMAYOwnerContributorBAU
25TFNFR33Precise Local TypesSHOULDOwnerContributorBAU
26TFNFR34Using Feature TogglesMUSTOwnerContributorBAU
27TFNFR35Reviewing Potential Breaking ChangesMUSTOwnerContributorBAU
28TFNFR36Setting prevent_deletion_if_contains_resourcesSHOULDOwnerContributorBAU
29TFNFR37Tool Usage by Module OwnerMAYOwnerContributorBAU
30TFNFR39Standard File LayoutMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

ID: TFNFR6 - Category: Code Style - Resource & Data Order

For the definition of resources in the same file, the resources be depended on SHOULD come first, after them are the resources depending on others.

Resources that have dependencies SHOULD be defined close to each other.




See origin...

ID: TFNFR7 - Category: Code Style - count & for_each Use

We can use count and for_each to deploy multiple resources, but the improper use of count can lead to anti pattern.

You can use count to create some kind of resources under certain conditions, for example:

resource "azapi_resource" "network_security_group" {
  count     = local.create_new_security_group ? 1 : 0
  type      = "Microsoft.Network/networkSecurityGroups@2023-11-01"
  name      = coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
  parent_id = var.parent_id
  location  = local.location
  tags      = var.new_network_security_group_tags
  body = {
    properties = {}
  }
  response_export_values = []
}

The module’s owners MUST use map(xxx) or set(xxx) as resource’s for_each collection, the map’s key or set’s element MUST be static literals.

Good example:

resource "azapi_resource" "subnet_pair" {
  for_each  = var.subnet_map // `map(string)`, when user call this module, it could be: `{ "subnet0": "subnet0" }`, or `{ "subnet0": azapi_resource.subnet0.name }`
  type      = "Microsoft.Network/virtualNetworks/subnets@2023-11-01"
  name      = "${each.value}-pair"
  parent_id = azapi_resource.virtual_network.id
  body = {
    properties = {
      addressPrefixes = ["10.0.1.0/24"]
    }
  }
  response_export_values = []
}

Bad example:

resource "azapi_resource" "subnet_pair" {
  for_each  = var.subnet_name_set // `set(string)`, when user use `toset([azapi_resource.subnet0.name])`, it would cause an error.
  type      = "Microsoft.Network/virtualNetworks/subnets@2023-11-01"
  name      = "${each.value}-pair"
  parent_id = azapi_resource.virtual_network.id
  body = {
    properties = {
      addressPrefixes = ["10.0.1.0/24"]
    }
  }
  response_export_values = []
}



See origin...

ID: TFNFR8 - Category: Code Style - Resource & Data Block Orders

There are 3 types of assignment statements in a resource or data block: argument, meta-argument and nested block. The argument assignment statement is a parameter followed by =:

location = azapi_resource.example.location

or:

tags = {
  environment = "Production"
}

Nested block is a assignment statement of parameter followed by {} block:

subnet {
  name           = "subnet1"
  address_prefix = "10.0.1.0/24"
}

Meta-arguments are assignment statements can be declared by all resource or data blocks. They are:

  • count
  • depends_on
  • for_each
  • lifecycle
  • provider

The order of declarations within resource or data blocks is:

All the meta-arguments SHOULD be declared on the top of resource or data blocks in the following order:

  1. provider
  2. count
  3. for_each

Then followed by:

  1. required arguments
  2. optional arguments
  3. required nested blocks
  4. optional nested blocks

All ranked in alphabetical order.

These meta-arguments SHOULD be declared at the bottom of a resource block with the following order:

  1. depends_on
  2. lifecycle

The parameters of lifecycle block SHOULD show up in the following order:

  1. create_before_destroy
  2. ignore_changes
  3. prevent_destroy

parameters under depends_on and ignore_changes are ranked in alphabetical order.

Meta-arguments, arguments and nested blocked are separated by blank lines.

dynamic nested blocks are ranked by the name comes after dynamic, for example:

  dynamic "linux_profile" {
    for_each = var.admin_username == null ? [] : ["linux_profile"]

    content {
      admin_username = var.admin_username

      ssh_key {
        key_data = replace(coalesce(var.public_ssh_key, tls_private_key.ssh[0].public_key_openssh), "\n", "")
      }
    }
  }

This dynamic block will be ranked as a block named linux_profile.

Code within a nested block will also be ranked following the rules above.

PS: You can use avmfix tool to reformat your code automatically.




See origin...

ID: TFNFR9 - Category: Code Style - Module Block Order

The meta-arguments below SHOULD be declared on the top of a module block with the following order:

  1. source
  2. version
  3. count
  4. for_each

blank lines will be used to separate them.

After them will be required arguments, optional arguments, all ranked in alphabetical order.

These meta-arguments below SHOULD be declared on the bottom of a resource block in the following order:

  1. depends_on
  2. providers

Arguments and meta-arguments SHOULD be separated by blank lines.




See origin...

ID: TFNFR10 - Category: Code Style - No Double Quotes in ignore_changes

The ignore_changes attribute MUST NOT be enclosed in double quotes.

Good example:

lifecycle {
    ignore_changes = [
      tags,
    ]
}

Bad example:

lifecycle {
    ignore_changes = [
      "tags",
    ]
}



See origin...

ID: TFNFR11 - Category: Code Style - Null Comparison Toggle

Sometimes we need to ensure that the resources created are compliant to some rules at a minimum extent, for example a subnet has to be connected to at least one network_security_group. The user SHOULD pass in a security_group_id and ask us to make a connection to an existing security_group, or want us to create a new security group.

Intuitively, we will define it like this:

variable "security_group_id" {
  type: string
}

resource "azapi_resource" "network_security_group" {
  count     = var.security_group_id == null ? 1 : 0
  type      = "Microsoft.Network/networkSecurityGroups@2023-11-01"
  name      = coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
  parent_id = var.parent_id
  location  = local.location
  tags      = var.new_network_security_group_tags
  body = {
    properties = {}
  }
  response_export_values = []
}

The disadvantage of this approach is if the user create a security group directly in the root module and use the id as a variable of the module, the expression which determines the value of count will contain an attribute from another resource, the value of this very attribute is “known after apply” at plan stage. Terraform core will not be able to get an exact plan of deployment during the “plan” stage.

You can’t do this:

resource "azapi_resource" "foo" {
  type      = "Microsoft.Network/networkSecurityGroups@2023-11-01"
  name      = "example-nsg"
  parent_id = "/subscriptions/.../resourceGroups/example-rg"
  location  = "eastus"
  body = {
    properties = {}
  }
  response_export_values = []
}

module "bar" {
  source = "xxxx"
  ...
  security_group_id = azapi_resource.foo.id
}

For this kind of parameters, wrapping with object type is RECOMMENDED:

variable "security_group" {
  type: object({
    id   = string
  })
  default     = null
}

The advantage of doing so is encapsulating the value which is “known after apply” in an object, and the object itself can be easily found out if it’s null or not. Since the id of a resource cannot be null, this approach can avoid the situation we are facing in the first example, like the following:

resource "azapi_resource" "foo" {
  type      = "Microsoft.Network/networkSecurityGroups@2023-11-01"
  name      = "example-nsg"
  parent_id = "/subscriptions/.../resourceGroups/example-rg"
  location  = "eastus"
  body = {
    properties = {}
  }
  response_export_values = []
}

module "bar" {
  source = "xxxx"
  ...
  security_group = {
    id = azapi_resource.foo.id
  }
}

This technique SHOULD be used under this use case only.




See origin...

ID: TFNFR12 - Category: Code Style - Dynamic for Optional Nested Objects

An example using AzAPI:

resource "azapi_resource" "main" {
  type      = "Microsoft.ContainerService/managedClusters@2024-09-01"
  name      = var.name
  parent_id = var.parent_id
  location  = var.location
  body = {
    properties = { ... }
  }

  dynamic "identity" {
    for_each = var.client_id == "" || var.client_secret == "" ? [1] : []

    content {
      type         = var.identity_type
      identity_ids = var.user_assigned_identity_ids
    }
  }
  response_export_values = []
}

Please refer to the coding style in the example. Nested blocks under conditions, MUST be declared as:

for_each = <condition> ? [<some_item>] : []



See origin...

ID: TFNFR13 - Category: Code Style - Default Values with coalesce/try

The following example shows how "${var.subnet_name}-nsg" SHOULD be used when var.new_network_security_group_name is null or ""

Good examples:

coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
try(coalesce(var.new_network_security_group.name, "${var.subnet_name}-nsg"), "${var.subnet_name}-nsg")

Bad examples:

var.new_network_security_group_name == null ? "${var.subnet_name}-nsg" : var.new_network_security_group_name)



See origin...

ID: TFNFR16 - Category: Code Style - Variable Naming Rules

The naming of a variable SHOULD follow HashiCorp’s naming rule.

variable used as feature switches SHOULD apply a positive statement, use xxx_enabled instead of xxx_disabled. Avoid double negatives like !xxx_disabled.

Please use xxx_enabled instead of xxx_disabled as name of a variable.




See origin...

ID: TFNFR17 - Category: Code Style - Variables with Descriptions

The target audience of description is the module users.

For a newly created variable (Eg. variable for switching dynamic block on-off), it’s description SHOULD precisely describe the input parameter’s purpose and the expected data type. description SHOULD NOT contain any information for module developers, this kind of information can only exist in code comments.

For object type variable, description can be composed in HEREDOC format:

variable "kubernetes_cluster_key_management_service" {
  type: object({
    key_vault_key_id         = string
    key_vault_network_access = optional(string)
  })
  default     = null
  description = <<DESCRIPTION
- `key_vault_key_id` - (Required) Identifier of Azure Key Vault key. See [key identifier format](https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#vault-name-and-object-name) for more details. When Azure Key Vault key management service is enabled, this field is required and must be a valid key identifier. When `enabled` is `false`, leave the field empty.
- `key_vault_network_access` - (Optional) Network access of the key vault Network access of key vault. The possible values are `Public` and `Private`. `Public` means the key vault allows public access from all networks. `Private` means the key vault disables public access and enables private link. Defaults to `Public`.
DESCRIPTION
}

You MUST remove all trailing whitespace so that terraform-docs renders the readme properly.




See origin...

ID: TFNFR18 - Category: Code Style - Variables with Types

type MUST be defined for every variable. type SHOULD be as precise as possible. Authors SHOULD NOT use any.

  • Use bool instead of string or number for true/false
  • Use string for text
  • Use concrete object instead of map(any)



See origin...

ID: TFNFR19 - Category: Code Style - Sensitive Data Variables

If variable’s type is object and contains one or more fields that would be assigned to a sensitive argument, then this whole variable SHOULD be declared as sensitive = true, otherwise you SHOULD extract sensitive field into separated variable block with sensitive = true.




See origin...

ID: TFNFR20 - Category: Code Style - Non-Nullable Defaults for collection values

Nullable SHOULD be set to false for collection values (e.g. sets, maps, lists) when using them in loops. However for scalar values like string and number, a null value MAY have a semantic meaning and as such these values are allowed.




See origin...

ID: TFNFR21 - Category: Code Style - Discourage Nullability by Default

nullable = true MUST be avoided.

Variables MUST be declared with nullable = false whenever the variable’s type has a meaningful zero value ({} for objects/maps, [] for lists/sets, "" for strings where empty has the same meaning as absent, etc.). Consumers should signal “no value” by omitting the input, not by explicitly passing null.

Exception — behavior-toggle inputs

A small, well-defined class of inputs MAY keep the implicit nullable = true (i.e. default = null) where null carries a distinct semantic meaning of “no override — use the underlying provider/AVM defaults”, and where representing that state with the type’s zero value would be ambiguous or wrong. Examples include:

  • var.retry and var.timeouts (per TFFR7) — null means “do not emit a retry/timeouts block; use the AzAPI provider defaults”.
  • var.lock (per the AVM lock interface) — null means “do not create a management lock”.
  • Optional sub-objects that toggle whole feature blocks on/off, where {} would be indistinguishable from “feature enabled with all defaults”.

Where this exception applies, the variable MUST:

  • Use default = null (the implicit nullable = true is permitted only for this purpose).
  • State explicitly in its description what null means.
  • Be consumed with a null-aware pattern (e.g. count = var.lock != null ? 1 : 0, or dynamic "timeouts" { for_each = var.timeouts == null ? [] : [var.timeouts] }).

This exception does not extend to required inputs, to collection-shaped inputs (TFNFR20), or to nested attributes inside an object — those MUST use nullable = false and the type’s zero value.




See origin...

ID: TFNFR22 - Category: Code Style - Avoid sensitive = false

sensitive = false MUST be avoided.




See origin...

ID: TFNFR23 - Category: Code Style - Sensitive Default Value Conditions

A default value MUST NOT be set for a sensitive input, unless it is an empty collection value.

Good example:

variable "example_map" {
  type        = map(string)
  default     = {}
  description = "An example map variable with an empty default value."
  sensitive   = true
}

Bad example:

variable "example_string" {
  type        = string
  default     = "sensitive_value"
  description = "An example string variable with a sensitive default value."
  sensitive   = true
}



See origin...

ID: TFNFR24 - Category: Code Style - Handling Deprecated Variables

Sometimes we will find names for some variable are not suitable anymore, or a change SHOULD be made to the data type. We want to ensure forward compatibility within a major version, so direct changes are strictly forbidden. The right way to do this is move this variable to an independent deprecated_variables.tf file, then redefine the new parameter in variable.tf and make sure it’s compatible everywhere else.

Deprecated variable MUST be annotated as DEPRECATED at the beginning of the description, at the same time the replacement’s name SHOULD be declared. E.g.,

variable "enable_network_security_group" {
  type        = string
  default     = null
  description = "DEPRECATED, use `network_security_group_enabled` instead; Whether to generate a network security group and assign it to the subnet. Changing this forces a new resource to be created."
}

A cleanup of deprecated_variables.tf SHOULD be performed during a major version release.




See origin...

ID: TFNFR25 - Category: Code Style - Verified Modules Requirements

The terraform.tf file MUST only contain one terraform block.

The first line of the terraform block MUST define a required_version property for the Terraform CLI.

The required_version property MUST include a constraint on the minimum version of the Terraform CLI. Previous releases of the Terraform CLI can have unexpected behavior.

The required_version property MUST include a constraint on the maximum major version of the Terraform CLI. Major version releases of the Terraform CLI can introduce breaking changes and MUST be tested.

The required_version property constraint SHOULD use the ~> #.# or the >= #.#.#, < #.#.# format.

Note: You can read more about Terraform version constraints in the documentation.

Example terraform.tf file:

terraform {
  required_version = "~> 1.6"
  required_providers {
    azapi = {
      source  = "Azure/azapi"
      version = "~> 2.9"
    }
  }
}



See origin...

ID: TFNFR26 - Category: Code Style - Providers in required_providers

The terraform block in terraform.tf MUST contain the required_providers block.

Each provider used directly in the module MUST be specified with the source and version properties. Providers in the required_providers block SHOULD be sorted in alphabetical order.

Do not add providers to the required_providers block that are not directly required by this module. If submodules are used then each submodule SHOULD have its own versions.tf file.

The source property MUST be in the format of namespace/name. If this is not explicitly specified, it can cause failure.

The version property MUST include a constraint on the minimum version of the provider. Older provider versions may not work as expected.

The version property MUST include a constraint on the maximum major version. A provider major version release may introduce breaking change, so updates to the major version constraint for a provider MUST be tested.

The version property constraint SHOULD use the ~> #.# or the >= #.#.#, < #.#.# format.

Note: You can read more about Terraform version constraints in the documentation.

Good examples:

terraform {
  required_version = "~> 1.6"
  required_providers {
    azapi = {
      source  = "Azure/azapi"
      version = "~> 2.9"
    }
  }
}
terraform {
  required_version = ">= 1.6.6, < 2.0.0"
  required_providers {
    azapi = {
      source  = "Azure/azapi"
      version = ">= 2.9.0, < 3.0.0"
    }
  }
}
terraform {
  required_version = ">= 1.6, < 2.0"
  required_providers {
    azapi = {
      source  = "Azure/azapi"
      version = ">= 2.9, < 3.0"
    }
  }
}

Acceptable example (but not recommended):

terraform {
  required_version = "1.6"
  required_providers {
    azapi = {
      source  = "Azure/azapi"
      version = "2.9"
    }
  }
}

Bad example:

terraform {
  required_version = ">= 1.6"
  required_providers {
    azapi = {
      source  = "Azure/azapi"
      version = ">= 2.9"
    }
  }
}



See origin...

ID: TFNFR27 - Category: Code Style - Provider Declarations in Modules

By rules, in the module code provider MUST NOT be declared. The only exception is when the module indeed need different instances of the same kind of provider(Eg. manipulating resources across different locations or accounts), you MUST declare configuration_aliases in terraform.required_providers. See details in this document.

provider block declared in the module MUST only be used to differentiate instances used in resource and data. Declaration of fields other than alias in provider block is strictly forbidden. It could lead to module users unable to utilize count, for_each or depends_on. Configurations of the provider instance SHOULD be passed in by the module users.

Good examples:

In verified module:

terraform {
  required_providers {
    azapi = {
      source                = "Azure/azapi"
      version               = "~> 2.9"
      configuration_aliases = [azapi.alternate]
    }
  }
}

In the root module where we call this verified module:

provider "azapi" {}

provider "azapi" {
  alias = "alternate"
}

module "foo" {
  source = "xxx"
  providers = {
    azapi           = azapi
    azapi.alternate = azapi.alternate
  }
}

Bad example:

In verified module:

provider "azapi" {
  # Configuration options
}



See origin...

ID: TFNFR29 - Category: Code Style - Sensitive Data Outputs

An output block that contains confidential data MUST be declared with sensitive = true.




See origin...

ID: TFNFR30 - Category: Code Style - Handling Deprecated Outputs

Sometimes we notice that the name of certain output is not appropriate anymore, however, since we have to ensure forward compatibility in the same major version, its name MUST NOT be changed directly. It MUST be moved to an independent deprecated_outputs.tf file, then redefine a new output in output.tf and make sure it’s compatible everywhere else in the module.

A cleanup SHOULD be performed to deprecated_outputs.tf and other logics related to compatibility during a major version upgrade.




See origin...

ID: TFNFR31 - Category: Code Style - locals.tf for Locals Only

In locals.tf, file we could declare multiple locals blocks, but only locals blocks are allowed.

You MAY declare locals blocks next to a resource block or data block for some advanced scenarios, like making a fake module to execute some light-weight tests aimed at the expressions.




See origin...

ID: TFNFR33 - Category: Code Style - Precise Local Types

Precise local types SHOULD be used.

Good example:

{
  name = "John"
  age  = 52
}

Bad example:

{
  name = "John"
  age  = "52" # age should be number
}



See origin...

ID: TFNFR34 - Category: Code Style - Using Feature Toggles

A toggle variable MUST be used to allow users to avoid the creation of a new resource block by default if it is added in a minor or patch version.

E.g., our previous release was v1.2.1 and next release would be v1.3.0, now we’d like to submit a pull request which contains such new resource:

resource "azapi_resource" "route_table" {
  type      = "Microsoft.Network/routeTables@2023-11-01"
  name      = coalesce(var.new_route_table_name, "${var.subnet_name}-rt")
  parent_id = var.parent_id
  location  = local.location
  body = {
    properties = {}
  }
  response_export_values = []
}

A user who’s just upgraded the module’s version would be surprised to see a new resource to be created in a newly generated plan file.

A better approach is adding a feature toggle to be turned off by default:

variable "create_route_table" {
  type     = bool
  default  = false
  nullable = false
}

resource "azapi_resource" "route_table" {
  count     = var.create_route_table ? 1 : 0
  type      = "Microsoft.Network/routeTables@2023-11-01"
  name      = coalesce(var.new_route_table_name, "${var.subnet_name}-rt")
  parent_id = var.parent_id
  location  = local.location
  body = {
    properties = {}
  }
  response_export_values = []
}



See origin...

ID: TFNFR35 - Category: Code Style - Reviewing Potential Breaking Changes

Potential breaking(surprise) changes introduced by resource block

  1. Adding a new resource without count or for_each for conditional creation, or creating by default
  2. Adding a new argument assignment with a value other than the default value provided by the provider’s schema
  3. Adding a new nested block without making it dynamic or omitting it by default
  4. Renaming a resource block without one or more corresponding moved blocks
  5. Change resource’s count to for_each, or vice versa

Terraform moved block could be your cure.

Potential breaking changes introduced by variable and output blocks

  1. Deleting(Renaming) a variable
  2. Changing type in a variable block
  3. Changing the default value in a variable block
  4. Changing variable’s nullable to false
  5. Changing variable’s sensitive from false to true
  6. Adding a new variable without default
  7. Deleting an output
  8. Changing an output’s value
  9. Changing an output’s sensitive value

These changes do not necessarily trigger breaking changes, but they are very likely to, they MUST be reviewed with caution.




See origin...

ID: TFNFR36 - Category: Code Style - Setting prevent_deletion_if_contains_resources (AzureRM only)

From Terraform AzureRM 3.0, the default value of prevent_deletion_if_contains_resources in provider block is true. This will lead to an unstable test because the test subscription has some policies applied, and they will add some extra resources during the run, which can cause failures during destroy of resource groups.

Since we cannot guarantee our testing environment won’t be applied some Azure Policy Remediation Tasks in the future, for a robust testing environment, prevent_deletion_if_contains_resources SHOULD be explicitly set to false.




See origin...

ID: TFNFR37 - Category: Code Style - Tool Usage by Module Owner

newres is a command-line tool that generates Terraform configuration files for a specified resource type. It automates the process of creating variables.tf and main.tf files, making it easier to get started with Terraform and reducing the time spent on manual configuration.

Module owners MAY use newres when they’re trying to add new resource block, attribute, or nested block. They MAY generate the whole block along with the corresponding variable blocks in an empty folder, then copy-paste the parts they need with essential refactoring.




See origin...

ID: TFNFR39 - Category: Code Style - Standard File Layout

Every Terraform AVM module (root module and every submodule) MUST organize its top-level Terraform code into the following files at the module’s root directory:

FileRequiredContents
terraform.tfMUSTThe single terraform { … } block — required_version, required_providers, and any backend configuration (root module only). Provider configuration blocks MUST NOT appear here.
variables.tfMUSTAll variable blocks for the module. MAY be split into additional variables.<topic>.tf files (see below).
outputs.tfMUSTAll output blocks for the module. MAY be split into additional outputs.<topic>.tf files (see below).
main.tfMUSTThe module’s primary resource, data, and module blocks. MAY be split into additional main.<topic>.tf files (see below).
locals.tfSHOULDAll locals blocks. Required if the module declares any locals. MAY be split into additional locals.<topic>.tf files (see below). MAY be omitted only when the module has no locals at all.

Splitting and naming additional files

For larger modules the contents of main.tf, variables.tf, outputs.tf, and locals.tf MAY each be split into multiple files along logical / topic lines. When this is done:

  • Additional Terraform files MUST use the canonical filename (main, variables, outputs, or locals) as the prefix, followed by a ., a short descriptive topic name, and the .tf extension — for example main.diagnostic_settings.tf, variables.diagnostic_settings.tf, outputs.diagnostic_settings.tf, locals.diagnostic_settings.tf.
  • The topic name MUST be snake_case (per TFNFR3).
  • The same topic name SHOULD be used across the four file types when they describe the same logical concern, so that (for example) main.private_endpoints.tf, variables.private_endpoints.tf, outputs.private_endpoints.tf, and locals.private_endpoints.tf all relate to the same feature.
  • Each split file MUST contain only the block kind matching its prefix:
    • main.<topic>.tf — only resource, data, and module blocks.
    • variables.<topic>.tf — only variable blocks.
    • outputs.<topic>.tf — only output blocks.
    • locals.<topic>.tf — only locals blocks.
  • The terraform { … } block MUST appear exactly once per module, in terraform.tf. It MUST NOT be split.

Files that MUST NOT appear at the module root

  • A providers.tf file — provider requirements belong in terraform.tf; provider configurations belong only in the consumer’s root module, never in an AVM module (per SFR2).
  • A single monolithic module.tf or everything.tf — the canonical filenames above MUST be used.

Rationale

Standardizing file layout means that any reviewer or consumer can find a module’s interface (variables.tf, outputs.tf), provider constraints (terraform.tf), and primary logic (main.tf / main.<topic>.tf) in the same place across every AVM Terraform module, without having to grep. It also makes the cascade rules in TFFR6, TFFR7, and TFRMNFR1 reviewable at a glance.

Notes

  • Submodules (per TFRMNFR1) follow the same layout in their own root directory under modules/<subresource>/. The submodule’s terraform.tf MUST declare the same set of required_providers it actually consumes.
  • Auto-generated documentation files (README.md, _header.md, _footer.md) and tooling configuration files (.terraform-docs.yml, .tflint.hcl, etc.) are out of scope of this rule and follow their own specs.



Inputs / Outputs

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR14Data TypesSHOULDOwnerContributorBAU
2SNFR22Parameters/Variables for Resource IDsMUSTOwnerContributorBAU
3SNFR26Output - Parameters - DecoratorsMUSTOwnerContributorBAU
4PMNFR5Parameter/Variable NamingSHOULDOwnerContributorBAU
5TFFR2Additional Terraform OutputsSHOULDOwnerContributorBAU
6TFFR6AzAPI - resource_types variableMUSTOwnerContributorBAU
7TFFR7AzAPI - retry and timeouts variablesMUSTOwnerContributorBAU
8TFNFR14Not allowed variablesMUSTOwnerContributorBAU
9TFNFR38Resource ID Variable ValidationMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

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.




See origin...

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
  }
}



See origin...

ID: SNFR26 - Output-Parameters - Decorators

Output parameters MUST implement:

Output parameters
@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
}



See origin...

ID: PMNFR5 - Category: Inputs - Parameter/Variable Naming

Parameter/variable input names SHOULD contain the resource to which they pertain. E.g., virtualMachineSku/virtualmachine_sku




See origin...

ID: TFFR2 - Category: Outputs - Additional Terraform Outputs

Authors SHOULD NOT output entire resource objects as these may contain sensitive outputs and the schema can change with API or provider versions.
Instead, authors SHOULD output the computed attributes of the resource as discreet outputs.
This kind of pattern protects against provider schema changes and is known as an anti-corruption layer.

Remember, you SHOULD NOT output values that are already inputs (other than name).

E.g.,

# Resource output, computed attribute.
output "foo" {
  description = "MyResource foo attribute"
  value = azapi_resource.myresource.output.properties.foo
}

# Resource output for resources that are deployed using `for_each`. Again only computed attributes.
output "childresource_foos" {
  description = "MyResource children's foo attributes"
  value = {
    for key, value in azapi_resource.mychildresource : key => value.output.properties.foo
  }
}

# Output of a sensitive attribute
output "bar" {
  description = "MyResource bar attribute"
  value     = azapi_resource.myresource.output.properties.bar
  sensitive = true
}



See origin...

ID: TFFR6 - Category: Inputs/Outputs - AzAPI - resource_types variable

Authors MUST NOT hard-code the type argument of an azapi_resource (or azapi_data_plane_resource, azapi_resource_action, azapi_update_resource) inline.

Instead, every AzAPI resource type string used by the module MUST be sourced from a single object variable named resource_types. This variable MUST:

  • Have one optional(string, "<provider>/<resource>@<api-version>") field per azapi_resource declared by the module (and by its submodules — see TFRMNFR1).
  • Default each field to the latest API version that the module has been tested against. The default MUST be a stable (non-preview) API version unless the module’s primary resource only ships a preview API.
  • Default the variable itself to {} so consumers only need to supply the keys they wish to override.
  • Be nullable = false.
  • Document each field in the variable’s description, including the resource it controls.

The rationale is to allow consumers to:

  • Target sovereign clouds (e.g., Azure US Government, Azure China) where older API versions may be the latest available.
  • Opt into a newer preview API version without waiting for a module release.
  • Pin a specific API version for compliance or reproducibility reasons.

Parent modules MUST cascade the relevant subset of resource_types to each submodule they instantiate, so that submodule API versions remain consistent with the parent’s chosen versions and a single override at the parent level propagates everywhere.

variable "resource_types" {
  type = object({
    widget = optional(string, "Microsoft.Example/widgets@2024-01-01")
    part   = optional(string, "Microsoft.Example/widgets/parts@2024-01-01")
    lock   = optional(string, "Microsoft.Authorization/locks@2020-05-01")
  })
  default     = {}
  nullable    = false
  description = <<DESCRIPTION
Override the AzAPI `<provider>/<resource>@<api-version>` strings used by this module. Each key defaults to a tested value; supply only the keys you want to override. Useful when targeting a sovereign cloud with older API versions, or when opting into a newer preview API.

- `widget` - The primary resource managed by this module.
- `part`   - Child resources of the primary resource.
- `lock`   - Management lock applied to the primary resource and its private endpoints.
DESCRIPTION
}

resource "azapi_resource" "this" {
  type      = var.resource_types.widget
  name      = var.name
  parent_id = var.parent_id
  body      = { /* ... */ }
}

module "part" {
  source = "./modules/part"

  # Cascade the relevant subset to the submodule.
  resource_types = {
    this = var.resource_types.part
  }

  # ...other arguments...
}



See origin...

ID: TFFR7 - Category: Inputs/Outputs - AzAPI - retry and timeouts variables

The retry and timeouts blocks of every azapi_resource declared by the module MUST be configurable by the consumer. Authors MUST NOT hard-code values inline that the consumer cannot override.

To meet this requirement, the module MUST expose two variables:

  • retry — an object variable controlling the AzAPI retry block.
  • timeouts — an object variable controlling the AzAPI timeouts block.

Both variables:

  • MAY define module-level defaults (e.g., a default error_message_regex such as "ScopeLocked" for resources that race with lock removal, or a default delete = "5m").
  • MUST allow the consumer to override the defaults — either by supplying a non-null value at the variable level, or by allowing per-field overrides through optional(...) attributes.
  • MUST be applied to every azapi_resource (and equivalent AzAPI resources) declared by the module.
  • MUST cascade to submodules — the parent module’s retry and timeouts values MUST be passed through to each submodule it instantiates (see TFRMNFR1). Submodules MAY additionally expose per-item overrides for cases where individual resources need different settings.
variable "retry" {
  type = object({
    error_message_regex  = optional(list(string))
    interval_seconds     = optional(number)
    max_interval_seconds = optional(number)
  })
  default     = null
  description = <<DESCRIPTION
Retry configuration applied to every `azapi` resource managed by the module (root resource and all submodules). Defaults to `null` (no custom retry).

- `error_message_regex`  - (Optional) A list of regex patterns matching error messages that trigger a retry.
- `interval_seconds`     - (Optional) Initial interval between retries in seconds.
- `max_interval_seconds` - (Optional) Maximum interval between retries in seconds.

See <https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/resource#retry> for full semantics.
DESCRIPTION
}

variable "timeouts" {
  type = object({
    create = optional(string)
    read   = optional(string)
    update = optional(string)
    delete = optional(string)
  })
  default     = null
  description = <<DESCRIPTION
Default per-operation timeouts applied to every `azapi` resource managed by the module. Defaults to `null` (provider defaults). Each value is a Go duration string (e.g. `30m`, `1h`).

- `create` - (Optional) Timeout for create operations.
- `read`   - (Optional) Timeout for read operations.
- `update` - (Optional) Timeout for update operations.
- `delete` - (Optional) Timeout for delete operations.
DESCRIPTION
}

resource "azapi_resource" "this" {
  type      = var.resource_types.this
  name      = var.name
  parent_id = var.parent_id
  body      = { /* ... */ }

  # `retry` is an attribute on `azapi_resource`, so the variable can be
  # assigned directly. `timeouts` is a block, so a `dynamic "timeouts"`
  # block is required to honor the variable's `null` default.
  retry = var.retry

  dynamic "timeouts" {
    for_each = var.timeouts == null ? [] : [var.timeouts]
    content {
      create = timeouts.value.create
      read   = timeouts.value.read
      update = timeouts.value.update
      delete = timeouts.value.delete
    }
  }

  response_export_values = []
}

module "child" {
  source = "./modules/child"

  # Cascade retry and timeouts to the submodule.
  retry    = var.retry
  timeouts = var.timeouts

  # ...other arguments...
}



See origin...

ID: TFNFR14 - Category: Inputs - Not allowed variables

Since Terraform 0.13, count, for_each and depends_on are introduced for modules, module development is significantly simplified. Module’s owners MUST NOT add variables like enabled or module_depends_on to control the entire module’s operation. Boolean feature toggles are acceptable however.




See origin...

ID: TFNFR38 - Category: Inputs/Outputs - Resource ID Variable Validation

Every input variable (or nested attribute) that holds an Azure ARM resource ID MUST be validated using the AzAPI provider-defined function provider::azapi::parse_resource_id, called with a literal string naming the expected resource type, and wrapped in can(...).

Hand-rolled regex, startswith, length, or split checks MUST NOT be used to validate resource IDs. The provider function knows the canonical ARM ID grammar for every resource type, is fixed in lockstep with the provider, and produces a single consistent error model — including for IDs whose grammar contains anomalies (such as classic resources, extension resources, or scope-based IDs).

This rule covers, but is not limited to:

  • Top-level scope variables such as parent_id (see TFRMFR1).
  • Variables that reference other Azure resources by ID (e.g. subnet_resource_id, key_vault_resource_id, workspace_resource_id, private_dns_zone_resource_ids, user_assigned_resource_ids).
  • Nested attributes inside object, map(object), set(object), or list(object) types that hold resource IDs.

Rules

  • The resource type passed to parse_resource_id MUST be a literal string (e.g. "Microsoft.Network/virtualNetworks/subnets"). It MUST NOT be a reference to another variable, local, or expression. This keeps each validation block self-contained and avoids requiring cross-variable validation.
  • For optional / nullable variables, the validation MUST short-circuit on null (e.g. var.x == null || can(provider::azapi::parse_resource_id("...", var.x))) so that callers omitting the value do not trip validation.
  • For collection-valued variables (set(string), list(string), map(string)), the validation MUST iterate the collection with alltrue([for v in ... : can(...)]).
  • For nested attributes within object types, the validation MUST iterate the parent collection (or reference the object directly) and validate each nested resource ID, again handling null for optional nested attributes.
  • Where a variable can legitimately hold IDs of more than one resource type (rare — e.g. marketplace_partner_resource_id in the diagnostic-settings interface), this rule does not apply and the variable SHOULD be left without resource-ID validation rather than validated against a single arbitrary type.

Examples

A required, single-value resource ID:

variable "key_vault_resource_id" {
  type     = string
  nullable = false

  validation {
    condition     = can(provider::azapi::parse_resource_id("Microsoft.KeyVault/vaults", var.key_vault_resource_id))
    error_message = "`key_vault_resource_id` must be a valid Azure Key Vault resource ID."
  }

  description = "The resource ID of the Key Vault that holds the customer-managed key."
}

An optional, single-value resource ID:

variable "workspace_resource_id" {
  type     = string
  default  = null
  nullable = true

  validation {
    condition     = var.workspace_resource_id == null || can(provider::azapi::parse_resource_id("Microsoft.OperationalInsights/workspaces", var.workspace_resource_id))
    error_message = "`workspace_resource_id` must be a valid Log Analytics workspace resource ID, or `null`."
  }

  description = "The resource ID of the Log Analytics workspace to send diagnostics to."
}

A collection of resource IDs:

variable "user_assigned_resource_ids" {
  type     = set(string)
  default  = []
  nullable = false

  validation {
    condition = alltrue([
      for id in var.user_assigned_resource_ids :
      can(provider::azapi::parse_resource_id("Microsoft.ManagedIdentity/userAssignedIdentities", id))
    ])
    error_message = "Each entry in `user_assigned_resource_ids` must be a valid user-assigned managed identity resource ID."
  }

  description = "A set of user-assigned managed identity resource IDs to attach to the resource."
}

A nested resource ID inside a map(object(...)):

variable "private_endpoints" {
  type = map(object({
    subnet_resource_id            = string
    private_dns_zone_resource_ids = optional(set(string), [])
    # ...other attributes...
  }))
  default  = {}
  nullable = false

  validation {
    condition = alltrue([
      for _, v in var.private_endpoints :
      can(provider::azapi::parse_resource_id("Microsoft.Network/virtualNetworks/subnets", v.subnet_resource_id))
    ])
    error_message = "Each `private_endpoints[*].subnet_resource_id` must be a valid subnet resource ID."
  }

  validation {
    condition = alltrue(flatten([
      for _, v in var.private_endpoints : [
        for id in v.private_dns_zone_resource_ids :
        can(provider::azapi::parse_resource_id("Microsoft.Network/privateDnsZones", id))
      ]
    ]))
    error_message = "Each entry in `private_endpoints[*].private_dns_zone_resource_ids` must be a valid private DNS zone resource ID."
  }
}

Notes

  • The rule applies regardless of whether the resource ID is required or optional, single-valued or collection-valued, top-level or nested.
  • parse_resource_id errors when (a) the input is not a well-formed ARM ID, or (b) the input does not parse as the supplied resource type. Wrapping in can(...) converts both failure modes into a single boolean suitable for a validation block’s condition.
  • This rule supersedes any older guidance suggesting startswith(var.x, "/") or hand-written regex for resource ID validation.



Testing

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR1Prescribed TestsMUSTOwnerContributorBAU
2SNFR2E2E TestingMUSTOwnerContributorBAU
3SNFR3AVM Compliance TestsMUSTOwnerContributorInitial
4SNFR4Unit TestsSHOULDOwnerContributorBAU
5SNFR5Upgrade TestsSHOULDOwnerContributorBAU
6SNFR6Static Analysis/Linting TestsMUSTOwnerContributorBAU
7SNFR7Idempotency TestsMUSTOwnerContributorBAU
8SNFR24Testing Child, Extension & Interface ResourcesMUSTOwnerContributorBAU
9TFNFR5Test ToolingMUSTOwnerContributorBAU
10TFNFR15Variable Definition OrderSHOULDOwnerContributorBAU
➕ See Specifications for this category
See origin...

ID: SNFR1 - Category: Testing - Prescribed Tests

Modules MUST use the prescribed tooling and testing frameworks defined in the language specific specs.




See origin...

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

To see a directory and file structure for a module, see the language specific contribution guide.

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.
➕ 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.

Details for use in E2E tests

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>).




See origin...

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/avm-core-team-technical-bicep, OR @Azure/avm-core-team-technical-terraform.




See origin...

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.




See origin...

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.




See origin...

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.




See origin...

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.




See origin...

ID: SNFR24 - Category: Testing - Testing Child, Extension & Interface Resources

Module owners MUST test that child and extension resources and those Bicep or Terreform interface resources that are supported by their modules, are validated in E2E tests as per SNFR2 to ensure they deploy and are configured correctly.

These MAY be tested in a separate E2E test and DO NOT have to be tested in each E2E test.




See origin...

ID: TFNFR5 - Category: Testing - Test Tooling

Module owners MUST use the below test script for unit/linting/static/security analysis tests.

  • ./avm pr-check



See origin...

ID: TFNFR15 - Category: Code Style - Variable Definition Order

Input variables SHOULD follow this order:

  1. All required fields, in alphabetical order
  2. All optional fields, in alphabetical order

A variable without default value is a required field, otherwise it’s an optional one.




Documentation

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR15Automatic Documentation GenerationMUSTOwnerContributorBAU
2SNFR16Examples/E2EMUSTOwnerContributorBAU
3TFNFR1DescriptionsMUSTOwnerContributorBAU
4TFNFR2Module Documentation GenerationMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

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.




See origin...

ID: SNFR16 - Category: Documentation - Examples/E2E

An examples/e2e directory MUST exist to provide named scenarios for module deployment.




See origin...

ID: TFNFR1 - Category: Documentation - Descriptions

Where descriptions for variables and outputs spans multiple lines. The description MAY provide variable input examples for each variable using the HEREDOC format and embedded markdown.

Example:

  variable "my_complex_input" {
    type = map(object({
      param1 = string
      param2 = optional(number, null)
    }))
    description = <<DESCRIPTION
  A complex input variable that is a map of objects.
  Each object has two attributes:
  
  - `param1`: A required string parameter.
  - `param2`: (Optional) An optional number parameter.
  
  Example Input:
  
  ```terraform
  my_complex_input = {
    "object1" = {
      param1 = "value1"
      param2 = 2
    }
    "object2" = {
      param1 = "value2"
    }
  }
  ```
  DESCRIPTION
  }
  



See origin...

ID: TFNFR2 - Category: Documentation - Module Documentation Generation

Terraform modules documentation MUST be automatically generated via Terraform Docs.

A file called .terraform-docs.yml MUST be present in the root of the module and have the following content:

  ---
  ### To generate the output file to partially incorporate in the README.md,
  ### Execute this command in the Terraform module's code folder:
  # terraform-docs -c .terraform-docs.yml .
  
  formatter: "markdown document" # this is required
  
  version: "0.16.0"
  
  header-from: "_header.md"
  footer-from: "_footer.md"
  
  recursive:
    enabled: false
    path: modules
  
  sections:
    hide: []
    show: []
  
  content: |-
    {{ .Header }}    
  
    <!-- markdownlint-disable MD033 -->
    {{ .Requirements }}
  
    {{ .Providers }}
  
    {{ .Resources }}
  
    <!-- markdownlint-disable MD013 -->
    {{ .Inputs }}
  
    {{ .Outputs }}
  
    {{ .Modules }}
  
    {{ .Footer }}
  
  output:
    file: README.md
    mode: replace
    template: |-
      <!-- BEGIN_TF_DOCS -->
      {{ .Content }}
      <!-- END_TF_DOCS -->      
  output-values:
    enabled: false
    from: ""
  
  sort:
    enabled: true
    by: required
  
  settings:
    anchor: true
    color: true
    default: true
    description: false
    escape: true
    hide-empty: false
    html: true
    indent: 2
    lockfile: true
    read-comments: true
    required: true
    sensitive: true
    type: true
  



Release / Publishing

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR17Semantic VersioningMUSTOwnerContributorBAU
2SNFR18Breaking ChangesSHOULDOwnerContributorBAU
3SNFR19Registries TargetedMUSTOwnerContributorBAU
4SNFR21Cross Language CollaborationSHOULDOwnerContributorBAU
➕ See Specifications for this category
See origin...

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 Version
  • Y == Minor Version
  • Z == Patch Version

Module versioning before first Major version release 1.0.0

  • Initially modules MUST be released as version 0.1.0 and 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 version 1.0.0.

    Note

    Releasing as version 0.1.0 initially 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.0 is released, given a version number X.Y.Z:

    • X Major version MUST NOT be bumped.
    • Y Minor version MUST be bumped when introducing breaking changes (which would normally bump Major after 1.0.0 release) or feature updates (same as it will be after 1.0.0 release).
    • Z Patch version MUST be bumped when introducing non-breaking, backward compatible bug fixes (same as it will be after 1.0.0 release).



See origin...

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.




See origin...

ID: SNFR19 - Category: Publishing - Registries Targeted

Modules MUST be published to their respective language public registries.

Tip

See the language specific contribution guides for detailed guidance and sample code to use in AVM modules to achieve this requirement.




See origin...

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.




Terraform Resource Module Specifications

Contribution / Support

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR8Module Owner(s) GitHubMUSTOwnerInitial
2SNFR20GitHub Teams OnlyMUSTOwnerInitial
3SNFR9AVM & PG Teams GitHub Repo PermissionsMUSTOwnerInitial
4SNFR10MIT LicensingMUSTOwnerInitial
5SNFR11Issues Response TimesMUSTOwnerContributorBAU
6SNFR12Versions SupportedMUSTOwnerBAU
7SNFR23GitHub Repo LabelsMUSTOwnerBAU
8PMNFR4Missing Resource Module(s)MUSTOwnerContributorBAU
9TFNFR3GitHub Repo Branch ProtectionMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

ID: SNFR8 - Category: Contribution/Support - Module Owner(s) GitHub

A module MUST have an owner that is defined and managed by a GitHub Team in the Azure GitHub organization.

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

The names for the GitHub teams for each approved module are already defined in the respective Module Indexes. These teams MUST be created (and used) for each module.




See origin...

ID: SNFR20 - Category: Contribution/Support - GitHub Teams Only

All GitHub repositories that AVM module are published from and hosted within MUST only assign GitHub repository permissions to GitHub teams only.

Each module MUST have a GitHub team assigned for module owners. This team MUST be created in the Azure organization in GitHub.

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

Important

As part of the module proposal process, the name of the GitHub team for each approved module is already defined in the respective Module Indexes (or CSV file). This team MUST be created (and used) for each module.

Module owners don’t need to construct the name of the GitHub team for their module themselves, instead they need use the name prescribed in the related CSV file, at the time of approval.

For a direct link, see the list of related index pages:

The @Azure prefix in the last column of the tables linked above represents the “Azure” GitHub organization all AVM-related repositories exist in. DO NOT include this segment in the team’s name!

Naming Convention

The naming convention for the GitHub teams MUST follow the below pattern:

  • <hyphenated module name>-module-owners-bicep - to grant permissions for module owners on Bicep modules

Segments:

  • <hyphenated module name> == the AVM Module’s name, with each segment separated by dashes, i.e., avm-res-<resource provider>-<ARM resource type>
    • See RMNFR1 for AVM Resource Module Naming
    • See PMNFR1 for AVM Pattern Module Naming
  • module-owners == the role the GitHub Team is assigned to
  • <bicep == the language the module is written in

Examples:

  • avm-res-compute-virtualmachine-module-owners-bicep
Note

The naming convention for Bicep modules is slightly different than the naming convention for their respective GitHub teams.

Add Team Members

All officially documented module owner(s) MUST be added to the -module-owners- team. The -module-owners- team MUST NOT have any other members.

Unless explicitly requested and agreed, members of the AVM core team or any PG teams MUST NOT be added to the -module-owners- teams as permissions for them are granted through the teams described in SNFR9.

Grant permissions through team memberships

Note

In case of Bicep modules, permissions to the BRM repository (the repo of the Bicep Registry) are granted via assigning the -module-owners- teams to parent teams that already have the required level access configured. While it is the module owner’s responsibility to initiate the addition of their team to the respective parent, only the AVM core team can approve this parent-child relationship.

Module owners MUST create their -module-owners- team and as part of the provisioning process, they MUST request the addition of this team to its respective parent team (see the table below for details).

GitHub Team NameDescriptionPermissionsPermissions granted throughWhere to work?
<hyphenated module name>-module-owners-bicepAVM Bicep Module Owners - <module name>WriteAssignment to the avm-technical-reviewers-bicep parent team.Need to work in a fork.

Example - GitHub team required for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

  • avm-res-network-virtualnetwork-module-owners-bicep –> assign to the avm-technical-reviewers-bicep parent team.
Tip

Direct link to create a new GitHub team and assign it to its parent: Create new team

Fill in the values as follows:

  • Team name: Following the naming convention described above, use the value defined in the module indexes.
  • Description: Follow the guidance above (see the Description column in the table above).
  • Parent team: Follow the guidance above (see the Permissions granted through column in the table above).
  • Team visibility: Visible
  • Team notifications: Enabled

CODEOWNERS file

As part of the “initial Pull Request” (that publishes the first version of the module), module owners MUST add an entry to the CODEOWNERS file in the BRM repository (here).

Note

Through this approach, the AVM core team will grant review permission to module owners as part of the standard PR review process.

Every CODEOWNERS entry (line) MUST include the following segments separated by a single whitespace character:

  • Path of the module, relative to the repo’s root, e.g.: /avm/res/network/virtual-network/
  • The -module-owners-team, with the @Azure/ prefix, e.g., @Azure/avm-res-network-virtualnetwork-module-owners-bicep
  • The GitHub team of the AVM Bicep reviewers, with the @Azure/ prefix, i.e., @Azure/avm-module-reviewers-bicep

Example - CODEOWNERS entry for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

  • /avm/res/network/virtual-network/ @Azure/avm-res-network-virtualnetwork-module-owners-bicep @Azure/avm-module-reviewers-bicep

Terraform

Note

Access management for Terraform repositories now uses a single team, membership of which is managed using an internal entitlement management tool (Core Identity).

All module owners MUST request access to the avm-module-owners-terraform GitHub team via the Azure Verified Module Owners Terraform entitlement in Core Identity (Microsoft internal tool).




See origin...

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

Note

These required GitHub teams are already associated to the BRM repository and have the required permissions.

Terraform

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.




See origin...

ID: SNFR10 - Category: Contribution/Support - MIT Licensing

A module MUST be published with the MIT License in the Azure GitHub organization.




See origin...

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.




See origin...

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.




See origin...

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:

➕ AVM Standard GitHub Labels

These labels are available in a CSV file from here

NameDescriptionHEX
AZD 🧑‍💻These modules are requested/used by the AZD team.
E0BFFA
Needs: Attention 👋Reply has been added to issue, maintainer to review
E99695
Needs: Immediate Attention ‼️Immediate attention of module owner / AVM team is needed
FF0000
Needs: Author Feedback 👂Awaiting feedback from the issue/PR author
F18A07
Needs: External Changes ⚒️When an issue/PR requires changes that are outside of the control of the module. e.g. to an RP.
DE389D
Needs: More Evidence ⚖We are looking for more evidence to make a decision on this
F64872
Needs: Triage 🔍Maintainers need to triage still
FBCA04
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.
FF0019
Needs: Module Contributor 📣This module needs secondary owner(s) or contributor(s) to develop or maintain it
C95474
Needs: Core Team 🧞‍♂️This item needs the AVM Core Team to review it
DB4503
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
800080
Status: Do Not Merge ⛔Do not merge PRs with this label attached as they are not ready or aligned to future direction etc.
8B4513
Status: External Contribution 🌍This is being worked on by someone outside of the AVM module owners/contributors or AVM core team
D8FA2C
Status: Fixed ✅Auto label applied when issue fixed by merged PR
90EE90
Status: Help Wanted 🆘Extra attention is needed
FF4500
Status: In Triage 🔍Picked up for triaging by an AVM core team member
D4AF37
Status: In PR 👉This is when an issue is due to be fixed in an open PR
EDEDED
Status: Invalid ❌This doesn't seem right
E4E669
Status: Long Term ⏳We will do it, but will take a longer amount of time due to complexity/priorities
B60205
Status: No Recent Activity 💤When an issue/PR has not been modified for X amount of days
808080
Status: Won't Fix 💔This will not be worked on
FFFFFF
Status: Owners Identified 🤘This module has its owners identified
FBEF2A
Status: Module Available 🟢The module is published
C8E6C9
Status: Module Deprecated 🔴This is a request to deprecate a module
000000
Status: Module Orphaned 🟡The module has no owner and is therefore orphaned at this time
F4A460
Status: Ready For Repository Creation 📝This module is approved and the owner is ready for the repository to be created (Terraform)
136A41
Status: Repository Created 📄This module has had it's repository created and configured ready for owner contribution (Terraform)
27AB03
Status: Response Overdue 🚩When an issue/PR has not been responded to for X amount of days
850000
Status: Looking For Assistance 🦆This item is looking for anyone to help develop the code and submit a PR for resolution
03FCC2
Type: Bug 🐛Something isn't working
D73A4A
Type: CI 🚀This issue is related to the AVM CI
74CFB0
Type: Documentation 📄Improvements or additions to documentation
0075CA
Type: Duplicate 🤲This issue or pull request already exists
CFD3D7
Type: Feature Request ➕New feature or request
A2EEEF
Type: Hygiene 🧹things related to testing, issue triage etc.
17016A
Type: New Module Proposal 💡A new module for AVM is being proposed
ADD8E6
Type: Question/Feedback 🙋‍♀️Further information is requested or just some feedback
CB6BA2
Type: Security Bug 🔒This is a security bug
FFFF00
Type: AVM 🅰️ ✌️ ⓜ️This is an AVM related issue
F0FFFF
Language: Terraform 🌐This is related to the Terraform IaC language
7740B6
Language: Bicep 💪This is related to the Bicep IaC language
1D73B3
Class: Resource Module 📦This is a resource module
D3D3D3
Class: Pattern Module 📦This is a pattern module
A9A9A9
Class: Utility Module 📦This is a utility module
CAD1DE
Class: Child Module 📦This is a child module
5E5186

To help apply these to a module GitHub repository you can use the below PowerShell script:

➕ Set-AvmGitHubLabels.ps1

For most scenario this is the command you’ll need to call the below PowerShell script with, replacing the value for RepositoryName:

  Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -CreateCsvLabelExports $false -NoUserPrompts $true
```shell
# Linux / MacOs
# For Windows replace $PWD with your the local path or your repository
#
docker run -it -v $PWD:/repo -w /repo mcr.microsoft.com/powershell pwsh -Command '
    #Invoke-WebRequest -Uri "https://azure.github.io/Azure-Verified-Modules/scripts/Set-AvmGitHubLabels.ps1" -OutFile "Set-AvmGitHubLabels.ps1"
    $gh_version = "2.44.1"
    Invoke-WebRequest -Uri "https://github.com/cli/cli/releases/download/v2.44.1/gh_2.44.1_linux_amd64.tar.gz" -OutFile "gh_$($gh_version)_linux_amd64.tar.gz"
    apt-get update && apt-get install -y git
    tar -xzf "gh_$($gh_version)_linux_amd64.tar.gz"
    ls -lsa
    mv "gh_$($gh_version)_linux_amd64/bin/gh" /usr/local/bin/
    rm "gh_$($gh_version)_linux_amd64.tar.gz" && rm -rf "gh_$($gh_version)_linux_amd64"
    gh --version
    ls -lsa
    gh auth login
    $OrgProject = "Azure/terraform-azurerm-avm-res-kusto-cluster"
    gh auth status
    ./Set-AvmGitHubLabels.ps1 -RepositoryName $OrgProject -CreateCsvLabelExports $false -NoUserPrompts $true

  '
```

By default this script will only update and append labels on the repository specified. However, this can be changed by setting the parameter -UpdateAndAddLabelsOnly to $false, which will remove all the labels from the repository first and then apply the AVM labels from the CSV only.

Make sure you elevate your privilege to admin level or the labels will not be applied to your repository. Go to repos.opensource.microsoft.com/orgs/Azure/repos/ to request admin access before running the script.

Full Script:

These Set-AvmGitHubLabels.ps1 can be downloaded from here.

  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Coloured output required in this script")]
  
  <#
  .SYNOPSIS
    This script can be used to create the Azure Verified Modules (AVM) standard GitHub labels to a GitHub repository.
  
  .DESCRIPTION
    This script can be used to create the Azure Verified Modules (AVM) standard GitHub labels to a GitHub repository.
  
    By default, the script will remove all pre-existing labels and apply the AVM labels. However, this can be changed by using the -RemoveExistingLabels parameter and setting it to $false. The tool will also output the labels that exist in the repository before and after the script has run to a CSV file in the current directory, or a directory specified by the -OutputDirectory parameter.
  
    The AVM labels to be created are documented here: TBC
  
  .NOTES
    Please ensure you have specified the GitHub repositry correctly. The script will prompt you to confirm the repository name before proceeding.
  
  .COMPONENT
    You must have the GitHub CLI installed and be authenticated to a GitHub account with access to the repository you are applying the labels to before running this script.
  
  .LINK
    TBC
  
  .Parameter RepositoryName
    The name of the GitHub repository to apply the labels to.
  
  .Parameter RemoveExistingLabels
    If set to $true, the default value, the script will remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels. If set to $false, the script will not remove any pre-existing labels.
  
  .Parameter UpdateAndAddLabelsOnly
    If set to $true, the default value, the script will only update and add labels to the repository specified in -RepositoryName. If set to $false, the script will remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels.
  
  .Parameter OutputDirectory
    The directory to output the pre-existing and post-existing labels to in a CSV file. The default value is the current directory.
  
  .Parameter CreateCsvLabelExports
    If set to $true, the default value, the script will output the pre-existing and post-existing labels to a CSV file in the current directory, or a directory specified by the -OutputDirectory parameter. If set to $false, the script will not output the pre-existing and post-existing labels to a CSV file.
  
  .Parameter GitHubCliLimit
    The maximum number of labels to return from the GitHub CLI. The default value is 999.
  
  .Parameter LabelsToApplyCsvUri
    The URI to the CSV file containing the labels to apply to the GitHub repository. The default value is https://raw.githubusercontent.com/jtracey93/label-source/main/avm-github-labels.csv.
  
  .Parameter NoUserPrompts
    If set to $true, the default value, the script will not prompt the user to confirm they want to remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels. If set to $false, the script will prompt the user to confirm they want to remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels.
  
    This is useful for running the script in automation workflows
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and remove all pre-existing labels.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo"
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -RemoveExistingLabels $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and output the pre-existing and post-existing labels to the directory C:\GitHubLabels.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels"
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and output the pre-existing and post-existing labels to the directory C:\GitHubLabels and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels" -RemoveExistingLabels $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not create the pre-existing and post-existing labels CSV files and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -RemoveExistingLabels $false -CreateCsvLabelExports $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not create the pre-existing and post-existing labels CSV files and do not remove any pre-existing labels, just overwrite any labels that have the same name. Finally, use a custom CSV file hosted on the internet to create the labels from.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels" -RemoveExistingLabels $false -CreateCsvLabelExports $false -LabelsToApplyCsvUri "https://example.com/csv/avm-github-labels.csv"
  
  #>
  
  #Requires -PSEdition Core
  
  [CmdletBinding()]
  param (
    [Parameter(Mandatory = $true)]
    [string]$RepositoryName,
  
    [Parameter(Mandatory = $false)]
    [bool]$RemoveExistingLabels = $true,
  
    [Parameter(Mandatory = $false)]
    [bool]$UpdateAndAddLabelsOnly = $true,
  
    [Parameter(Mandatory = $false)]
    [bool]$CreateCsvLabelExports = $true,
  
    [Parameter(Mandatory = $false)]
    [string]$OutputDirectory = (Get-Location),
  
    [Parameter(Mandatory = $false)]
    [int]$GitHubCliLimit = 999,
  
    [Parameter(Mandatory = $false)]
    [string]$LabelsToApplyCsvUri = "https://azure.github.io/Azure-Verified-Modules/governance/avm-standard-github-labels.csv",
  
    [Parameter(Mandatory = $false)]
    [bool]$NoUserPrompts = $false
  )
  
  # Check if the GitHub CLI is installed
  $GitHubCliInstalled = Get-Command gh -ErrorAction SilentlyContinue
  if ($null -eq $GitHubCliInstalled) {
    throw "The GitHub CLI is not installed. Please install the GitHub CLI and try again."
  }
  Write-Host "The GitHub CLI is installed..." -ForegroundColor Green
  
  # Check if GitHub CLI is authenticated
  $GitHubCliAuthenticated = gh auth status
  if ($LASTEXITCODE -ne 0) {
    Write-Host $GitHubCliAuthenticated -ForegroundColor Red
    throw "Not authenticated to GitHub. Please authenticate to GitHub using the GitHub CLI, `gh auth login`, and try again."
  }
  Write-Host "Authenticated to GitHub..." -ForegroundColor Green
  
  # Check if GitHub repository name is valid
  $GitHubRepositoryNameValid = $RepositoryName -match "^[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$"
  if ($false -eq $GitHubRepositoryNameValid) {
    throw "The GitHub repository name $RepositoryName is not valid. Please check the repository name and try again. The format must be <OrgName>/<RepoName>"
  }
  
  # List GitHub repository provided and check it exists
  $GitHubRepository = gh repo view $RepositoryName
  if ($LASTEXITCODE -ne 0) {
    Write-Host $GitHubRepository -ForegroundColor Red
    throw "The GitHub repository $RepositoryName does not exist. Please check the repository name and try again."
  }
  Write-Host "The GitHub repository $RepositoryName exists..." -ForegroundColor Green
  
  # PRE - Get the current GitHub repository labels and export to a CSV file in the current directory or where -OutputDirectory specifies if set to a valid directory path and the directory exists or can be created if it does not exist already
  if ($RemoveExistingLabels -or $UpdateAndAddLabelsOnly) {
    Write-Host "Getting the current GitHub repository (pre) labels for $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
  
    if ($null -ne $GitHubRepositoryLabels -and $CreateCsvLabelExports -eq $true) {
      $csvFileNamePathPre = "$OutputDirectory\$($RepositoryName.Replace('/', '_'))-Labels-Pre-$(Get-Date -Format FileDateTime).csv"
      Write-Host "Exporting the current GitHub repository (pre) labels for $RepositoryName to $csvFileNamePathPre" -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | Export-Csv -Path $csvFileNamePathPre -NoTypeInformation
    }
  }
  
  # Remove all pre-existing labels if -RemoveExistingLabels is set to $true and user confirms they want to remove all pre-existing labels
  if ($null -ne $GitHubRepositoryLabels) {
    $GitHubRepositoryLabelsJson = $GitHubRepositoryLabels | ConvertFrom-Json
    if ($RemoveExistingLabels -eq $true -and $NoUserPrompts -eq $false -and $UpdateAndAddLabelsOnly -eq $false) {
      $RemoveExistingLabelsConfirmation = Read-Host "Are you sure you want to remove all $($GitHubRepositoryLabelsJson.Count) pre-existing labels from $($RepositoryName)? (Y/N)"
      if ($RemoveExistingLabelsConfirmation -eq "Y") {
        Write-Host "Removing all pre-existing labels from $RepositoryName..." -ForegroundColor Yellow
        $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
          Write-Host "Removing label $($_.name) from $RepositoryName..." -ForegroundColor DarkRed
          gh label delete -R $RepositoryName $_.name --yes
        }
      }
    }
    if ($RemoveExistingLabels -eq $true -and $NoUserPrompts -eq $true -and $UpdateAndAddLabelsOnly -eq $false) {
      Write-Host "Removing all pre-existing labels from $RepositoryName..." -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
        Write-Host "Removing label $($_.name) from $RepositoryName..." -ForegroundColor DarkRed
        gh label delete -R $RepositoryName $_.name --yes
      }
    }
  }
  if ($null -eq $GitHubRepositoryLabels) {
    Write-Host "No pre-existing labels to remove or not selected to be removed from $RepositoryName..." -ForegroundColor Magenta
  }
  
  # Check LabelsToApplyCsvUri is valid and contains a CSV content
  Write-Host "Checking $LabelsToApplyCsvUri is valid..." -ForegroundColor Yellow
  $LabelsToApplyCsvUriValid = $LabelsToApplyCsvUri -match "^https?://"
  if ($false -eq $LabelsToApplyCsvUriValid) {
    throw "The LabelsToApplyCsvUri $LabelsToApplyCsvUri is not valid. Please check the URI and try again. The format must be a valid URI."
  }
  Write-Host "The LabelsToApplyCsvUri $LabelsToApplyCsvUri is valid..." -ForegroundColor Green
  
  # Create AVM lables from the AVM labels CSV file stored on the web using the convertfrom-csv cmdlet
  $avmLabelsCsv = Invoke-WebRequest -Uri $LabelsToApplyCsvUri | ConvertFrom-Csv
  
  # Check if the AVM labels CSV file contains the following columns: Name, Description, HEX
  $avmLabelsCsvColumns = $avmLabelsCsv | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name
  $avmLabelsCsvColumnsValid = $avmLabelsCsvColumns -contains "Name" -and $avmLabelsCsvColumns -contains "Description" -and $avmLabelsCsvColumns -contains "HEX"
  if ($false -eq $avmLabelsCsvColumnsValid) {
    throw "The labels CSV file does not contain the required columns: Name, Description, HEX. Please check the CSV file and try again. It contains the following columns: $avmLabelsCsvColumns"
  }
  Write-Host "The labels CSV file contains the required columns: Name, Description, HEX" -ForegroundColor Green
  
  # Create the AVM labels in the GitHub repository
  Write-Host "Creating/Updating the $($avmLabelsCsv.Count) AVM labels in $RepositoryName..." -ForegroundColor Yellow
  $avmLabelsCsv | ForEach-Object {
    if ($GitHubRepositoryLabelsJson.name -contains $_.name) {
      Write-Host "The label $($_.name) already exists in $RepositoryName. Updating the label to ensure description and color are consitent..." -ForegroundColor Magenta
      gh label create -R $RepositoryName "$($_.name)" -c $_.HEX -d $($_.Description) --force
    }
    else {
      Write-Host "The label $($_.name) does not exist in $RepositoryName. Creating label $($_.name) in $RepositoryName..." -ForegroundColor Cyan
      gh label create -R $RepositoryName "$($_.Name)" -c $_.HEX -d $($_.Description) --force
    }
  }
  
  # POST - Get the current GitHub repository labels and export to a CSV file in the current directory or where -OutputDirectory specifies if set to a valid directory path and the directory exists or can be created if it does not exist already
  if ($CreateCsvLabelExports -eq $true) {
    Write-Host "Getting the current GitHub repository (post) labels for $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
  
    if ($null -ne $GitHubRepositoryLabels) {
      $csvFileNamePathPre = "$OutputDirectory\$($RepositoryName.Replace('/', '_'))-Labels-Post-$(Get-Date -Format FileDateTime).csv"
      Write-Host "Exporting the current GitHub repository (post) labels for $RepositoryName to $csvFileNamePathPre" -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | Export-Csv -Path $csvFileNamePathPre -NoTypeInformation
    }
  }
  
  # If -RemoveExistingLabels is set to $true and user confirms they want to remove all pre-existing labels check that only the avm labels exist in the repository
  if ($RemoveExistingLabels -eq $true -and ($RemoveExistingLabelsConfirmation -eq "Y" -or $NoUserPrompts -eq $true) -and $UpdateAndAddLabelsOnly -eq $false) {
    Write-Host "Checking that only the AVM labels exist in $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
    $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
      if ($avmLabelsCsv.Name -notcontains $_.name) {
        throw "The label $($_.name) exists in $RepositoryName but is not in the CSV file."
      }
    }
    Write-Host "Only the CSV labels exist in $RepositoryName..." -ForegroundColor Green
  }
  
  Write-Host "The CSV labels have been created/updated in $RepositoryName..." -ForegroundColor Green
  



See origin...

ID: PMNFR4 - Category: Hygiene - Missing Resource Module(s)

An item MUST be logged onto as an issue on the AVM Central Repo (Azure/Azure-Verified-Modules) if a Resource Module does not exist for resources deployed by the pattern module.

Exception

If the Resource Module adds no value, see Resource Module functional requirement ID: RMFR2.




See origin...

ID: TFNFR3 - Category: Contribution/Support - GitHub Repo Branch Protection

Module owners MUST set a branch protection policy on their GitHub Repositories for AVM modules against their default branch, typically main, to do the following:

  1. Requires a Pull Request before merging
  2. Require approval of the most recent reviewable push
  3. Dismiss stale pull request approvals when new commits are pushed
  4. Require linear history
  5. Prevents force pushes
  6. Not allow deletions
  7. Require CODEOWNERS review
  8. Do not allow bypassing the above settings
  9. Above settings MUST also be enforced to administrators
Tip

If you use the template repository as mentioned in the contribution guide, the above will automatically be set.




Telemetry

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SFR3Deployment/Usage TelemetryMUSTOwnerInitial
2SFR4Telemetry Enablement FlexibilityMUSTOwnerInitial
➕ See Specifications for this category
See origin...

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 more details on this requirement, here.)

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.ps1 utility
  • Terraform: Executing the make docs command with the note and header ## Data Collection being placed in the module’s _footer.md beforehand
### 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

We will maintain a set of CSV files in the AVM Central Repo (Azure/Azure-Verified-Modules) with the required TelemetryId prefixes to enable checks to utilize this list to ensure the correct IDs are used. To see the formatted content of these CSV files with additional information, please visit the AVM Module Indexes page.

The value you need to use for your module is defined in the related module index. You can look it up on the index pages for Resource Modules, Pattern Modules and Utility Modules.

The ARM deployment name used for the telemetry MUST follow the pattern and MUST be no longer than 64 characters in length: 46d3xbcp.<res/ptn>.<(short) module name>.<version>.<uniqueness>

  • <res/ptn> == AVM Resource or Pattern Module
  • <(short) module name> == The AVM Module’s, possibly shortened, name including the resource provider and the resource type, without;
    • The prefixes: avm-res-
    • The prefixes: avm-ptn-
  • <version> == The AVM Module’s MAJOR.MINOR version (only) with . (periods) replaced with - (hyphens), to allow simpler splitting of the ARM deployment name
  • <uniqueness> == This section of the ARM deployment name is to be used to ensure uniqueness of the deployment name.
    • This is to cater for the following scenarios:
      • The module is deployed multiple times to the same:
        • Location/Region
        • Scope (Tenant, Management Group,Subscription, Resource Group)
Note

Due to the 64-character length limit of Azure deployment names, the <(short) module name> segment has a length limit of 36 characters, so if the module name is longer than that, it MUST be truncated to 36 characters. If any of the semantic version’s segments are longer than 1 character, it further restricts the number of characters that can be used for naming the module.

An example deployment name for the AVM Virtual Machine Resource Module would be: 46d3xbcp.res.compute-virtualmachine.1-2-3.eum3

An example deployment name for a shortened module name would be: 46d3xbcp.res.desktopvirtualization-appgroup.1-2-3.eum3

Tip

Terraform: Terraform uses a telemetry provider, the configuration of which is the same for every module and is included in the template repo.

General: See the language specific contribution guides for detailed guidance and sample code to use in AVM modules to achieve this requirement.

Terraform

To enable telemetry data collection for Terraform modules, the modtm telemetry provider MUST be used. This lightweight telemetry provider sends telemetry data to Azure Application Insights via a HTTP POST front end service.

The modtm telemetry provider is included in all Terraform modules and is enabled by default through the main.telemetry.tf file being automatically distributed from the template repo.

The modtm provider MUST be listed under the required_providers section in the module’s terraform.tf file using the following entry. This is also validated by the linter.

terraform {
  required_providers {
    # .. other required providers as needed
    modtm = {
      source = "Azure/modtm"
      version = "~> 0.3"
    }
  }
}



See origin...

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

Currently, no further requirements apply.




Naming / Composition

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SFR1Preview ServicesMUSTOwnerBAU
2SFR2WAF AlignedSHOULDOwnerBAU
3SFR5Availability ZonesMUSTOwnerInitial
4SFR6Data RedundancyMUSTOwnerInitial
5SNFR25Resource NamingMUSTOwnerInitial
6RMFR1Single Resource OnlyMUSTOwnerContributorBAU
7RMFR2No Resource Wrapper ModulesMUSTOwnerInitial
8RMFR4AVM Consistent Feature & Extension Resources Value AddMUSTOwnerContributorBAU
9RMFR5AVM Consistent Feature & Extension Resources Value Add Interfaces/SchemasMUSTOwnerContributorBAU
10RMFR8Dependency on child and other resourcesMUSTOwnerContributorBAU
11RMFR9End-of-life resource versionsSHOULDOwnerContributorBAU
12RMNFR1Module NamingMUSTOwnerInitial
13RMNFR3RP CollaborationSHOULDOwnerBAU
14TFFR1Cross-Referencing ModulesMUSTOwnerContributorBAU
15TFFR3Providers - Permitted VersionsMUSTOwnerContributorBAU
16TFFR4AzAPI - response_export_valuesMUSTOwnerContributorBAU
17TFFR5AzAPI - replace_triggers_refsMUSTOwnerContributorBAU
18TFNFR4Lower snake_casingMUSTOwnerContributorBAU
19TFRMNFR1Subresources as submodulesMUSTOwnerContributorBAU
20TFRMNFR2Primary Resource NamingMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

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., is 2021-05-01-preview
    • Otherwise the latest “non-preview” version of the API SHOULD be used

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”



See origin...

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:

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.




See origin...

ID: SFR5 - Category: Composition - Availability Zones

Modules that deploy zone-redundant resources MUST enable the spanning across as many zones as possible by default, typically all 3.

Modules that deploy zonal resources MUST provide the ability to specify a zone for the resources to be deployed/pinned to. However, they MUST NOT default to a particular zone by default, e.g. 1 in an effort to make the consumer aware of the zone they are selecting to suit their architecture requirements.

For both scenarios the modules MUST expose these configuration options via configurable parameters/variables.

Note

For information on the differences between zonal and zone-redundant services, see Availability zone service and regional support




See origin...

ID: SFR6 - Category: Composition - Data Redundancy

Modules that deploy resources or patterns that support data redundancy SHOULD enable this to the highest possible value by default, e.g. RA-GZRS. When a resource or pattern doesn’t provide the ability to specify data redundancy as a simple property, e.g. GRS etc., then the modules MUST provide the ability to enable data redundancy for the resources or pattern via parameters/variables.

For example, a Storage Account module can simply set the sku.name property to Standard_RAGZRS. Whereas a SQL DB or Cosmos DB module will need to expose more properties, via parameters/variables, to allow the specification of the regions to replicate data to as per the consumers requirements.

Note

For information on the data redundancy options in Azure, see Cross-region replication in Azure




See origin...

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.




See origin...

ID: RMFR1 - Category: Composition - Single Resource Only

A resource module MUST only deploy a single instance of the primary resource, e.g., one virtual machine per instance.

Multiple instances of the module MUST be used to scale out.




See origin...

ID: RMFR2 - Category: Composition - No Resource Wrapper Modules

A resource module MUST add value by including additional features on top of the primary resource.




See origin...

ID: RMFR4 - Category: Composition - AVM Consistent Feature & Extension Resources Value Add

Resource modules support the following optional features/extension resources, as specified, if supported by the primary resource. The top-level variable/parameter names MUST be:

Optional Features/Extension ResourcesBicep Parameter NameTerraform Variable NameMUST/SHOULD
Diagnostic SettingsdiagnosticSettingsdiagnostic_settingsMUST
Role AssignmentsroleAssignmentsrole_assignmentsMUST
Resource LockslocklockMUST
TagstagstagsMUST
Managed Identities (System / User Assigned)managedIdentitiesmanaged_identitiesMUST
Private EndpointsprivateEndpointsprivate_endpointsMUST
Customer Managed KeyscustomerManagedKeycustomer_managed_keyMUST
Azure Monitor AlertsalertsalertsSHOULD

Resource modules MUST NOT deploy required/dependent resources for the optional features/extension resources specified above. For example, for Diagnostic Settings the resource module MUST NOT deploy the Log Analytics Workspace, this is expected to be already in existence from the perspective of the resource module deployed via another method/module etc.

Note

Please note that the implementation of Customer Managed Keys from an ARM API perspective is different across various RPs that implement Customer Managed Keys in their service. For that reason you may see differences between modules on how Customer Managed Keys are handled and implemented, but functionality will be as expected.

Module owners MAY choose to utilize cross repo dependencies for these “add-on” resources, or MAY chose to implement the code directly in their own repo/module. So long as the implementation and outputs are as per the specifications requirements, then this is acceptable.

Tip

Make sure to checkout the language specific specifications for more info on this:




See origin...

ID: RMFR5 - Category: Composition - AVM Consistent Feature & Extension Resources Value Add Interfaces/Schemas

Resource modules MUST implement a common interface, e.g. the input’s data structures and properties within them (objects/arrays/dictionaries/maps), for the optional features/extension resources:

See:




See origin...

ID: RMFR8 - Category: Composition - Dependency on child and other resources

A resource module MAY contain references to other resource modules, however MUST NOT contain references to non-AVM modules nor AVM pattern modules.

See BCPFR1 and TFFR1 for more information on this.




See origin...

ID: RMFR9 - Category: Composition - End-of-life resource versions

When a given version of an Azure resource used in a resource module reaches its end-of-life (EOL) and is no longer supported by Microsoft, the module owner SHOULD ensure that:

  1. The module is aligned with these changes and only includes supported versions of the resource. This is typically achieved through the allowed values in the parameter that specifies the resource SKU or type.
  2. The following notice is shown under the Notes section of the module’s readme.md. (If any related public announcement is available, it can also be linked to from the Notes section.):

    “Certain versions of this Azure resource reached their end of life. The latest version of this module only includes supported versions of the resource. All unsupported versions have been removed from the related parameters.”

  3. AND the related parameter’s description:

    “Certain versions of this Azure resource reached their end of life. The latest version of this module only includes supported versions of the resource. All unsupported versions have been removed from this parameter.”




See origin...

ID: RMNFR1 - Category: Naming - Module Naming

Resource modules MUST follow the below naming conventions (all lower case).

Important

As part of the module proposal process, the module’s approved name is captured both in the module proposal issue AND the related module index page (backed by the corresponding CSV file).

Therefore, module owners don’t need to construct the module’s name themselves, instead they need use the name prescribed in the module proposal issue or in the related CSV file, at the time of approval.

Note

We will maintain a set of CSV files in the AVM Central Repo (Azure/Azure-Verified-Modules) with the correct singular names for all resource types to enable checks to utilize this list to ensure repos are named correctly. To see the formatted content of these CSV files with additional information, please visit the AVM Module Indexes page.

This will be updated quarterly, or ad-hoc as new RPs/ Resources are created and highlighted via a check failure.

Bicep Resource Module Naming

  • Naming convention (module name for registry): avm/res/<hyphenated resource provider name>/<hyphenated ARM resource type>
  • Example: avm/res/compute/virtual-machine or avm/res/managed-identity/user-assigned-identity
  • Segments:
    • res defines this is a resource module
    • <hyphenated resource provider name> is the resource provider’s name after the Microsoft part, with each word starting with a capital letter separated by dashes, e.g., Microsoft.Compute = compute, Microsoft.ManagedIdentity = managed-identity.
    • <hyphenated ARM resource type> is the singular version of the word after the resource provider, with each word starting with a capital letter separated by dashes, e.g., Microsoft.Compute/virtualMachines = virtual-machine, BUT Microsoft.Network/trafficmanagerprofiles = trafficmanagerprofile - since trafficmanagerprofiles is all lower case as per the ARM API definition.

Bicep Child Module Naming

  • Naming convention (module name for registry):avm/res/<hyphenated resource provider name>/<hyphenated ARM resource type>/ <hyphenated child resource type/<hyphenated grandchild resource type>/<etc.>

  • Example: avm/res/network/virtual-network/subnet or avm/res/storage/storage-account/blob-service/container

  • Segments:

    • res defines this is a resource module
    • <hyphenated resource provider name> is the resource provider’s name after the Microsoft part, with each word starting with a capital letter separated by dashes, e.g., Microsoft.Network = network.
    • <hyphenated ARM resource type> is the singular version of the word after the resource provider, with each word starting with a capital letter separated by dashes, e.g., Microsoft.Network/virtualNetworks = virtual-network.
    • <hyphenated child resource type (to be repeated for grandchildren, etc.)> is the singular version of the word after the resource provider, with each word starting with a capital letter separated by dashes, e.g., Microsoft.Network/virtualNetworks/subnets = subnet or Microsoft.Storage/storageAccounts/blobServices/containers = blob-service/container.

Terraform Resource Module Naming

  • Naming convention:
    • avm-res-<resource provider>-<ARM resource type> (module name for registry)
    • terraform-<provider>-avm-res-<resource provider>-<ARM resource type> (GitHub repository name to meet registry naming requirements)
  • Example: avm-res-compute-virtualmachine or avm-res-managedidentity-userassignedidentity
  • Segments:
    • <provider> is a legacy requirement of the Terraform registry. This must be set to azure
    • res defines this is a resource module
    • <resource provider> is the resource provider’s name after the Microsoft part, e.g., Microsoft.Compute = compute.
    • <ARM resource type> is the singular version of the word after the resource provider, e.g., Microsoft.Compute/virtualMachines = virtualmachine



See origin...

ID: RMNFR3 - Category: Composition - RP Collaboration

Module owners (Microsoft FTEs) SHOULD reach out to the respective Resource Provider teams to build a partnership and collaboration on the modules creation, existence and long term maintenance.

Review this wiki page (Microsoft Internal) for more information.




See origin...

ID: TFFR1 - Category: Composition - Cross-Referencing Modules

Module owners MAY cross-references other modules to build either Resource or Pattern modules. However, they MUST be referenced only by a HashiCorp Terraform registry reference to a pinned version e.g.,

module "other-module" {
  source  = "Azure/xxx/azure"
  version = "1.2.3"
}

They MUST NOT use git reference to a module.

module "other-module" {
  source = "git::https://xxx.yyy/xxx.git"
}
module "other-module" {
  source = "github.com/xxx/yyy"
}

Modules MUST NOT contain references to non-AVM modules.

Tip

See Module Sources for more information.




See origin...

ID: TFFR3 - Category: Providers - Permitted Versions

Authors MUST only use the following Azure providers, and versions, in their modules:

providermin versionmax version
Azure/azapi>= 2.0< 3.0

The AzureRM provider MUST NOT be used, except where the narrow exception below applies.

Exception — AzureRM for resources with no AzAPI equivalent

An AVM Terraform module MAY declare the AzureRM provider only for resources whose functionality is genuinely unavailable through any AzAPI resource — that is, where there is no equivalent in azapi_resource, azapi_data_plane_resource, azapi_resource_action, or azapi_update_resource. In practice this is limited to a small set of edge cases, most commonly data-plane operations such as Key Vault secrets and certificates, Storage blobs, and a handful of resources whose azurerm_* implementation calls non-ARM APIs.

Where this exception applies the module MUST:

  • Pin the AzureRM provider to ~> 4.0 in required_providers.

  • Use AzAPI for every resource that has an AzAPI equivalent. AzureRM MUST NOT be used as a convenience alternative to AzAPI.

  • Document the exception in the module’s README.md, listing each azurerm_* resource used, the data-plane / non-ARM API it wraps, why no AzAPI equivalent exists today, and the upstream AzAPI issue or PR tracking the eventual replacement.

  • Replace each azurerm_* resource with its AzAPI equivalent as soon as one becomes available, in the next module release after the AzAPI capability ships.

  • Add the following TFLint exclusion (only required because the AzureRM provider is otherwise blocked by AVM tooling):

    rule "provider_azurerm_disallowed" {
      enabled = false
    }

This exception MUST NOT be used to:

  • Avoid migrating an existing AzureRM resource that does have an AzAPI equivalent.
  • Reduce author effort where the AzAPI body schema is more verbose than the AzureRM resource.
  • Side-step any other AzAPI-specific spec (for example TFFR4, TFFR5, TFFR6, or TFFR7) — those rules continue to apply to every AzAPI resource the module declares, regardless of whether the module also uses AzureRM under this exception.

Authors MUST use the required_providers block in their module to enforce the provider versions.

The following is an example.

terraform {
  required_providers {
    # Include one or both providers, as needed
    azapi = {
      source  = "Azure/azapi"
      version = "~> 2.9"
    }
  }
}



See origin...

ID: TFFR4 - Category: Composition - AzAPI - response_export_values

Authors MUST specify the response_export_values argument when using the AzAPI provider:

resource "azapi_resource" "example" {
  type      = "Microsoft.Example/resourceType@2021-01-01"
  name      = "example-resource"
  location  = "West US"
  response_export_values = [] # must be specified, even if empty
  body = {
    properties = {
      exampleProperty = "exampleValue"
    }
  }
}

If you require read-only properties to be returned from the resource, you SHOULD include them as follows:

resource "azapi_resource" "example" {
  type      = "Microsoft.Example/resourceType@2021-01-01"
  name      = "example-resource"
  location  = "West US"
  # Example as a list:
  response_export_values = ["properties.readOnlyProperty"]
  # Example as a map:
  # response_export_values = {
  #   read_only_property = "properties.readOnlyProperty"
  # }
  body = {
    properties = {
      exampleProperty = "exampleValue"
    }
  }
}

output "read_only_property" {
  # Example if response_export_values is a list:
  value = azapi_resource.example.output.properties.readOnlyProperty
  # Example if response_export_values is a map:
  # value = azapi_resource.example.output.read_only_property
}



See origin...

ID: TFFR5 - Category: Composition - AzAPI - replace_triggers_refs

Authors MUST specify the replace_triggers_refs argument when using the AzAPI provider.
The values should contain the body paths that would cause the resource to be replaced when they change.
You do not need to include name, or location, as these already trigger replacement.

This is to ensure that changes to properties that require replacement of the resource are handled correctly by Terraform.

resource "azapi_resource" "example" {
  type      = "Microsoft.Example/resourceType@2021-01-01"
  name      = "example-resource"
  location  = "West US"
  replace_triggers_refs = [
    "properties.exampleProperty"
  ] # must be specified, even if empty
  body = {
    properties = {
      exampleProperty = "exampleValue"
    }
  }
}



See origin...

ID: TFNFR4 - Category: Composition - Code Styling - lower snake_casing

Module owners MUST use lower snake_casing for naming the following:

  • Locals
  • Variables
  • Outputs
  • Resources (symbolic names)
  • Modules (symbolic names)

For example: snake_casing_example (every word in lowercase, with each word separated by an underscore _)




See origin...

ID: TFRMNFR1 - Category: Composition - Subresources as submodules

Resource modules MUST implement each ARM subresource (a child resource type as defined in the API spec, for example Microsoft.Example/widgets/parts is a subresource of Microsoft.Example/widgets) as a Terraform submodule.

Submodules MUST be located under a modules/<subresource-singular-name>/ directory at the root of the module, where <subresource-singular-name> is the singular form of the ARM subresource name as per PMNFR1.

For example, a resource module for Microsoft.Example/widgets would have the following layout:

terraform-azure-avm-res-example-widget/
├─ main.tf                         # azapi_resource for Microsoft.Example/widgets
├─ variables.tf
├─ outputs.tf
├─ terraform.tf
├─ _header.md                      # required for top-level docs generation
├─ _footer.md                      # required for top-level docs generation
├─ modules/
│  ├─ part/                        # subresource: Microsoft.Example/widgets/parts
│  │  ├─ main.tf
│  │  ├─ variables.tf
│  │  ├─ outputs.tf
│  │  ├─ terraform.tf
│  │  ├─ _header.md                # required for submodule docs generation
│  │  └─ _footer.md                # required for submodule docs generation
│  └─ gadget/                      # subresource: Microsoft.Example/widgets/gadgets
│     ├─ main.tf
│     ├─ variables.tf
│     ├─ outputs.tf
│     ├─ terraform.tf
│     ├─ _header.md
│     └─ _footer.md
└─ examples/

The parent module MUST reference and compose its submodules so that supported subresources can be expressed through the parent module, but each submodule MUST also be independently consumable.

“Independently consumable” means a caller can source the submodule directly and use it without relying on hidden behavior in the parent module. Therefore, a submodule MUST follow the same interface and specification rules as a root AVM Terraform module (as listed below), even when the parent module also instantiates it.

Submodule cardinality

Submodules MUST deploy exactly one instance of the resource they manage. The submodule’s primary azapi_resource (or equivalent) MUST NOT declare count or for_each, and the submodule MUST NOT otherwise create multiple instances of its primary resource.

Cardinality is the parent module’s responsibility: the parent module MUST use count or for_each on its submodule call to control how many instances of the subresource are deployed. This keeps each submodule’s variables, outputs and tests focused on a single resource and pushes cardinality concerns up to the consumer.

This rule applies equally when a submodule is consumed through its parent module and when the same submodule is consumed directly by another caller.

For example, a parent module deploying multiple parts calls its part submodule using for_each:

module "part" {
  source   = "./modules/part"
  for_each = var.parts

  name           = each.value.name
  parent_id      = azapi_resource.this.id
  resource_types = { this = var.resource_types.part }
  retry          = var.retry
  timeouts       = var.timeouts
}

The following pattern is NOT allowed inside a submodule, because it pushes cardinality into the submodule itself:

# modules/part/main.tf (invalid)
resource "azapi_resource" "this" {
  for_each = var.parts
  # ...
}

Module source references

Parent modules MUST reference each submodule using a local relative path rooted at the parent module’s directory:

module "part" {
  source = "./modules/part"
  # ...other arguments...
}

Submodules MAY reference sibling submodules using a relative path that traverses up to the shared modules/ directory and back down into the sibling:

# Inside modules/part/main.tf, calling its sibling submodule modules/sub-part/
module "sub_part" {
  source = "../sub-part"
  # ...other arguments...
}

This pattern is useful when an ARM resource provider exposes child resources nested more than one level deep — for example Microsoft.Example/widgets/parts/components, where the part submodule itself needs to instantiate its own component submodule.

Submodules MUST NOT reference a sibling submodule via the Terraform Registry (for example Azure/avm-res-example-widget/azure//modules/part) or via a Git URL when the sibling lives in the same repository. Using a relative path keeps the entire module tree as a single unit that can be developed, tested and released atomically.

Submodule documentation files

Each submodule directory MUST contain its own _header.md and _footer.md files at the root of the submodule (alongside main.tf). These files are consumed by the AVM terraform-docs documentation generation pipeline (see TFNFR2) to produce the submodule’s README.md. Without them, the generated submodule documentation will be missing its introduction and footer sections and the documentation pipeline will not produce a complete README.md.

The submodule _header.md and _footer.md MUST:

  • Describe the subresource the submodule manages, not the parent resource.
  • Be checked in to source control (they are inputs to documentation generation, not generated artifacts).
  • Be present in every submodule under modules/, even if the submodule is not intended to be consumed independently.

Submodules are full AVM modules

Submodules MUST meet every requirement that applies to a top-level AVM Terraform resource module, including (but not limited to):

  • All shared specifications (SFR and SNFR prefixed specs).
  • All resource module specifications (RMFR and RMNFR prefixed specs).
  • All Terraform specifications (TFFR and TFNFR prefixed specs), including:
    • TFFR3 — AzAPI provider usage.
    • TFFR4response_export_values.
    • TFFR5replace_triggers_refs.
    • TFFR6resource_types variable.
    • TFFR7retry and timeouts variables (which the parent module MUST cascade to each submodule).
  • All applicable interface specifications (managed identities, role assignments, locks, diagnostic settings, private endpoints, customer-managed keys, tags) — for any interface that is supported by the underlying ARM subresource.

To avoid duplication, this specification deliberately states the requirement once: every requirement that applies to a top-level resource module applies equally to every one of its submodules. Where a requirement contradicts the submodule’s nature (for example, a submodule that is never published independently still MUST include all required documentation files but is not itself listed in the registry), the requirement is interpreted in the context of the submodule.

Rationale

Implementing subresources as submodules:

  • Provides a clean, narrowly-scoped Terraform interface per ARM resource type, mirroring the ARM/AzAPI model where each resource type has its own type identifier and API version.
  • Allows consumers to use only the subresources they need, without paying the cost of unused resources.
  • Keeps each submodule’s variables, outputs and tests focused, which improves readability, testability and review velocity.
  • Aligns with the equivalent Bicep guidance in BCPRMNFR3 so that AVM resource modules in both languages share a consistent structure.



See origin...

ID: TFRMNFR2 - Category: Naming/Composition - Primary Resource Naming

The primary azapi_resource (or equivalent AzAPI resource) declared in a Terraform resource module MUST be named this. The same rule applies to the primary resource declared in any submodule (per TFRMNFR1).

The “primary resource” is the single Azure resource that the module exists to manage — the one whose ARM resource type appears in the module’s name (per RMNFR1). Every other resource declared by the module (locks, role assignments, diagnostic settings, private endpoints, private DNS zone groups, child / extension resources required by the primary resource, etc.) is a satellite resource and MUST NOT be named this; instead, satellites MUST be named after what they represent (for example azapi_resource.lock, azapi_resource.role_assignments, azapi_resource.diagnostic_settings, azapi_resource.private_endpoints).

Standardizing on this for the primary resource lets consumers, CI checks, and the AVM interface utility module reference it predictably — most notably as azapi_resource.this.id for downstream parent_id wiring, and azapi_resource.this.output for exported values.

Example

resource "azapi_resource" "this" {
  type      = var.resource_types.this
  name      = var.name
  parent_id = var.parent_id
  body      = { /* ... */ }
}

resource "azapi_resource" "lock" {
  count = var.lock != null ? 1 : 0

  type      = module.avm_interfaces.lock_azapi.type
  name      = module.avm_interfaces.lock_azapi.name
  parent_id = module.avm_interfaces.lock_azapi.parent_id
  body      = module.avm_interfaces.lock_azapi.body
}

resource "azapi_resource" "role_assignments" {
  for_each = module.avm_interfaces.role_assignments_azapi

  type      = each.value.type
  name      = each.value.name
  parent_id = each.value.parent_id
  body      = each.value.body
}

Exceptions

The this rule MAY be relaxed only when all of the following are true:

  • The module is a utility module (per Module Classifications) OR the module’s primary functionality is implemented by two or more azapi_resource declarations that are peers (no resource is the ARM parent of any other, and no resource depends on another resource’s ID for its own creation).
  • No single azapi_resource would, on its own, be a meaningful handle for downstream consumers (i.e. there is no resource whose id would be the obvious value of a single canonical resource_id output).

A module where one azapi_resource is the ARM parent of, or a hard dependency for, another azapi_resource is NOT exempted — the parent resource is the primary and MUST be named this.

Where this exception applies, each resource MUST be named after what it represents, and the module’s README.md MUST document why the this convention does not apply.

Notes

  • This rule applies regardless of whether the primary resource uses azapi_resource, azapi_resource_action, azapi_update_resource, or any other AzAPI resource type.
  • The rule applies independently to every submodule: each submodule has its own this (the primary resource it manages) — that is the contract enabling the parent module to write module.<submodule>.resource_id.
  • The rule does not apply to data sources or to azapi_resource_list lookups; those SHOULD still be named after what they represent.



Code Style

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1TFNFR6Resource & Data OrderSHOULDOwnerContributorBAU
2TFNFR7Count & for_each UseMUSTOwnerContributorBAU
3TFNFR8Resource & Data Block OrdersSHOULDOwnerContributorBAU
4TFNFR9Module Block OrderSHOULDOwnerContributorBAU
5TFNFR10No Double Quotes in ignore_changesMUSTOwnerContributorBAU
6TFNFR11Null Comparison ToggleSHOULDOwnerContributorBAU
7TFNFR12Dynamic for Optional Nested ObjectsMUSTOwnerContributorBAU
8TFNFR13Default Values with coalesce/trySHOULDOwnerContributorBAU
9TFNFR16Variable Naming RulesSHOULDOwnerContributorBAU
10TFNFR17Variables with DescriptionsSHOULDOwnerContributorBAU
11TFNFR18Variables with TypesMUSTOwnerContributorBAU
12TFNFR19Sensitive Data VariablesSHOULDOwnerContributorBAU
13TFNFR20Non-Nullable Defaults for collection valuesSHOULDOwnerContributorBAU
14TFNFR21Discourage Nullability by DefaultMUSTOwnerContributorBAU
15TFNFR22Avoid sensitive = falseMUSTOwnerContributorBAU
16TFNFR23Sensitive Default Value ConditionsMUSTOwnerContributorBAU
17TFNFR24Handling Deprecated VariablesMUSTOwnerContributorBAU
18TFNFR25Verified Modules RequirementsMUSTOwnerContributorBAU
19TFNFR26Providers in required_providersMUSTOwnerContributorBAU
20TFNFR27Provider Declarations in ModulesMUSTOwnerContributorBAU
21TFNFR29Sensitive Data OutputsMUSTOwnerContributorBAU
22TFNFR30Handling Deprecated OutputsMUSTOwnerContributorBAU
23TFNFR31locals.tf for Locals OnlyMAYOwnerContributorBAU
25TFNFR33Precise Local TypesSHOULDOwnerContributorBAU
26TFNFR34Using Feature TogglesMUSTOwnerContributorBAU
27TFNFR35Reviewing Potential Breaking ChangesMUSTOwnerContributorBAU
28TFNFR36Setting prevent_deletion_if_contains_resourcesSHOULDOwnerContributorBAU
29TFNFR37Tool Usage by Module OwnerMAYOwnerContributorBAU
30TFNFR39Standard File LayoutMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

ID: TFNFR6 - Category: Code Style - Resource & Data Order

For the definition of resources in the same file, the resources be depended on SHOULD come first, after them are the resources depending on others.

Resources that have dependencies SHOULD be defined close to each other.




See origin...

ID: TFNFR7 - Category: Code Style - count & for_each Use

We can use count and for_each to deploy multiple resources, but the improper use of count can lead to anti pattern.

You can use count to create some kind of resources under certain conditions, for example:

resource "azapi_resource" "network_security_group" {
  count     = local.create_new_security_group ? 1 : 0
  type      = "Microsoft.Network/networkSecurityGroups@2023-11-01"
  name      = coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
  parent_id = var.parent_id
  location  = local.location
  tags      = var.new_network_security_group_tags
  body = {
    properties = {}
  }
  response_export_values = []
}

The module’s owners MUST use map(xxx) or set(xxx) as resource’s for_each collection, the map’s key or set’s element MUST be static literals.

Good example:

resource "azapi_resource" "subnet_pair" {
  for_each  = var.subnet_map // `map(string)`, when user call this module, it could be: `{ "subnet0": "subnet0" }`, or `{ "subnet0": azapi_resource.subnet0.name }`
  type      = "Microsoft.Network/virtualNetworks/subnets@2023-11-01"
  name      = "${each.value}-pair"
  parent_id = azapi_resource.virtual_network.id
  body = {
    properties = {
      addressPrefixes = ["10.0.1.0/24"]
    }
  }
  response_export_values = []
}

Bad example:

resource "azapi_resource" "subnet_pair" {
  for_each  = var.subnet_name_set // `set(string)`, when user use `toset([azapi_resource.subnet0.name])`, it would cause an error.
  type      = "Microsoft.Network/virtualNetworks/subnets@2023-11-01"
  name      = "${each.value}-pair"
  parent_id = azapi_resource.virtual_network.id
  body = {
    properties = {
      addressPrefixes = ["10.0.1.0/24"]
    }
  }
  response_export_values = []
}



See origin...

ID: TFNFR8 - Category: Code Style - Resource & Data Block Orders

There are 3 types of assignment statements in a resource or data block: argument, meta-argument and nested block. The argument assignment statement is a parameter followed by =:

location = azapi_resource.example.location

or:

tags = {
  environment = "Production"
}

Nested block is a assignment statement of parameter followed by {} block:

subnet {
  name           = "subnet1"
  address_prefix = "10.0.1.0/24"
}

Meta-arguments are assignment statements can be declared by all resource or data blocks. They are:

  • count
  • depends_on
  • for_each
  • lifecycle
  • provider

The order of declarations within resource or data blocks is:

All the meta-arguments SHOULD be declared on the top of resource or data blocks in the following order:

  1. provider
  2. count
  3. for_each

Then followed by:

  1. required arguments
  2. optional arguments
  3. required nested blocks
  4. optional nested blocks

All ranked in alphabetical order.

These meta-arguments SHOULD be declared at the bottom of a resource block with the following order:

  1. depends_on
  2. lifecycle

The parameters of lifecycle block SHOULD show up in the following order:

  1. create_before_destroy
  2. ignore_changes
  3. prevent_destroy

parameters under depends_on and ignore_changes are ranked in alphabetical order.

Meta-arguments, arguments and nested blocked are separated by blank lines.

dynamic nested blocks are ranked by the name comes after dynamic, for example:

  dynamic "linux_profile" {
    for_each = var.admin_username == null ? [] : ["linux_profile"]

    content {
      admin_username = var.admin_username

      ssh_key {
        key_data = replace(coalesce(var.public_ssh_key, tls_private_key.ssh[0].public_key_openssh), "\n", "")
      }
    }
  }

This dynamic block will be ranked as a block named linux_profile.

Code within a nested block will also be ranked following the rules above.

PS: You can use avmfix tool to reformat your code automatically.




See origin...

ID: TFNFR9 - Category: Code Style - Module Block Order

The meta-arguments below SHOULD be declared on the top of a module block with the following order:

  1. source
  2. version
  3. count
  4. for_each

blank lines will be used to separate them.

After them will be required arguments, optional arguments, all ranked in alphabetical order.

These meta-arguments below SHOULD be declared on the bottom of a resource block in the following order:

  1. depends_on
  2. providers

Arguments and meta-arguments SHOULD be separated by blank lines.




See origin...

ID: TFNFR10 - Category: Code Style - No Double Quotes in ignore_changes

The ignore_changes attribute MUST NOT be enclosed in double quotes.

Good example:

lifecycle {
    ignore_changes = [
      tags,
    ]
}

Bad example:

lifecycle {
    ignore_changes = [
      "tags",
    ]
}



See origin...

ID: TFNFR11 - Category: Code Style - Null Comparison Toggle

Sometimes we need to ensure that the resources created are compliant to some rules at a minimum extent, for example a subnet has to be connected to at least one network_security_group. The user SHOULD pass in a security_group_id and ask us to make a connection to an existing security_group, or want us to create a new security group.

Intuitively, we will define it like this:

variable "security_group_id" {
  type: string
}

resource "azapi_resource" "network_security_group" {
  count     = var.security_group_id == null ? 1 : 0
  type      = "Microsoft.Network/networkSecurityGroups@2023-11-01"
  name      = coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
  parent_id = var.parent_id
  location  = local.location
  tags      = var.new_network_security_group_tags
  body = {
    properties = {}
  }
  response_export_values = []
}

The disadvantage of this approach is if the user create a security group directly in the root module and use the id as a variable of the module, the expression which determines the value of count will contain an attribute from another resource, the value of this very attribute is “known after apply” at plan stage. Terraform core will not be able to get an exact plan of deployment during the “plan” stage.

You can’t do this:

resource "azapi_resource" "foo" {
  type      = "Microsoft.Network/networkSecurityGroups@2023-11-01"
  name      = "example-nsg"
  parent_id = "/subscriptions/.../resourceGroups/example-rg"
  location  = "eastus"
  body = {
    properties = {}
  }
  response_export_values = []
}

module "bar" {
  source = "xxxx"
  ...
  security_group_id = azapi_resource.foo.id
}

For this kind of parameters, wrapping with object type is RECOMMENDED:

variable "security_group" {
  type: object({
    id   = string
  })
  default     = null
}

The advantage of doing so is encapsulating the value which is “known after apply” in an object, and the object itself can be easily found out if it’s null or not. Since the id of a resource cannot be null, this approach can avoid the situation we are facing in the first example, like the following:

resource "azapi_resource" "foo" {
  type      = "Microsoft.Network/networkSecurityGroups@2023-11-01"
  name      = "example-nsg"
  parent_id = "/subscriptions/.../resourceGroups/example-rg"
  location  = "eastus"
  body = {
    properties = {}
  }
  response_export_values = []
}

module "bar" {
  source = "xxxx"
  ...
  security_group = {
    id = azapi_resource.foo.id
  }
}

This technique SHOULD be used under this use case only.




See origin...

ID: TFNFR12 - Category: Code Style - Dynamic for Optional Nested Objects

An example using AzAPI:

resource "azapi_resource" "main" {
  type      = "Microsoft.ContainerService/managedClusters@2024-09-01"
  name      = var.name
  parent_id = var.parent_id
  location  = var.location
  body = {
    properties = { ... }
  }

  dynamic "identity" {
    for_each = var.client_id == "" || var.client_secret == "" ? [1] : []

    content {
      type         = var.identity_type
      identity_ids = var.user_assigned_identity_ids
    }
  }
  response_export_values = []
}

Please refer to the coding style in the example. Nested blocks under conditions, MUST be declared as:

for_each = <condition> ? [<some_item>] : []



See origin...

ID: TFNFR13 - Category: Code Style - Default Values with coalesce/try

The following example shows how "${var.subnet_name}-nsg" SHOULD be used when var.new_network_security_group_name is null or ""

Good examples:

coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
try(coalesce(var.new_network_security_group.name, "${var.subnet_name}-nsg"), "${var.subnet_name}-nsg")

Bad examples:

var.new_network_security_group_name == null ? "${var.subnet_name}-nsg" : var.new_network_security_group_name)



See origin...

ID: TFNFR16 - Category: Code Style - Variable Naming Rules

The naming of a variable SHOULD follow HashiCorp’s naming rule.

variable used as feature switches SHOULD apply a positive statement, use xxx_enabled instead of xxx_disabled. Avoid double negatives like !xxx_disabled.

Please use xxx_enabled instead of xxx_disabled as name of a variable.




See origin...

ID: TFNFR17 - Category: Code Style - Variables with Descriptions

The target audience of description is the module users.

For a newly created variable (Eg. variable for switching dynamic block on-off), it’s description SHOULD precisely describe the input parameter’s purpose and the expected data type. description SHOULD NOT contain any information for module developers, this kind of information can only exist in code comments.

For object type variable, description can be composed in HEREDOC format:

variable "kubernetes_cluster_key_management_service" {
  type: object({
    key_vault_key_id         = string
    key_vault_network_access = optional(string)
  })
  default     = null
  description = <<DESCRIPTION
- `key_vault_key_id` - (Required) Identifier of Azure Key Vault key. See [key identifier format](https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#vault-name-and-object-name) for more details. When Azure Key Vault key management service is enabled, this field is required and must be a valid key identifier. When `enabled` is `false`, leave the field empty.
- `key_vault_network_access` - (Optional) Network access of the key vault Network access of key vault. The possible values are `Public` and `Private`. `Public` means the key vault allows public access from all networks. `Private` means the key vault disables public access and enables private link. Defaults to `Public`.
DESCRIPTION
}

You MUST remove all trailing whitespace so that terraform-docs renders the readme properly.




See origin...

ID: TFNFR18 - Category: Code Style - Variables with Types

type MUST be defined for every variable. type SHOULD be as precise as possible. Authors SHOULD NOT use any.

  • Use bool instead of string or number for true/false
  • Use string for text
  • Use concrete object instead of map(any)



See origin...

ID: TFNFR19 - Category: Code Style - Sensitive Data Variables

If variable’s type is object and contains one or more fields that would be assigned to a sensitive argument, then this whole variable SHOULD be declared as sensitive = true, otherwise you SHOULD extract sensitive field into separated variable block with sensitive = true.




See origin...

ID: TFNFR20 - Category: Code Style - Non-Nullable Defaults for collection values

Nullable SHOULD be set to false for collection values (e.g. sets, maps, lists) when using them in loops. However for scalar values like string and number, a null value MAY have a semantic meaning and as such these values are allowed.




See origin...

ID: TFNFR21 - Category: Code Style - Discourage Nullability by Default

nullable = true MUST be avoided.

Variables MUST be declared with nullable = false whenever the variable’s type has a meaningful zero value ({} for objects/maps, [] for lists/sets, "" for strings where empty has the same meaning as absent, etc.). Consumers should signal “no value” by omitting the input, not by explicitly passing null.

Exception — behavior-toggle inputs

A small, well-defined class of inputs MAY keep the implicit nullable = true (i.e. default = null) where null carries a distinct semantic meaning of “no override — use the underlying provider/AVM defaults”, and where representing that state with the type’s zero value would be ambiguous or wrong. Examples include:

  • var.retry and var.timeouts (per TFFR7) — null means “do not emit a retry/timeouts block; use the AzAPI provider defaults”.
  • var.lock (per the AVM lock interface) — null means “do not create a management lock”.
  • Optional sub-objects that toggle whole feature blocks on/off, where {} would be indistinguishable from “feature enabled with all defaults”.

Where this exception applies, the variable MUST:

  • Use default = null (the implicit nullable = true is permitted only for this purpose).
  • State explicitly in its description what null means.
  • Be consumed with a null-aware pattern (e.g. count = var.lock != null ? 1 : 0, or dynamic "timeouts" { for_each = var.timeouts == null ? [] : [var.timeouts] }).

This exception does not extend to required inputs, to collection-shaped inputs (TFNFR20), or to nested attributes inside an object — those MUST use nullable = false and the type’s zero value.




See origin...

ID: TFNFR22 - Category: Code Style - Avoid sensitive = false

sensitive = false MUST be avoided.




See origin...

ID: TFNFR23 - Category: Code Style - Sensitive Default Value Conditions

A default value MUST NOT be set for a sensitive input, unless it is an empty collection value.

Good example:

variable "example_map" {
  type        = map(string)
  default     = {}
  description = "An example map variable with an empty default value."
  sensitive   = true
}

Bad example:

variable "example_string" {
  type        = string
  default     = "sensitive_value"
  description = "An example string variable with a sensitive default value."
  sensitive   = true
}



See origin...

ID: TFNFR24 - Category: Code Style - Handling Deprecated Variables

Sometimes we will find names for some variable are not suitable anymore, or a change SHOULD be made to the data type. We want to ensure forward compatibility within a major version, so direct changes are strictly forbidden. The right way to do this is move this variable to an independent deprecated_variables.tf file, then redefine the new parameter in variable.tf and make sure it’s compatible everywhere else.

Deprecated variable MUST be annotated as DEPRECATED at the beginning of the description, at the same time the replacement’s name SHOULD be declared. E.g.,

variable "enable_network_security_group" {
  type        = string
  default     = null
  description = "DEPRECATED, use `network_security_group_enabled` instead; Whether to generate a network security group and assign it to the subnet. Changing this forces a new resource to be created."
}

A cleanup of deprecated_variables.tf SHOULD be performed during a major version release.




See origin...

ID: TFNFR25 - Category: Code Style - Verified Modules Requirements

The terraform.tf file MUST only contain one terraform block.

The first line of the terraform block MUST define a required_version property for the Terraform CLI.

The required_version property MUST include a constraint on the minimum version of the Terraform CLI. Previous releases of the Terraform CLI can have unexpected behavior.

The required_version property MUST include a constraint on the maximum major version of the Terraform CLI. Major version releases of the Terraform CLI can introduce breaking changes and MUST be tested.

The required_version property constraint SHOULD use the ~> #.# or the >= #.#.#, < #.#.# format.

Note: You can read more about Terraform version constraints in the documentation.

Example terraform.tf file:

terraform {
  required_version = "~> 1.6"
  required_providers {
    azapi = {
      source  = "Azure/azapi"
      version = "~> 2.9"
    }
  }
}



See origin...

ID: TFNFR26 - Category: Code Style - Providers in required_providers

The terraform block in terraform.tf MUST contain the required_providers block.

Each provider used directly in the module MUST be specified with the source and version properties. Providers in the required_providers block SHOULD be sorted in alphabetical order.

Do not add providers to the required_providers block that are not directly required by this module. If submodules are used then each submodule SHOULD have its own versions.tf file.

The source property MUST be in the format of namespace/name. If this is not explicitly specified, it can cause failure.

The version property MUST include a constraint on the minimum version of the provider. Older provider versions may not work as expected.

The version property MUST include a constraint on the maximum major version. A provider major version release may introduce breaking change, so updates to the major version constraint for a provider MUST be tested.

The version property constraint SHOULD use the ~> #.# or the >= #.#.#, < #.#.# format.

Note: You can read more about Terraform version constraints in the documentation.

Good examples:

terraform {
  required_version = "~> 1.6"
  required_providers {
    azapi = {
      source  = "Azure/azapi"
      version = "~> 2.9"
    }
  }
}
terraform {
  required_version = ">= 1.6.6, < 2.0.0"
  required_providers {
    azapi = {
      source  = "Azure/azapi"
      version = ">= 2.9.0, < 3.0.0"
    }
  }
}
terraform {
  required_version = ">= 1.6, < 2.0"
  required_providers {
    azapi = {
      source  = "Azure/azapi"
      version = ">= 2.9, < 3.0"
    }
  }
}

Acceptable example (but not recommended):

terraform {
  required_version = "1.6"
  required_providers {
    azapi = {
      source  = "Azure/azapi"
      version = "2.9"
    }
  }
}

Bad example:

terraform {
  required_version = ">= 1.6"
  required_providers {
    azapi = {
      source  = "Azure/azapi"
      version = ">= 2.9"
    }
  }
}



See origin...

ID: TFNFR27 - Category: Code Style - Provider Declarations in Modules

By rules, in the module code provider MUST NOT be declared. The only exception is when the module indeed need different instances of the same kind of provider(Eg. manipulating resources across different locations or accounts), you MUST declare configuration_aliases in terraform.required_providers. See details in this document.

provider block declared in the module MUST only be used to differentiate instances used in resource and data. Declaration of fields other than alias in provider block is strictly forbidden. It could lead to module users unable to utilize count, for_each or depends_on. Configurations of the provider instance SHOULD be passed in by the module users.

Good examples:

In verified module:

terraform {
  required_providers {
    azapi = {
      source                = "Azure/azapi"
      version               = "~> 2.9"
      configuration_aliases = [azapi.alternate]
    }
  }
}

In the root module where we call this verified module:

provider "azapi" {}

provider "azapi" {
  alias = "alternate"
}

module "foo" {
  source = "xxx"
  providers = {
    azapi           = azapi
    azapi.alternate = azapi.alternate
  }
}

Bad example:

In verified module:

provider "azapi" {
  # Configuration options
}



See origin...

ID: TFNFR29 - Category: Code Style - Sensitive Data Outputs

An output block that contains confidential data MUST be declared with sensitive = true.




See origin...

ID: TFNFR30 - Category: Code Style - Handling Deprecated Outputs

Sometimes we notice that the name of certain output is not appropriate anymore, however, since we have to ensure forward compatibility in the same major version, its name MUST NOT be changed directly. It MUST be moved to an independent deprecated_outputs.tf file, then redefine a new output in output.tf and make sure it’s compatible everywhere else in the module.

A cleanup SHOULD be performed to deprecated_outputs.tf and other logics related to compatibility during a major version upgrade.




See origin...

ID: TFNFR31 - Category: Code Style - locals.tf for Locals Only

In locals.tf, file we could declare multiple locals blocks, but only locals blocks are allowed.

You MAY declare locals blocks next to a resource block or data block for some advanced scenarios, like making a fake module to execute some light-weight tests aimed at the expressions.




See origin...

ID: TFNFR33 - Category: Code Style - Precise Local Types

Precise local types SHOULD be used.

Good example:

{
  name = "John"
  age  = 52
}

Bad example:

{
  name = "John"
  age  = "52" # age should be number
}



See origin...

ID: TFNFR34 - Category: Code Style - Using Feature Toggles

A toggle variable MUST be used to allow users to avoid the creation of a new resource block by default if it is added in a minor or patch version.

E.g., our previous release was v1.2.1 and next release would be v1.3.0, now we’d like to submit a pull request which contains such new resource:

resource "azapi_resource" "route_table" {
  type      = "Microsoft.Network/routeTables@2023-11-01"
  name      = coalesce(var.new_route_table_name, "${var.subnet_name}-rt")
  parent_id = var.parent_id
  location  = local.location
  body = {
    properties = {}
  }
  response_export_values = []
}

A user who’s just upgraded the module’s version would be surprised to see a new resource to be created in a newly generated plan file.

A better approach is adding a feature toggle to be turned off by default:

variable "create_route_table" {
  type     = bool
  default  = false
  nullable = false
}

resource "azapi_resource" "route_table" {
  count     = var.create_route_table ? 1 : 0
  type      = "Microsoft.Network/routeTables@2023-11-01"
  name      = coalesce(var.new_route_table_name, "${var.subnet_name}-rt")
  parent_id = var.parent_id
  location  = local.location
  body = {
    properties = {}
  }
  response_export_values = []
}



See origin...

ID: TFNFR35 - Category: Code Style - Reviewing Potential Breaking Changes

Potential breaking(surprise) changes introduced by resource block

  1. Adding a new resource without count or for_each for conditional creation, or creating by default
  2. Adding a new argument assignment with a value other than the default value provided by the provider’s schema
  3. Adding a new nested block without making it dynamic or omitting it by default
  4. Renaming a resource block without one or more corresponding moved blocks
  5. Change resource’s count to for_each, or vice versa

Terraform moved block could be your cure.

Potential breaking changes introduced by variable and output blocks

  1. Deleting(Renaming) a variable
  2. Changing type in a variable block
  3. Changing the default value in a variable block
  4. Changing variable’s nullable to false
  5. Changing variable’s sensitive from false to true
  6. Adding a new variable without default
  7. Deleting an output
  8. Changing an output’s value
  9. Changing an output’s sensitive value

These changes do not necessarily trigger breaking changes, but they are very likely to, they MUST be reviewed with caution.




See origin...

ID: TFNFR36 - Category: Code Style - Setting prevent_deletion_if_contains_resources (AzureRM only)

From Terraform AzureRM 3.0, the default value of prevent_deletion_if_contains_resources in provider block is true. This will lead to an unstable test because the test subscription has some policies applied, and they will add some extra resources during the run, which can cause failures during destroy of resource groups.

Since we cannot guarantee our testing environment won’t be applied some Azure Policy Remediation Tasks in the future, for a robust testing environment, prevent_deletion_if_contains_resources SHOULD be explicitly set to false.




See origin...

ID: TFNFR37 - Category: Code Style - Tool Usage by Module Owner

newres is a command-line tool that generates Terraform configuration files for a specified resource type. It automates the process of creating variables.tf and main.tf files, making it easier to get started with Terraform and reducing the time spent on manual configuration.

Module owners MAY use newres when they’re trying to add new resource block, attribute, or nested block. They MAY generate the whole block along with the corresponding variable blocks in an empty folder, then copy-paste the parts they need with essential refactoring.




See origin...

ID: TFNFR39 - Category: Code Style - Standard File Layout

Every Terraform AVM module (root module and every submodule) MUST organize its top-level Terraform code into the following files at the module’s root directory:

FileRequiredContents
terraform.tfMUSTThe single terraform { … } block — required_version, required_providers, and any backend configuration (root module only). Provider configuration blocks MUST NOT appear here.
variables.tfMUSTAll variable blocks for the module. MAY be split into additional variables.<topic>.tf files (see below).
outputs.tfMUSTAll output blocks for the module. MAY be split into additional outputs.<topic>.tf files (see below).
main.tfMUSTThe module’s primary resource, data, and module blocks. MAY be split into additional main.<topic>.tf files (see below).
locals.tfSHOULDAll locals blocks. Required if the module declares any locals. MAY be split into additional locals.<topic>.tf files (see below). MAY be omitted only when the module has no locals at all.

Splitting and naming additional files

For larger modules the contents of main.tf, variables.tf, outputs.tf, and locals.tf MAY each be split into multiple files along logical / topic lines. When this is done:

  • Additional Terraform files MUST use the canonical filename (main, variables, outputs, or locals) as the prefix, followed by a ., a short descriptive topic name, and the .tf extension — for example main.diagnostic_settings.tf, variables.diagnostic_settings.tf, outputs.diagnostic_settings.tf, locals.diagnostic_settings.tf.
  • The topic name MUST be snake_case (per TFNFR3).
  • The same topic name SHOULD be used across the four file types when they describe the same logical concern, so that (for example) main.private_endpoints.tf, variables.private_endpoints.tf, outputs.private_endpoints.tf, and locals.private_endpoints.tf all relate to the same feature.
  • Each split file MUST contain only the block kind matching its prefix:
    • main.<topic>.tf — only resource, data, and module blocks.
    • variables.<topic>.tf — only variable blocks.
    • outputs.<topic>.tf — only output blocks.
    • locals.<topic>.tf — only locals blocks.
  • The terraform { … } block MUST appear exactly once per module, in terraform.tf. It MUST NOT be split.

Files that MUST NOT appear at the module root

  • A providers.tf file — provider requirements belong in terraform.tf; provider configurations belong only in the consumer’s root module, never in an AVM module (per SFR2).
  • A single monolithic module.tf or everything.tf — the canonical filenames above MUST be used.

Rationale

Standardizing file layout means that any reviewer or consumer can find a module’s interface (variables.tf, outputs.tf), provider constraints (terraform.tf), and primary logic (main.tf / main.<topic>.tf) in the same place across every AVM Terraform module, without having to grep. It also makes the cascade rules in TFFR6, TFFR7, and TFRMNFR1 reviewable at a glance.

Notes

  • Submodules (per TFRMNFR1) follow the same layout in their own root directory under modules/<subresource>/. The submodule’s terraform.tf MUST declare the same set of required_providers it actually consumes.
  • Auto-generated documentation files (README.md, _header.md, _footer.md) and tooling configuration files (.terraform-docs.yml, .tflint.hcl, etc.) are out of scope of this rule and follow their own specs.



Inputs / Outputs

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR14Data TypesSHOULDOwnerContributorBAU
2SNFR22Parameters/Variables for Resource IDsMUSTOwnerContributorBAU
3SNFR26Output - Parameters - DecoratorsMUSTOwnerContributorBAU
4RMFR6Parameter/Variable NamingMUSTOwnerContributorBAU
5RMFR7Minimum Required OutputsMUSTOwnerContributorBAU
6RMNFR2Parameter/Variable NamingMUSTOwnerContributorBAU
7TFFR2Additional Terraform OutputsSHOULDOwnerContributorBAU
8TFFR6AzAPI - resource_types variableMUSTOwnerContributorBAU
9TFFR7AzAPI - retry and timeouts variablesMUSTOwnerContributorBAU
10TFNFR14Not allowed variablesMUSTOwnerContributorBAU
11TFNFR38Resource ID Variable ValidationMUSTOwnerContributorBAU
12TFRMFR1Resource Module Parent IDMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

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.




See origin...

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
  }
}



See origin...

ID: SNFR26 - Output-Parameters - Decorators

Output parameters MUST implement:

Output parameters
@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
}



See origin...

ID: RMFR6 - Category: Inputs - Parameter/Variable Naming

Parameters/variables that pertain to the primary resource MUST NOT use the resource type in the name.

e.g., use sku, vs. virtualMachineSku/virtualmachine_sku

Another example for where RPs contain some of their name within a property, leave the property unchanged. E.g. Key Vault has a property called keySize, it is fine to leave as this and not remove the key part from the property/parameter name.




See origin...

ID: RMFR7 - Category: Outputs - Minimum Required Outputs

Module owners MUST output the following outputs as a minimum in their modules:

OutputBicep Output NameTerraform Output Name
Resource Namenamename
Resource IDresourceIdresource_id
System Assigned Managed Identity Principal ID (if supported by module)systemAssignedMIPrincipalIdsystem_assigned_mi_principal_id
Tip

Module owners MAY also have to provide additional outputs depending on the IaC language, please check the language specific specs:




See origin...

ID: RMNFR2 - Category: Inputs - Parameter/Variable Naming

A resource module MUST use the following standard inputs:

  • name (no default)
  • location (if supported by the resource and not a global resource, then use Resource Group location, if resource supports Resource Groups, otherwise no default)



See origin...

ID: TFFR2 - Category: Outputs - Additional Terraform Outputs

Authors SHOULD NOT output entire resource objects as these may contain sensitive outputs and the schema can change with API or provider versions.
Instead, authors SHOULD output the computed attributes of the resource as discreet outputs.
This kind of pattern protects against provider schema changes and is known as an anti-corruption layer.

Remember, you SHOULD NOT output values that are already inputs (other than name).

E.g.,

# Resource output, computed attribute.
output "foo" {
  description = "MyResource foo attribute"
  value = azapi_resource.myresource.output.properties.foo
}

# Resource output for resources that are deployed using `for_each`. Again only computed attributes.
output "childresource_foos" {
  description = "MyResource children's foo attributes"
  value = {
    for key, value in azapi_resource.mychildresource : key => value.output.properties.foo
  }
}

# Output of a sensitive attribute
output "bar" {
  description = "MyResource bar attribute"
  value     = azapi_resource.myresource.output.properties.bar
  sensitive = true
}



See origin...

ID: TFFR6 - Category: Inputs/Outputs - AzAPI - resource_types variable

Authors MUST NOT hard-code the type argument of an azapi_resource (or azapi_data_plane_resource, azapi_resource_action, azapi_update_resource) inline.

Instead, every AzAPI resource type string used by the module MUST be sourced from a single object variable named resource_types. This variable MUST:

  • Have one optional(string, "<provider>/<resource>@<api-version>") field per azapi_resource declared by the module (and by its submodules — see TFRMNFR1).
  • Default each field to the latest API version that the module has been tested against. The default MUST be a stable (non-preview) API version unless the module’s primary resource only ships a preview API.
  • Default the variable itself to {} so consumers only need to supply the keys they wish to override.
  • Be nullable = false.
  • Document each field in the variable’s description, including the resource it controls.

The rationale is to allow consumers to:

  • Target sovereign clouds (e.g., Azure US Government, Azure China) where older API versions may be the latest available.
  • Opt into a newer preview API version without waiting for a module release.
  • Pin a specific API version for compliance or reproducibility reasons.

Parent modules MUST cascade the relevant subset of resource_types to each submodule they instantiate, so that submodule API versions remain consistent with the parent’s chosen versions and a single override at the parent level propagates everywhere.

variable "resource_types" {
  type = object({
    widget = optional(string, "Microsoft.Example/widgets@2024-01-01")
    part   = optional(string, "Microsoft.Example/widgets/parts@2024-01-01")
    lock   = optional(string, "Microsoft.Authorization/locks@2020-05-01")
  })
  default     = {}
  nullable    = false
  description = <<DESCRIPTION
Override the AzAPI `<provider>/<resource>@<api-version>` strings used by this module. Each key defaults to a tested value; supply only the keys you want to override. Useful when targeting a sovereign cloud with older API versions, or when opting into a newer preview API.

- `widget` - The primary resource managed by this module.
- `part`   - Child resources of the primary resource.
- `lock`   - Management lock applied to the primary resource and its private endpoints.
DESCRIPTION
}

resource "azapi_resource" "this" {
  type      = var.resource_types.widget
  name      = var.name
  parent_id = var.parent_id
  body      = { /* ... */ }
}

module "part" {
  source = "./modules/part"

  # Cascade the relevant subset to the submodule.
  resource_types = {
    this = var.resource_types.part
  }

  # ...other arguments...
}



See origin...

ID: TFFR7 - Category: Inputs/Outputs - AzAPI - retry and timeouts variables

The retry and timeouts blocks of every azapi_resource declared by the module MUST be configurable by the consumer. Authors MUST NOT hard-code values inline that the consumer cannot override.

To meet this requirement, the module MUST expose two variables:

  • retry — an object variable controlling the AzAPI retry block.
  • timeouts — an object variable controlling the AzAPI timeouts block.

Both variables:

  • MAY define module-level defaults (e.g., a default error_message_regex such as "ScopeLocked" for resources that race with lock removal, or a default delete = "5m").
  • MUST allow the consumer to override the defaults — either by supplying a non-null value at the variable level, or by allowing per-field overrides through optional(...) attributes.
  • MUST be applied to every azapi_resource (and equivalent AzAPI resources) declared by the module.
  • MUST cascade to submodules — the parent module’s retry and timeouts values MUST be passed through to each submodule it instantiates (see TFRMNFR1). Submodules MAY additionally expose per-item overrides for cases where individual resources need different settings.
variable "retry" {
  type = object({
    error_message_regex  = optional(list(string))
    interval_seconds     = optional(number)
    max_interval_seconds = optional(number)
  })
  default     = null
  description = <<DESCRIPTION
Retry configuration applied to every `azapi` resource managed by the module (root resource and all submodules). Defaults to `null` (no custom retry).

- `error_message_regex`  - (Optional) A list of regex patterns matching error messages that trigger a retry.
- `interval_seconds`     - (Optional) Initial interval between retries in seconds.
- `max_interval_seconds` - (Optional) Maximum interval between retries in seconds.

See <https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/resource#retry> for full semantics.
DESCRIPTION
}

variable "timeouts" {
  type = object({
    create = optional(string)
    read   = optional(string)
    update = optional(string)
    delete = optional(string)
  })
  default     = null
  description = <<DESCRIPTION
Default per-operation timeouts applied to every `azapi` resource managed by the module. Defaults to `null` (provider defaults). Each value is a Go duration string (e.g. `30m`, `1h`).

- `create` - (Optional) Timeout for create operations.
- `read`   - (Optional) Timeout for read operations.
- `update` - (Optional) Timeout for update operations.
- `delete` - (Optional) Timeout for delete operations.
DESCRIPTION
}

resource "azapi_resource" "this" {
  type      = var.resource_types.this
  name      = var.name
  parent_id = var.parent_id
  body      = { /* ... */ }

  # `retry` is an attribute on `azapi_resource`, so the variable can be
  # assigned directly. `timeouts` is a block, so a `dynamic "timeouts"`
  # block is required to honor the variable's `null` default.
  retry = var.retry

  dynamic "timeouts" {
    for_each = var.timeouts == null ? [] : [var.timeouts]
    content {
      create = timeouts.value.create
      read   = timeouts.value.read
      update = timeouts.value.update
      delete = timeouts.value.delete
    }
  }

  response_export_values = []
}

module "child" {
  source = "./modules/child"

  # Cascade retry and timeouts to the submodule.
  retry    = var.retry
  timeouts = var.timeouts

  # ...other arguments...
}



See origin...

ID: TFNFR14 - Category: Inputs - Not allowed variables

Since Terraform 0.13, count, for_each and depends_on are introduced for modules, module development is significantly simplified. Module’s owners MUST NOT add variables like enabled or module_depends_on to control the entire module’s operation. Boolean feature toggles are acceptable however.




See origin...

ID: TFNFR38 - Category: Inputs/Outputs - Resource ID Variable Validation

Every input variable (or nested attribute) that holds an Azure ARM resource ID MUST be validated using the AzAPI provider-defined function provider::azapi::parse_resource_id, called with a literal string naming the expected resource type, and wrapped in can(...).

Hand-rolled regex, startswith, length, or split checks MUST NOT be used to validate resource IDs. The provider function knows the canonical ARM ID grammar for every resource type, is fixed in lockstep with the provider, and produces a single consistent error model — including for IDs whose grammar contains anomalies (such as classic resources, extension resources, or scope-based IDs).

This rule covers, but is not limited to:

  • Top-level scope variables such as parent_id (see TFRMFR1).
  • Variables that reference other Azure resources by ID (e.g. subnet_resource_id, key_vault_resource_id, workspace_resource_id, private_dns_zone_resource_ids, user_assigned_resource_ids).
  • Nested attributes inside object, map(object), set(object), or list(object) types that hold resource IDs.

Rules

  • The resource type passed to parse_resource_id MUST be a literal string (e.g. "Microsoft.Network/virtualNetworks/subnets"). It MUST NOT be a reference to another variable, local, or expression. This keeps each validation block self-contained and avoids requiring cross-variable validation.
  • For optional / nullable variables, the validation MUST short-circuit on null (e.g. var.x == null || can(provider::azapi::parse_resource_id("...", var.x))) so that callers omitting the value do not trip validation.
  • For collection-valued variables (set(string), list(string), map(string)), the validation MUST iterate the collection with alltrue([for v in ... : can(...)]).
  • For nested attributes within object types, the validation MUST iterate the parent collection (or reference the object directly) and validate each nested resource ID, again handling null for optional nested attributes.
  • Where a variable can legitimately hold IDs of more than one resource type (rare — e.g. marketplace_partner_resource_id in the diagnostic-settings interface), this rule does not apply and the variable SHOULD be left without resource-ID validation rather than validated against a single arbitrary type.

Examples

A required, single-value resource ID:

variable "key_vault_resource_id" {
  type     = string
  nullable = false

  validation {
    condition     = can(provider::azapi::parse_resource_id("Microsoft.KeyVault/vaults", var.key_vault_resource_id))
    error_message = "`key_vault_resource_id` must be a valid Azure Key Vault resource ID."
  }

  description = "The resource ID of the Key Vault that holds the customer-managed key."
}

An optional, single-value resource ID:

variable "workspace_resource_id" {
  type     = string
  default  = null
  nullable = true

  validation {
    condition     = var.workspace_resource_id == null || can(provider::azapi::parse_resource_id("Microsoft.OperationalInsights/workspaces", var.workspace_resource_id))
    error_message = "`workspace_resource_id` must be a valid Log Analytics workspace resource ID, or `null`."
  }

  description = "The resource ID of the Log Analytics workspace to send diagnostics to."
}

A collection of resource IDs:

variable "user_assigned_resource_ids" {
  type     = set(string)
  default  = []
  nullable = false

  validation {
    condition = alltrue([
      for id in var.user_assigned_resource_ids :
      can(provider::azapi::parse_resource_id("Microsoft.ManagedIdentity/userAssignedIdentities", id))
    ])
    error_message = "Each entry in `user_assigned_resource_ids` must be a valid user-assigned managed identity resource ID."
  }

  description = "A set of user-assigned managed identity resource IDs to attach to the resource."
}

A nested resource ID inside a map(object(...)):

variable "private_endpoints" {
  type = map(object({
    subnet_resource_id            = string
    private_dns_zone_resource_ids = optional(set(string), [])
    # ...other attributes...
  }))
  default  = {}
  nullable = false

  validation {
    condition = alltrue([
      for _, v in var.private_endpoints :
      can(provider::azapi::parse_resource_id("Microsoft.Network/virtualNetworks/subnets", v.subnet_resource_id))
    ])
    error_message = "Each `private_endpoints[*].subnet_resource_id` must be a valid subnet resource ID."
  }

  validation {
    condition = alltrue(flatten([
      for _, v in var.private_endpoints : [
        for id in v.private_dns_zone_resource_ids :
        can(provider::azapi::parse_resource_id("Microsoft.Network/privateDnsZones", id))
      ]
    ]))
    error_message = "Each entry in `private_endpoints[*].private_dns_zone_resource_ids` must be a valid private DNS zone resource ID."
  }
}

Notes

  • The rule applies regardless of whether the resource ID is required or optional, single-valued or collection-valued, top-level or nested.
  • parse_resource_id errors when (a) the input is not a well-formed ARM ID, or (b) the input does not parse as the supplied resource type. Wrapping in can(...) converts both failure modes into a single boolean suitable for a validation block’s condition.
  • This rule supersedes any older guidance suggesting startswith(var.x, "/") or hand-written regex for resource ID validation.



See origin...

ID: TFRMFR1 - Category: Inputs/Outputs - Resource Module Parent ID

A Terraform resource module MUST expose its parent scope to consumers as a single string variable named parent_id, and MUST assign that variable to the parent_id argument of every primary azapi_resource (or equivalent AzAPI resource) it manages.

parent_id is the AzAPI provider’s universal way of expressing where a resource lives in the Azure Resource Manager hierarchy. Depending on the resource type, it can be:

  • A subscription ID (e.g. /subscriptions/{subscriptionId}) — for tenant- or subscription-scoped resources.
  • A management group ID (e.g. /providers/Microsoft.Management/managementGroups/{name}) — for management-group-scoped resources.
  • A resource group ID (e.g. /subscriptions/{subscriptionId}/resourceGroups/{rgName}) — for the most common case of resources that live inside a resource group.
  • The resource ID of a parent ARM resource (e.g. the ID of a virtual network for subnets, the ID of a storage account for blob containers) — for child / nested resources.

Because the same variable describes every possible parent scope, modules MUST NOT expose resource_group_name, resource_group_resource_id, or any other parent-scope-specific variable. The fully-qualified ARM ID supplied via parent_id is sufficient and works uniformly for every kind of Azure resource.

parent_id MUST be validated using the AzAPI provider’s provider-defined functions, per TFNFR38. The required function is provider::azapi::parse_resource_id, called with the expected parent resource type for the module’s primary resource (for example Microsoft.Resources/resourceGroups for resources that live inside a resource group, or Microsoft.Network/virtualNetworks for a subnet module). Hand-rolled regex, startswith, or length checks MUST NOT be used.

This rule supersedes the Terraform clause of RMFR3 (which historically required a resource_group_name variable in Terraform). RMFR3 still applies to Bicep modules; for AVM Terraform modules the rules in this spec take precedence.

Variable declaration

variable "parent_id" {
  type     = string
  nullable = false

  validation {
    # Validate via the AzAPI provider's `parse_resource_id` function. The function
    # errors if `parent_id` is malformed OR if it does not parse as the expected
    # parent resource type (e.g. passing a subscription ID where a resource group
    # is required). Replace `Microsoft.Resources/resourceGroups` with the parent
    # resource type expected by this module's primary resource (for example
    # `Microsoft.Network/virtualNetworks` for a subnet module).
    condition     = can(provider::azapi::parse_resource_id("Microsoft.Resources/resourceGroups", var.parent_id))
    error_message = "`parent_id` must be a valid Azure resource group resource ID."
  }

  description = <<DESCRIPTION
The fully-qualified ARM resource ID of the scope into which the resource managed by this module will be deployed. Examples:

- Subscription scope:        `/subscriptions/00000000-0000-0000-0000-000000000000`
- Management group scope:    `/providers/Microsoft.Management/managementGroups/example-mg`
- Resource group scope:      `/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/example-rg`
- Parent resource scope:     `/subscriptions/.../resourceGroups/example-rg/providers/Microsoft.Network/virtualNetworks/example-vnet`

This module **does not** create the parent scope. The consumer (or composing pattern module) is responsible for providing a `parent_id` for an existing scope.
DESCRIPTION
}

Use in the resource block

resource "azapi_resource" "this" {
  type      = var.resource_types.this
  name      = var.name
  parent_id = var.parent_id
  body      = { /* ... */ }

  response_export_values = []
}

Notes

  • parent_id MUST be of type string, MUST be required (no default), and MUST be validated using provider::azapi::parse_resource_id wrapped in can(...), per TFNFR38.
  • The resource type passed to parse_resource_id MUST be a literal string naming the expected parent resource type for the module’s primary resource (e.g. "Microsoft.Resources/resourceGroups" for a resource that lives inside a resource group, or "Microsoft.Network/virtualNetworks" for a subnet module). It MUST NOT be a reference to another variable. This keeps the validation block self-contained.
  • Modules MUST NOT accept resource_group_name, resource_group_resource_id, or any other parent-scope-specific variable. If a module needs to be told which resource group (or subscription, or management group) to deploy into, it does so exclusively via parent_id.
  • Modules MUST NOT create the parent scope themselves (see RMFR3 for the resource-group case). The consumer or composing pattern module supplies an existing scope’s ARM ID.
  • Submodules (per TFRMNFR1) MUST also expose parent_id and follow the same rules. The parent module typically passes its own primary resource’s ID to each child, e.g. parent_id = azapi_resource.this.id.
  • Modules MAY expose additional, narrower scope variables only when a single resource genuinely needs two different parent scopes (rare). In that case the additional variable MUST still be a parent_id-shaped string (fully-qualified ARM ID), validated with the same provider-defined function pattern, and MUST NOT be named after a specific scope kind such as resource_group_name.

Exception — extension-resource modules

A small class of resource modules manages an Azure extension resource (a resource type that attaches to any parent ARM resource, regardless of its provider). Examples include modules whose primary resource is Microsoft.Authorization/locks, Microsoft.Authorization/roleAssignments, Microsoft.Insights/diagnosticSettings, Microsoft.Resources/tags, or similar. For these modules, the parent resource type is intentionally polymorphic and a literal parse_resource_id("Microsoft.X/y", var.parent_id) validation MUST NOT be used.

Where this exception applies, the module MUST still:

  • Expose the parent scope as the variable named parent_id (no other name), of type string, required, and nullable = false.

  • Validate that parent_id is a non-empty fully-qualified ARM ID using a generic check, e.g.:

    validation {
      condition     = length(var.parent_id) > 0 && (startswith(var.parent_id, "/subscriptions/") || startswith(var.parent_id, "/providers/"))
      error_message = "`parent_id` must be a fully-qualified ARM resource ID starting with `/subscriptions/` or `/providers/`."
    }
  • Document in the variable’s description that any ARM resource ID is accepted because the module manages an extension resource.

  • Document the exception in the module’s README.md so reviewers immediately understand why the standard parse_resource_id validation is absent.




Testing

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR1Prescribed TestsMUSTOwnerContributorBAU
2SNFR2E2E TestingMUSTOwnerContributorBAU
3SNFR3AVM Compliance TestsMUSTOwnerContributorInitial
4SNFR4Unit TestsSHOULDOwnerContributorBAU
5SNFR5Upgrade TestsSHOULDOwnerContributorBAU
6SNFR6Static Analysis/Linting TestsMUSTOwnerContributorBAU
7SNFR7Idempotency TestsMUSTOwnerContributorBAU
8SNFR24Testing Child, Extension & Interface ResourcesMUSTOwnerContributorBAU
9TFNFR5Test ToolingMUSTOwnerContributorBAU
10TFNFR15Variable Definition OrderSHOULDOwnerContributorBAU
➕ See Specifications for this category
See origin...

ID: SNFR1 - Category: Testing - Prescribed Tests

Modules MUST use the prescribed tooling and testing frameworks defined in the language specific specs.




See origin...

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

To see a directory and file structure for a module, see the language specific contribution guide.

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.
➕ 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.

Details for use in E2E tests

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>).




See origin...

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/avm-core-team-technical-bicep, OR @Azure/avm-core-team-technical-terraform.




See origin...

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.




See origin...

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.




See origin...

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.




See origin...

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.




See origin...

ID: SNFR24 - Category: Testing - Testing Child, Extension & Interface Resources

Module owners MUST test that child and extension resources and those Bicep or Terreform interface resources that are supported by their modules, are validated in E2E tests as per SNFR2 to ensure they deploy and are configured correctly.

These MAY be tested in a separate E2E test and DO NOT have to be tested in each E2E test.




See origin...

ID: TFNFR5 - Category: Testing - Test Tooling

Module owners MUST use the below test script for unit/linting/static/security analysis tests.

  • ./avm pr-check



See origin...

ID: TFNFR15 - Category: Code Style - Variable Definition Order

Input variables SHOULD follow this order:

  1. All required fields, in alphabetical order
  2. All optional fields, in alphabetical order

A variable without default value is a required field, otherwise it’s an optional one.




Documentation

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR15Automatic Documentation GenerationMUSTOwnerContributorBAU
2SNFR16Examples/E2EMUSTOwnerContributorBAU
3TFNFR1DescriptionsMUSTOwnerContributorBAU
4TFNFR2Module Documentation GenerationMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

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.




See origin...

ID: SNFR16 - Category: Documentation - Examples/E2E

An examples/e2e directory MUST exist to provide named scenarios for module deployment.




See origin...

ID: TFNFR1 - Category: Documentation - Descriptions

Where descriptions for variables and outputs spans multiple lines. The description MAY provide variable input examples for each variable using the HEREDOC format and embedded markdown.

Example:

  variable "my_complex_input" {
    type = map(object({
      param1 = string
      param2 = optional(number, null)
    }))
    description = <<DESCRIPTION
  A complex input variable that is a map of objects.
  Each object has two attributes:
  
  - `param1`: A required string parameter.
  - `param2`: (Optional) An optional number parameter.
  
  Example Input:
  
  ```terraform
  my_complex_input = {
    "object1" = {
      param1 = "value1"
      param2 = 2
    }
    "object2" = {
      param1 = "value2"
    }
  }
  ```
  DESCRIPTION
  }
  



See origin...

ID: TFNFR2 - Category: Documentation - Module Documentation Generation

Terraform modules documentation MUST be automatically generated via Terraform Docs.

A file called .terraform-docs.yml MUST be present in the root of the module and have the following content:

  ---
  ### To generate the output file to partially incorporate in the README.md,
  ### Execute this command in the Terraform module's code folder:
  # terraform-docs -c .terraform-docs.yml .
  
  formatter: "markdown document" # this is required
  
  version: "0.16.0"
  
  header-from: "_header.md"
  footer-from: "_footer.md"
  
  recursive:
    enabled: false
    path: modules
  
  sections:
    hide: []
    show: []
  
  content: |-
    {{ .Header }}    
  
    <!-- markdownlint-disable MD033 -->
    {{ .Requirements }}
  
    {{ .Providers }}
  
    {{ .Resources }}
  
    <!-- markdownlint-disable MD013 -->
    {{ .Inputs }}
  
    {{ .Outputs }}
  
    {{ .Modules }}
  
    {{ .Footer }}
  
  output:
    file: README.md
    mode: replace
    template: |-
      <!-- BEGIN_TF_DOCS -->
      {{ .Content }}
      <!-- END_TF_DOCS -->      
  output-values:
    enabled: false
    from: ""
  
  sort:
    enabled: true
    by: required
  
  settings:
    anchor: true
    color: true
    default: true
    description: false
    escape: true
    hide-empty: false
    html: true
    indent: 2
    lockfile: true
    read-comments: true
    required: true
    sensitive: true
    type: true
  



Release / Publishing

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR17Semantic VersioningMUSTOwnerContributorBAU
2SNFR18Breaking ChangesSHOULDOwnerContributorBAU
3SNFR19Registries TargetedMUSTOwnerContributorBAU
4SNFR21Cross Language CollaborationSHOULDOwnerContributorBAU
➕ See Specifications for this category
See origin...

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 Version
  • Y == Minor Version
  • Z == Patch Version

Module versioning before first Major version release 1.0.0

  • Initially modules MUST be released as version 0.1.0 and 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 version 1.0.0.

    Note

    Releasing as version 0.1.0 initially 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.0 is released, given a version number X.Y.Z:

    • X Major version MUST NOT be bumped.
    • Y Minor version MUST be bumped when introducing breaking changes (which would normally bump Major after 1.0.0 release) or feature updates (same as it will be after 1.0.0 release).
    • Z Patch version MUST be bumped when introducing non-breaking, backward compatible bug fixes (same as it will be after 1.0.0 release).



See origin...

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.




See origin...

ID: SNFR19 - Category: Publishing - Registries Targeted

Modules MUST be published to their respective language public registries.

Tip

See the language specific contribution guides for detailed guidance and sample code to use in AVM modules to achieve this requirement.




See origin...

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.




Terraform Utility Module Specifications

Contribution / Support

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR8Module Owner(s) GitHubMUSTOwnerInitial
2SNFR20GitHub Teams OnlyMUSTOwnerInitial
3SNFR9AVM & PG Teams GitHub Repo PermissionsMUSTOwnerInitial
4SNFR10MIT LicensingMUSTOwnerInitial
5SNFR11Issues Response TimesMUSTOwnerContributorBAU
6SNFR12Versions SupportedMUSTOwnerBAU
7SNFR23GitHub Repo LabelsMUSTOwnerBAU
8TFNFR3GitHub Repo Branch ProtectionMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

ID: SNFR8 - Category: Contribution/Support - Module Owner(s) GitHub

A module MUST have an owner that is defined and managed by a GitHub Team in the Azure GitHub organization.

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

The names for the GitHub teams for each approved module are already defined in the respective Module Indexes. These teams MUST be created (and used) for each module.




See origin...

ID: SNFR20 - Category: Contribution/Support - GitHub Teams Only

All GitHub repositories that AVM module are published from and hosted within MUST only assign GitHub repository permissions to GitHub teams only.

Each module MUST have a GitHub team assigned for module owners. This team MUST be created in the Azure organization in GitHub.

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

Important

As part of the module proposal process, the name of the GitHub team for each approved module is already defined in the respective Module Indexes (or CSV file). This team MUST be created (and used) for each module.

Module owners don’t need to construct the name of the GitHub team for their module themselves, instead they need use the name prescribed in the related CSV file, at the time of approval.

For a direct link, see the list of related index pages:

The @Azure prefix in the last column of the tables linked above represents the “Azure” GitHub organization all AVM-related repositories exist in. DO NOT include this segment in the team’s name!

Naming Convention

The naming convention for the GitHub teams MUST follow the below pattern:

  • <hyphenated module name>-module-owners-bicep - to grant permissions for module owners on Bicep modules

Segments:

  • <hyphenated module name> == the AVM Module’s name, with each segment separated by dashes, i.e., avm-res-<resource provider>-<ARM resource type>
    • See RMNFR1 for AVM Resource Module Naming
    • See PMNFR1 for AVM Pattern Module Naming
  • module-owners == the role the GitHub Team is assigned to
  • <bicep == the language the module is written in

Examples:

  • avm-res-compute-virtualmachine-module-owners-bicep
Note

The naming convention for Bicep modules is slightly different than the naming convention for their respective GitHub teams.

Add Team Members

All officially documented module owner(s) MUST be added to the -module-owners- team. The -module-owners- team MUST NOT have any other members.

Unless explicitly requested and agreed, members of the AVM core team or any PG teams MUST NOT be added to the -module-owners- teams as permissions for them are granted through the teams described in SNFR9.

Grant permissions through team memberships

Note

In case of Bicep modules, permissions to the BRM repository (the repo of the Bicep Registry) are granted via assigning the -module-owners- teams to parent teams that already have the required level access configured. While it is the module owner’s responsibility to initiate the addition of their team to the respective parent, only the AVM core team can approve this parent-child relationship.

Module owners MUST create their -module-owners- team and as part of the provisioning process, they MUST request the addition of this team to its respective parent team (see the table below for details).

GitHub Team NameDescriptionPermissionsPermissions granted throughWhere to work?
<hyphenated module name>-module-owners-bicepAVM Bicep Module Owners - <module name>WriteAssignment to the avm-technical-reviewers-bicep parent team.Need to work in a fork.

Example - GitHub team required for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

  • avm-res-network-virtualnetwork-module-owners-bicep –> assign to the avm-technical-reviewers-bicep parent team.
Tip

Direct link to create a new GitHub team and assign it to its parent: Create new team

Fill in the values as follows:

  • Team name: Following the naming convention described above, use the value defined in the module indexes.
  • Description: Follow the guidance above (see the Description column in the table above).
  • Parent team: Follow the guidance above (see the Permissions granted through column in the table above).
  • Team visibility: Visible
  • Team notifications: Enabled

CODEOWNERS file

As part of the “initial Pull Request” (that publishes the first version of the module), module owners MUST add an entry to the CODEOWNERS file in the BRM repository (here).

Note

Through this approach, the AVM core team will grant review permission to module owners as part of the standard PR review process.

Every CODEOWNERS entry (line) MUST include the following segments separated by a single whitespace character:

  • Path of the module, relative to the repo’s root, e.g.: /avm/res/network/virtual-network/
  • The -module-owners-team, with the @Azure/ prefix, e.g., @Azure/avm-res-network-virtualnetwork-module-owners-bicep
  • The GitHub team of the AVM Bicep reviewers, with the @Azure/ prefix, i.e., @Azure/avm-module-reviewers-bicep

Example - CODEOWNERS entry for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network):

  • /avm/res/network/virtual-network/ @Azure/avm-res-network-virtualnetwork-module-owners-bicep @Azure/avm-module-reviewers-bicep

Terraform

Note

Access management for Terraform repositories now uses a single team, membership of which is managed using an internal entitlement management tool (Core Identity).

All module owners MUST request access to the avm-module-owners-terraform GitHub team via the Azure Verified Module Owners Terraform entitlement in Core Identity (Microsoft internal tool).




See origin...

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

Note

These required GitHub teams are already associated to the BRM repository and have the required permissions.

Terraform

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.




See origin...

ID: SNFR10 - Category: Contribution/Support - MIT Licensing

A module MUST be published with the MIT License in the Azure GitHub organization.




See origin...

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.




See origin...

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.




See origin...

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:

➕ AVM Standard GitHub Labels

These labels are available in a CSV file from here

NameDescriptionHEX
AZD 🧑‍💻These modules are requested/used by the AZD team.
E0BFFA
Needs: Attention 👋Reply has been added to issue, maintainer to review
E99695
Needs: Immediate Attention ‼️Immediate attention of module owner / AVM team is needed
FF0000
Needs: Author Feedback 👂Awaiting feedback from the issue/PR author
F18A07
Needs: External Changes ⚒️When an issue/PR requires changes that are outside of the control of the module. e.g. to an RP.
DE389D
Needs: More Evidence ⚖We are looking for more evidence to make a decision on this
F64872
Needs: Triage 🔍Maintainers need to triage still
FBCA04
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.
FF0019
Needs: Module Contributor 📣This module needs secondary owner(s) or contributor(s) to develop or maintain it
C95474
Needs: Core Team 🧞‍♂️This item needs the AVM Core Team to review it
DB4503
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
800080
Status: Do Not Merge ⛔Do not merge PRs with this label attached as they are not ready or aligned to future direction etc.
8B4513
Status: External Contribution 🌍This is being worked on by someone outside of the AVM module owners/contributors or AVM core team
D8FA2C
Status: Fixed ✅Auto label applied when issue fixed by merged PR
90EE90
Status: Help Wanted 🆘Extra attention is needed
FF4500
Status: In Triage 🔍Picked up for triaging by an AVM core team member
D4AF37
Status: In PR 👉This is when an issue is due to be fixed in an open PR
EDEDED
Status: Invalid ❌This doesn't seem right
E4E669
Status: Long Term ⏳We will do it, but will take a longer amount of time due to complexity/priorities
B60205
Status: No Recent Activity 💤When an issue/PR has not been modified for X amount of days
808080
Status: Won't Fix 💔This will not be worked on
FFFFFF
Status: Owners Identified 🤘This module has its owners identified
FBEF2A
Status: Module Available 🟢The module is published
C8E6C9
Status: Module Deprecated 🔴This is a request to deprecate a module
000000
Status: Module Orphaned 🟡The module has no owner and is therefore orphaned at this time
F4A460
Status: Ready For Repository Creation 📝This module is approved and the owner is ready for the repository to be created (Terraform)
136A41
Status: Repository Created 📄This module has had it's repository created and configured ready for owner contribution (Terraform)
27AB03
Status: Response Overdue 🚩When an issue/PR has not been responded to for X amount of days
850000
Status: Looking For Assistance 🦆This item is looking for anyone to help develop the code and submit a PR for resolution
03FCC2
Type: Bug 🐛Something isn't working
D73A4A
Type: CI 🚀This issue is related to the AVM CI
74CFB0
Type: Documentation 📄Improvements or additions to documentation
0075CA
Type: Duplicate 🤲This issue or pull request already exists
CFD3D7
Type: Feature Request ➕New feature or request
A2EEEF
Type: Hygiene 🧹things related to testing, issue triage etc.
17016A
Type: New Module Proposal 💡A new module for AVM is being proposed
ADD8E6
Type: Question/Feedback 🙋‍♀️Further information is requested or just some feedback
CB6BA2
Type: Security Bug 🔒This is a security bug
FFFF00
Type: AVM 🅰️ ✌️ ⓜ️This is an AVM related issue
F0FFFF
Language: Terraform 🌐This is related to the Terraform IaC language
7740B6
Language: Bicep 💪This is related to the Bicep IaC language
1D73B3
Class: Resource Module 📦This is a resource module
D3D3D3
Class: Pattern Module 📦This is a pattern module
A9A9A9
Class: Utility Module 📦This is a utility module
CAD1DE
Class: Child Module 📦This is a child module
5E5186

To help apply these to a module GitHub repository you can use the below PowerShell script:

➕ Set-AvmGitHubLabels.ps1

For most scenario this is the command you’ll need to call the below PowerShell script with, replacing the value for RepositoryName:

  Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -CreateCsvLabelExports $false -NoUserPrompts $true
```shell
# Linux / MacOs
# For Windows replace $PWD with your the local path or your repository
#
docker run -it -v $PWD:/repo -w /repo mcr.microsoft.com/powershell pwsh -Command '
    #Invoke-WebRequest -Uri "https://azure.github.io/Azure-Verified-Modules/scripts/Set-AvmGitHubLabels.ps1" -OutFile "Set-AvmGitHubLabels.ps1"
    $gh_version = "2.44.1"
    Invoke-WebRequest -Uri "https://github.com/cli/cli/releases/download/v2.44.1/gh_2.44.1_linux_amd64.tar.gz" -OutFile "gh_$($gh_version)_linux_amd64.tar.gz"
    apt-get update && apt-get install -y git
    tar -xzf "gh_$($gh_version)_linux_amd64.tar.gz"
    ls -lsa
    mv "gh_$($gh_version)_linux_amd64/bin/gh" /usr/local/bin/
    rm "gh_$($gh_version)_linux_amd64.tar.gz" && rm -rf "gh_$($gh_version)_linux_amd64"
    gh --version
    ls -lsa
    gh auth login
    $OrgProject = "Azure/terraform-azurerm-avm-res-kusto-cluster"
    gh auth status
    ./Set-AvmGitHubLabels.ps1 -RepositoryName $OrgProject -CreateCsvLabelExports $false -NoUserPrompts $true

  '
```

By default this script will only update and append labels on the repository specified. However, this can be changed by setting the parameter -UpdateAndAddLabelsOnly to $false, which will remove all the labels from the repository first and then apply the AVM labels from the CSV only.

Make sure you elevate your privilege to admin level or the labels will not be applied to your repository. Go to repos.opensource.microsoft.com/orgs/Azure/repos/ to request admin access before running the script.

Full Script:

These Set-AvmGitHubLabels.ps1 can be downloaded from here.

  [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Coloured output required in this script")]
  
  <#
  .SYNOPSIS
    This script can be used to create the Azure Verified Modules (AVM) standard GitHub labels to a GitHub repository.
  
  .DESCRIPTION
    This script can be used to create the Azure Verified Modules (AVM) standard GitHub labels to a GitHub repository.
  
    By default, the script will remove all pre-existing labels and apply the AVM labels. However, this can be changed by using the -RemoveExistingLabels parameter and setting it to $false. The tool will also output the labels that exist in the repository before and after the script has run to a CSV file in the current directory, or a directory specified by the -OutputDirectory parameter.
  
    The AVM labels to be created are documented here: TBC
  
  .NOTES
    Please ensure you have specified the GitHub repositry correctly. The script will prompt you to confirm the repository name before proceeding.
  
  .COMPONENT
    You must have the GitHub CLI installed and be authenticated to a GitHub account with access to the repository you are applying the labels to before running this script.
  
  .LINK
    TBC
  
  .Parameter RepositoryName
    The name of the GitHub repository to apply the labels to.
  
  .Parameter RemoveExistingLabels
    If set to $true, the default value, the script will remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels. If set to $false, the script will not remove any pre-existing labels.
  
  .Parameter UpdateAndAddLabelsOnly
    If set to $true, the default value, the script will only update and add labels to the repository specified in -RepositoryName. If set to $false, the script will remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels.
  
  .Parameter OutputDirectory
    The directory to output the pre-existing and post-existing labels to in a CSV file. The default value is the current directory.
  
  .Parameter CreateCsvLabelExports
    If set to $true, the default value, the script will output the pre-existing and post-existing labels to a CSV file in the current directory, or a directory specified by the -OutputDirectory parameter. If set to $false, the script will not output the pre-existing and post-existing labels to a CSV file.
  
  .Parameter GitHubCliLimit
    The maximum number of labels to return from the GitHub CLI. The default value is 999.
  
  .Parameter LabelsToApplyCsvUri
    The URI to the CSV file containing the labels to apply to the GitHub repository. The default value is https://raw.githubusercontent.com/jtracey93/label-source/main/avm-github-labels.csv.
  
  .Parameter NoUserPrompts
    If set to $true, the default value, the script will not prompt the user to confirm they want to remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels. If set to $false, the script will prompt the user to confirm they want to remove all pre-existing labels from the repository specified in -RepositoryName before applying the AVM labels.
  
    This is useful for running the script in automation workflows
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and remove all pre-existing labels.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo"
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -RemoveExistingLabels $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and output the pre-existing and post-existing labels to the directory C:\GitHubLabels.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels"
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and output the pre-existing and post-existing labels to the directory C:\GitHubLabels and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels" -RemoveExistingLabels $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not create the pre-existing and post-existing labels CSV files and do not remove any pre-existing labels, just overwrite any labels that have the same name.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -RemoveExistingLabels $false -CreateCsvLabelExports $false
  
  .EXAMPLE
    Create the AVM labels in the repository Org/MyGitHubRepo and do not create the pre-existing and post-existing labels CSV files and do not remove any pre-existing labels, just overwrite any labels that have the same name. Finally, use a custom CSV file hosted on the internet to create the labels from.
  
    Set-AvmGitHubLabels.ps1 -RepositoryName "Org/MyGitHubRepo" -OutputDirectory "C:\GitHubLabels" -RemoveExistingLabels $false -CreateCsvLabelExports $false -LabelsToApplyCsvUri "https://example.com/csv/avm-github-labels.csv"
  
  #>
  
  #Requires -PSEdition Core
  
  [CmdletBinding()]
  param (
    [Parameter(Mandatory = $true)]
    [string]$RepositoryName,
  
    [Parameter(Mandatory = $false)]
    [bool]$RemoveExistingLabels = $true,
  
    [Parameter(Mandatory = $false)]
    [bool]$UpdateAndAddLabelsOnly = $true,
  
    [Parameter(Mandatory = $false)]
    [bool]$CreateCsvLabelExports = $true,
  
    [Parameter(Mandatory = $false)]
    [string]$OutputDirectory = (Get-Location),
  
    [Parameter(Mandatory = $false)]
    [int]$GitHubCliLimit = 999,
  
    [Parameter(Mandatory = $false)]
    [string]$LabelsToApplyCsvUri = "https://azure.github.io/Azure-Verified-Modules/governance/avm-standard-github-labels.csv",
  
    [Parameter(Mandatory = $false)]
    [bool]$NoUserPrompts = $false
  )
  
  # Check if the GitHub CLI is installed
  $GitHubCliInstalled = Get-Command gh -ErrorAction SilentlyContinue
  if ($null -eq $GitHubCliInstalled) {
    throw "The GitHub CLI is not installed. Please install the GitHub CLI and try again."
  }
  Write-Host "The GitHub CLI is installed..." -ForegroundColor Green
  
  # Check if GitHub CLI is authenticated
  $GitHubCliAuthenticated = gh auth status
  if ($LASTEXITCODE -ne 0) {
    Write-Host $GitHubCliAuthenticated -ForegroundColor Red
    throw "Not authenticated to GitHub. Please authenticate to GitHub using the GitHub CLI, `gh auth login`, and try again."
  }
  Write-Host "Authenticated to GitHub..." -ForegroundColor Green
  
  # Check if GitHub repository name is valid
  $GitHubRepositoryNameValid = $RepositoryName -match "^[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$"
  if ($false -eq $GitHubRepositoryNameValid) {
    throw "The GitHub repository name $RepositoryName is not valid. Please check the repository name and try again. The format must be <OrgName>/<RepoName>"
  }
  
  # List GitHub repository provided and check it exists
  $GitHubRepository = gh repo view $RepositoryName
  if ($LASTEXITCODE -ne 0) {
    Write-Host $GitHubRepository -ForegroundColor Red
    throw "The GitHub repository $RepositoryName does not exist. Please check the repository name and try again."
  }
  Write-Host "The GitHub repository $RepositoryName exists..." -ForegroundColor Green
  
  # PRE - Get the current GitHub repository labels and export to a CSV file in the current directory or where -OutputDirectory specifies if set to a valid directory path and the directory exists or can be created if it does not exist already
  if ($RemoveExistingLabels -or $UpdateAndAddLabelsOnly) {
    Write-Host "Getting the current GitHub repository (pre) labels for $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
  
    if ($null -ne $GitHubRepositoryLabels -and $CreateCsvLabelExports -eq $true) {
      $csvFileNamePathPre = "$OutputDirectory\$($RepositoryName.Replace('/', '_'))-Labels-Pre-$(Get-Date -Format FileDateTime).csv"
      Write-Host "Exporting the current GitHub repository (pre) labels for $RepositoryName to $csvFileNamePathPre" -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | Export-Csv -Path $csvFileNamePathPre -NoTypeInformation
    }
  }
  
  # Remove all pre-existing labels if -RemoveExistingLabels is set to $true and user confirms they want to remove all pre-existing labels
  if ($null -ne $GitHubRepositoryLabels) {
    $GitHubRepositoryLabelsJson = $GitHubRepositoryLabels | ConvertFrom-Json
    if ($RemoveExistingLabels -eq $true -and $NoUserPrompts -eq $false -and $UpdateAndAddLabelsOnly -eq $false) {
      $RemoveExistingLabelsConfirmation = Read-Host "Are you sure you want to remove all $($GitHubRepositoryLabelsJson.Count) pre-existing labels from $($RepositoryName)? (Y/N)"
      if ($RemoveExistingLabelsConfirmation -eq "Y") {
        Write-Host "Removing all pre-existing labels from $RepositoryName..." -ForegroundColor Yellow
        $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
          Write-Host "Removing label $($_.name) from $RepositoryName..." -ForegroundColor DarkRed
          gh label delete -R $RepositoryName $_.name --yes
        }
      }
    }
    if ($RemoveExistingLabels -eq $true -and $NoUserPrompts -eq $true -and $UpdateAndAddLabelsOnly -eq $false) {
      Write-Host "Removing all pre-existing labels from $RepositoryName..." -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
        Write-Host "Removing label $($_.name) from $RepositoryName..." -ForegroundColor DarkRed
        gh label delete -R $RepositoryName $_.name --yes
      }
    }
  }
  if ($null -eq $GitHubRepositoryLabels) {
    Write-Host "No pre-existing labels to remove or not selected to be removed from $RepositoryName..." -ForegroundColor Magenta
  }
  
  # Check LabelsToApplyCsvUri is valid and contains a CSV content
  Write-Host "Checking $LabelsToApplyCsvUri is valid..." -ForegroundColor Yellow
  $LabelsToApplyCsvUriValid = $LabelsToApplyCsvUri -match "^https?://"
  if ($false -eq $LabelsToApplyCsvUriValid) {
    throw "The LabelsToApplyCsvUri $LabelsToApplyCsvUri is not valid. Please check the URI and try again. The format must be a valid URI."
  }
  Write-Host "The LabelsToApplyCsvUri $LabelsToApplyCsvUri is valid..." -ForegroundColor Green
  
  # Create AVM lables from the AVM labels CSV file stored on the web using the convertfrom-csv cmdlet
  $avmLabelsCsv = Invoke-WebRequest -Uri $LabelsToApplyCsvUri | ConvertFrom-Csv
  
  # Check if the AVM labels CSV file contains the following columns: Name, Description, HEX
  $avmLabelsCsvColumns = $avmLabelsCsv | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name
  $avmLabelsCsvColumnsValid = $avmLabelsCsvColumns -contains "Name" -and $avmLabelsCsvColumns -contains "Description" -and $avmLabelsCsvColumns -contains "HEX"
  if ($false -eq $avmLabelsCsvColumnsValid) {
    throw "The labels CSV file does not contain the required columns: Name, Description, HEX. Please check the CSV file and try again. It contains the following columns: $avmLabelsCsvColumns"
  }
  Write-Host "The labels CSV file contains the required columns: Name, Description, HEX" -ForegroundColor Green
  
  # Create the AVM labels in the GitHub repository
  Write-Host "Creating/Updating the $($avmLabelsCsv.Count) AVM labels in $RepositoryName..." -ForegroundColor Yellow
  $avmLabelsCsv | ForEach-Object {
    if ($GitHubRepositoryLabelsJson.name -contains $_.name) {
      Write-Host "The label $($_.name) already exists in $RepositoryName. Updating the label to ensure description and color are consitent..." -ForegroundColor Magenta
      gh label create -R $RepositoryName "$($_.name)" -c $_.HEX -d $($_.Description) --force
    }
    else {
      Write-Host "The label $($_.name) does not exist in $RepositoryName. Creating label $($_.name) in $RepositoryName..." -ForegroundColor Cyan
      gh label create -R $RepositoryName "$($_.Name)" -c $_.HEX -d $($_.Description) --force
    }
  }
  
  # POST - Get the current GitHub repository labels and export to a CSV file in the current directory or where -OutputDirectory specifies if set to a valid directory path and the directory exists or can be created if it does not exist already
  if ($CreateCsvLabelExports -eq $true) {
    Write-Host "Getting the current GitHub repository (post) labels for $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
  
    if ($null -ne $GitHubRepositoryLabels) {
      $csvFileNamePathPre = "$OutputDirectory\$($RepositoryName.Replace('/', '_'))-Labels-Post-$(Get-Date -Format FileDateTime).csv"
      Write-Host "Exporting the current GitHub repository (post) labels for $RepositoryName to $csvFileNamePathPre" -ForegroundColor Yellow
      $GitHubRepositoryLabels | ConvertFrom-Json | Export-Csv -Path $csvFileNamePathPre -NoTypeInformation
    }
  }
  
  # If -RemoveExistingLabels is set to $true and user confirms they want to remove all pre-existing labels check that only the avm labels exist in the repository
  if ($RemoveExistingLabels -eq $true -and ($RemoveExistingLabelsConfirmation -eq "Y" -or $NoUserPrompts -eq $true) -and $UpdateAndAddLabelsOnly -eq $false) {
    Write-Host "Checking that only the AVM labels exist in $RepositoryName..." -ForegroundColor Yellow
    $GitHubRepositoryLabels = gh label list -R $RepositoryName -L $GitHubCliLimit --json name,description,color
    $GitHubRepositoryLabels | ConvertFrom-Json | ForEach-Object {
      if ($avmLabelsCsv.Name -notcontains $_.name) {
        throw "The label $($_.name) exists in $RepositoryName but is not in the CSV file."
      }
    }
    Write-Host "Only the CSV labels exist in $RepositoryName..." -ForegroundColor Green
  }
  
  Write-Host "The CSV labels have been created/updated in $RepositoryName..." -ForegroundColor Green
  



See origin...

ID: TFNFR3 - Category: Contribution/Support - GitHub Repo Branch Protection

Module owners MUST set a branch protection policy on their GitHub Repositories for AVM modules against their default branch, typically main, to do the following:

  1. Requires a Pull Request before merging
  2. Require approval of the most recent reviewable push
  3. Dismiss stale pull request approvals when new commits are pushed
  4. Require linear history
  5. Prevents force pushes
  6. Not allow deletions
  7. Require CODEOWNERS review
  8. Do not allow bypassing the above settings
  9. Above settings MUST also be enforced to administrators
Tip

If you use the template repository as mentioned in the contribution guide, the above will automatically be set.




Telemetry

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SFR3Deployment/Usage TelemetryMUSTOwnerInitial
2SFR4Telemetry Enablement FlexibilityMUSTOwnerInitial
➕ See Specifications for this category
See origin...

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 more details on this requirement, here.)

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.ps1 utility
  • Terraform: Executing the make docs command with the note and header ## Data Collection being placed in the module’s _footer.md beforehand
### 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

We will maintain a set of CSV files in the AVM Central Repo (Azure/Azure-Verified-Modules) with the required TelemetryId prefixes to enable checks to utilize this list to ensure the correct IDs are used. To see the formatted content of these CSV files with additional information, please visit the AVM Module Indexes page.

The value you need to use for your module is defined in the related module index. You can look it up on the index pages for Resource Modules, Pattern Modules and Utility Modules.

The ARM deployment name used for the telemetry MUST follow the pattern and MUST be no longer than 64 characters in length: 46d3xbcp.<res/ptn>.<(short) module name>.<version>.<uniqueness>

  • <res/ptn> == AVM Resource or Pattern Module
  • <(short) module name> == The AVM Module’s, possibly shortened, name including the resource provider and the resource type, without;
    • The prefixes: avm-res-
    • The prefixes: avm-ptn-
  • <version> == The AVM Module’s MAJOR.MINOR version (only) with . (periods) replaced with - (hyphens), to allow simpler splitting of the ARM deployment name
  • <uniqueness> == This section of the ARM deployment name is to be used to ensure uniqueness of the deployment name.
    • This is to cater for the following scenarios:
      • The module is deployed multiple times to the same:
        • Location/Region
        • Scope (Tenant, Management Group,Subscription, Resource Group)
Note

Due to the 64-character length limit of Azure deployment names, the <(short) module name> segment has a length limit of 36 characters, so if the module name is longer than that, it MUST be truncated to 36 characters. If any of the semantic version’s segments are longer than 1 character, it further restricts the number of characters that can be used for naming the module.

An example deployment name for the AVM Virtual Machine Resource Module would be: 46d3xbcp.res.compute-virtualmachine.1-2-3.eum3

An example deployment name for a shortened module name would be: 46d3xbcp.res.desktopvirtualization-appgroup.1-2-3.eum3

Tip

Terraform: Terraform uses a telemetry provider, the configuration of which is the same for every module and is included in the template repo.

General: See the language specific contribution guides for detailed guidance and sample code to use in AVM modules to achieve this requirement.

Terraform

To enable telemetry data collection for Terraform modules, the modtm telemetry provider MUST be used. This lightweight telemetry provider sends telemetry data to Azure Application Insights via a HTTP POST front end service.

The modtm telemetry provider is included in all Terraform modules and is enabled by default through the main.telemetry.tf file being automatically distributed from the template repo.

The modtm provider MUST be listed under the required_providers section in the module’s terraform.tf file using the following entry. This is also validated by the linter.

terraform {
  required_providers {
    # .. other required providers as needed
    modtm = {
      source = "Azure/modtm"
      version = "~> 0.3"
    }
  }
}



See origin...

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

Currently, no further requirements apply.




Naming / Composition

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SFR1Preview ServicesMUSTOwnerBAU
2SFR2WAF AlignedSHOULDOwnerBAU
3SNFR25Resource NamingMUSTOwnerInitial
4UMNFR1Module NamingMUSTOwnerInitial
5TFFR1Cross-Referencing ModulesMUSTOwnerContributorBAU
6TFFR3Providers - Permitted VersionsMUSTOwnerContributorBAU
7TFFR4AzAPI - response_export_valuesMUSTOwnerContributorBAU
8TFFR5AzAPI - replace_triggers_refsMUSTOwnerContributorBAU
9TFNFR4Lower snake_casingMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

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., is 2021-05-01-preview
    • Otherwise the latest “non-preview” version of the API SHOULD be used

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”



See origin...

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:

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.




See origin...

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.




See origin...

ID: UMNFR1 - Category: Naming - Module Naming

Utility Modules MUST follow the below naming conventions (all lower case).

Important

As part of the module proposal process, the module’s approved name is captured both in the module proposal issue AND the related module index page (backed by the corresponding CSV file).

Therefore, module owners don’t need to construct the module’s name themselves, instead they need use the name prescribed in the module proposal issue or in the related CSV file, at the time of approval.

Bicep Utility Module Naming

  • Naming convention: avm/utl/<hyphenated grouping/category name>/<hyphenated utility module name>
  • Example: avm/utl/general/get-environment or avm/utl/types/avm-common-types
  • Segments:
    • utl defines 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: general or types
    • <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-finder or avm-utl-naming
  • Segments:
    • <provider> is a legacy requirement of the Terraform registry. For AVM Terraform utility modules this MUST be set to azure (for example Azure/avm-utl-naming/azure). Older utility modules may still use the azurerm or azuread segments.
    • utl defines 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.



See origin...

ID: TFFR1 - Category: Composition - Cross-Referencing Modules

Module owners MAY cross-references other modules to build either Resource or Pattern modules. However, they MUST be referenced only by a HashiCorp Terraform registry reference to a pinned version e.g.,

module "other-module" {
  source  = "Azure/xxx/azure"
  version = "1.2.3"
}

They MUST NOT use git reference to a module.

module "other-module" {
  source = "git::https://xxx.yyy/xxx.git"
}
module "other-module" {
  source = "github.com/xxx/yyy"
}

Modules MUST NOT contain references to non-AVM modules.

Tip

See Module Sources for more information.




See origin...

ID: TFFR3 - Category: Providers - Permitted Versions

Authors MUST only use the following Azure providers, and versions, in their modules:

providermin versionmax version
Azure/azapi>= 2.0< 3.0

The AzureRM provider MUST NOT be used, except where the narrow exception below applies.

Exception — AzureRM for resources with no AzAPI equivalent

An AVM Terraform module MAY declare the AzureRM provider only for resources whose functionality is genuinely unavailable through any AzAPI resource — that is, where there is no equivalent in azapi_resource, azapi_data_plane_resource, azapi_resource_action, or azapi_update_resource. In practice this is limited to a small set of edge cases, most commonly data-plane operations such as Key Vault secrets and certificates, Storage blobs, and a handful of resources whose azurerm_* implementation calls non-ARM APIs.

Where this exception applies the module MUST:

  • Pin the AzureRM provider to ~> 4.0 in required_providers.

  • Use AzAPI for every resource that has an AzAPI equivalent. AzureRM MUST NOT be used as a convenience alternative to AzAPI.

  • Document the exception in the module’s README.md, listing each azurerm_* resource used, the data-plane / non-ARM API it wraps, why no AzAPI equivalent exists today, and the upstream AzAPI issue or PR tracking the eventual replacement.

  • Replace each azurerm_* resource with its AzAPI equivalent as soon as one becomes available, in the next module release after the AzAPI capability ships.

  • Add the following TFLint exclusion (only required because the AzureRM provider is otherwise blocked by AVM tooling):

    rule "provider_azurerm_disallowed" {
      enabled = false
    }

This exception MUST NOT be used to:

  • Avoid migrating an existing AzureRM resource that does have an AzAPI equivalent.
  • Reduce author effort where the AzAPI body schema is more verbose than the AzureRM resource.
  • Side-step any other AzAPI-specific spec (for example TFFR4, TFFR5, TFFR6, or TFFR7) — those rules continue to apply to every AzAPI resource the module declares, regardless of whether the module also uses AzureRM under this exception.

Authors MUST use the required_providers block in their module to enforce the provider versions.

The following is an example.

terraform {
  required_providers {
    # Include one or both providers, as needed
    azapi = {
      source  = "Azure/azapi"
      version = "~> 2.9"
    }
  }
}



See origin...

ID: TFFR4 - Category: Composition - AzAPI - response_export_values

Authors MUST specify the response_export_values argument when using the AzAPI provider:

resource "azapi_resource" "example" {
  type      = "Microsoft.Example/resourceType@2021-01-01"
  name      = "example-resource"
  location  = "West US"
  response_export_values = [] # must be specified, even if empty
  body = {
    properties = {
      exampleProperty = "exampleValue"
    }
  }
}

If you require read-only properties to be returned from the resource, you SHOULD include them as follows:

resource "azapi_resource" "example" {
  type      = "Microsoft.Example/resourceType@2021-01-01"
  name      = "example-resource"
  location  = "West US"
  # Example as a list:
  response_export_values = ["properties.readOnlyProperty"]
  # Example as a map:
  # response_export_values = {
  #   read_only_property = "properties.readOnlyProperty"
  # }
  body = {
    properties = {
      exampleProperty = "exampleValue"
    }
  }
}

output "read_only_property" {
  # Example if response_export_values is a list:
  value = azapi_resource.example.output.properties.readOnlyProperty
  # Example if response_export_values is a map:
  # value = azapi_resource.example.output.read_only_property
}



See origin...

ID: TFFR5 - Category: Composition - AzAPI - replace_triggers_refs

Authors MUST specify the replace_triggers_refs argument when using the AzAPI provider.
The values should contain the body paths that would cause the resource to be replaced when they change.
You do not need to include name, or location, as these already trigger replacement.

This is to ensure that changes to properties that require replacement of the resource are handled correctly by Terraform.

resource "azapi_resource" "example" {
  type      = "Microsoft.Example/resourceType@2021-01-01"
  name      = "example-resource"
  location  = "West US"
  replace_triggers_refs = [
    "properties.exampleProperty"
  ] # must be specified, even if empty
  body = {
    properties = {
      exampleProperty = "exampleValue"
    }
  }
}



See origin...

ID: TFNFR4 - Category: Composition - Code Styling - lower snake_casing

Module owners MUST use lower snake_casing for naming the following:

  • Locals
  • Variables
  • Outputs
  • Resources (symbolic names)
  • Modules (symbolic names)

For example: snake_casing_example (every word in lowercase, with each word separated by an underscore _)




Code Style

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1TFNFR6Resource & Data OrderSHOULDOwnerContributorBAU
2TFNFR7Count & for_each UseMUSTOwnerContributorBAU
3TFNFR8Resource & Data Block OrdersSHOULDOwnerContributorBAU
4TFNFR9Module Block OrderSHOULDOwnerContributorBAU
5TFNFR10No Double Quotes in ignore_changesMUSTOwnerContributorBAU
6TFNFR11Null Comparison ToggleSHOULDOwnerContributorBAU
7TFNFR12Dynamic for Optional Nested ObjectsMUSTOwnerContributorBAU
8TFNFR13Default Values with coalesce/trySHOULDOwnerContributorBAU
9TFNFR16Variable Naming RulesSHOULDOwnerContributorBAU
10TFNFR17Variables with DescriptionsSHOULDOwnerContributorBAU
11TFNFR18Variables with TypesMUSTOwnerContributorBAU
12TFNFR19Sensitive Data VariablesSHOULDOwnerContributorBAU
13TFNFR20Non-Nullable Defaults for collection valuesSHOULDOwnerContributorBAU
14TFNFR21Discourage Nullability by DefaultMUSTOwnerContributorBAU
15TFNFR22Avoid sensitive = falseMUSTOwnerContributorBAU
16TFNFR23Sensitive Default Value ConditionsMUSTOwnerContributorBAU
17TFNFR24Handling Deprecated VariablesMUSTOwnerContributorBAU
18TFNFR25Verified Modules RequirementsMUSTOwnerContributorBAU
19TFNFR26Providers in required_providersMUSTOwnerContributorBAU
20TFNFR27Provider Declarations in ModulesMUSTOwnerContributorBAU
21TFNFR29Sensitive Data OutputsMUSTOwnerContributorBAU
22TFNFR30Handling Deprecated OutputsMUSTOwnerContributorBAU
23TFNFR31locals.tf for Locals OnlyMAYOwnerContributorBAU
25TFNFR33Precise Local TypesSHOULDOwnerContributorBAU
26TFNFR34Using Feature TogglesMUSTOwnerContributorBAU
27TFNFR35Reviewing Potential Breaking ChangesMUSTOwnerContributorBAU
28TFNFR36Setting prevent_deletion_if_contains_resourcesSHOULDOwnerContributorBAU
29TFNFR37Tool Usage by Module OwnerMAYOwnerContributorBAU
30TFNFR39Standard File LayoutMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

ID: TFNFR6 - Category: Code Style - Resource & Data Order

For the definition of resources in the same file, the resources be depended on SHOULD come first, after them are the resources depending on others.

Resources that have dependencies SHOULD be defined close to each other.




See origin...

ID: TFNFR7 - Category: Code Style - count & for_each Use

We can use count and for_each to deploy multiple resources, but the improper use of count can lead to anti pattern.

You can use count to create some kind of resources under certain conditions, for example:

resource "azapi_resource" "network_security_group" {
  count     = local.create_new_security_group ? 1 : 0
  type      = "Microsoft.Network/networkSecurityGroups@2023-11-01"
  name      = coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
  parent_id = var.parent_id
  location  = local.location
  tags      = var.new_network_security_group_tags
  body = {
    properties = {}
  }
  response_export_values = []
}

The module’s owners MUST use map(xxx) or set(xxx) as resource’s for_each collection, the map’s key or set’s element MUST be static literals.

Good example:

resource "azapi_resource" "subnet_pair" {
  for_each  = var.subnet_map // `map(string)`, when user call this module, it could be: `{ "subnet0": "subnet0" }`, or `{ "subnet0": azapi_resource.subnet0.name }`
  type      = "Microsoft.Network/virtualNetworks/subnets@2023-11-01"
  name      = "${each.value}-pair"
  parent_id = azapi_resource.virtual_network.id
  body = {
    properties = {
      addressPrefixes = ["10.0.1.0/24"]
    }
  }
  response_export_values = []
}

Bad example:

resource "azapi_resource" "subnet_pair" {
  for_each  = var.subnet_name_set // `set(string)`, when user use `toset([azapi_resource.subnet0.name])`, it would cause an error.
  type      = "Microsoft.Network/virtualNetworks/subnets@2023-11-01"
  name      = "${each.value}-pair"
  parent_id = azapi_resource.virtual_network.id
  body = {
    properties = {
      addressPrefixes = ["10.0.1.0/24"]
    }
  }
  response_export_values = []
}



See origin...

ID: TFNFR8 - Category: Code Style - Resource & Data Block Orders

There are 3 types of assignment statements in a resource or data block: argument, meta-argument and nested block. The argument assignment statement is a parameter followed by =:

location = azapi_resource.example.location

or:

tags = {
  environment = "Production"
}

Nested block is a assignment statement of parameter followed by {} block:

subnet {
  name           = "subnet1"
  address_prefix = "10.0.1.0/24"
}

Meta-arguments are assignment statements can be declared by all resource or data blocks. They are:

  • count
  • depends_on
  • for_each
  • lifecycle
  • provider

The order of declarations within resource or data blocks is:

All the meta-arguments SHOULD be declared on the top of resource or data blocks in the following order:

  1. provider
  2. count
  3. for_each

Then followed by:

  1. required arguments
  2. optional arguments
  3. required nested blocks
  4. optional nested blocks

All ranked in alphabetical order.

These meta-arguments SHOULD be declared at the bottom of a resource block with the following order:

  1. depends_on
  2. lifecycle

The parameters of lifecycle block SHOULD show up in the following order:

  1. create_before_destroy
  2. ignore_changes
  3. prevent_destroy

parameters under depends_on and ignore_changes are ranked in alphabetical order.

Meta-arguments, arguments and nested blocked are separated by blank lines.

dynamic nested blocks are ranked by the name comes after dynamic, for example:

  dynamic "linux_profile" {
    for_each = var.admin_username == null ? [] : ["linux_profile"]

    content {
      admin_username = var.admin_username

      ssh_key {
        key_data = replace(coalesce(var.public_ssh_key, tls_private_key.ssh[0].public_key_openssh), "\n", "")
      }
    }
  }

This dynamic block will be ranked as a block named linux_profile.

Code within a nested block will also be ranked following the rules above.

PS: You can use avmfix tool to reformat your code automatically.




See origin...

ID: TFNFR9 - Category: Code Style - Module Block Order

The meta-arguments below SHOULD be declared on the top of a module block with the following order:

  1. source
  2. version
  3. count
  4. for_each

blank lines will be used to separate them.

After them will be required arguments, optional arguments, all ranked in alphabetical order.

These meta-arguments below SHOULD be declared on the bottom of a resource block in the following order:

  1. depends_on
  2. providers

Arguments and meta-arguments SHOULD be separated by blank lines.




See origin...

ID: TFNFR10 - Category: Code Style - No Double Quotes in ignore_changes

The ignore_changes attribute MUST NOT be enclosed in double quotes.

Good example:

lifecycle {
    ignore_changes = [
      tags,
    ]
}

Bad example:

lifecycle {
    ignore_changes = [
      "tags",
    ]
}



See origin...

ID: TFNFR11 - Category: Code Style - Null Comparison Toggle

Sometimes we need to ensure that the resources created are compliant to some rules at a minimum extent, for example a subnet has to be connected to at least one network_security_group. The user SHOULD pass in a security_group_id and ask us to make a connection to an existing security_group, or want us to create a new security group.

Intuitively, we will define it like this:

variable "security_group_id" {
  type: string
}

resource "azapi_resource" "network_security_group" {
  count     = var.security_group_id == null ? 1 : 0
  type      = "Microsoft.Network/networkSecurityGroups@2023-11-01"
  name      = coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
  parent_id = var.parent_id
  location  = local.location
  tags      = var.new_network_security_group_tags
  body = {
    properties = {}
  }
  response_export_values = []
}

The disadvantage of this approach is if the user create a security group directly in the root module and use the id as a variable of the module, the expression which determines the value of count will contain an attribute from another resource, the value of this very attribute is “known after apply” at plan stage. Terraform core will not be able to get an exact plan of deployment during the “plan” stage.

You can’t do this:

resource "azapi_resource" "foo" {
  type      = "Microsoft.Network/networkSecurityGroups@2023-11-01"
  name      = "example-nsg"
  parent_id = "/subscriptions/.../resourceGroups/example-rg"
  location  = "eastus"
  body = {
    properties = {}
  }
  response_export_values = []
}

module "bar" {
  source = "xxxx"
  ...
  security_group_id = azapi_resource.foo.id
}

For this kind of parameters, wrapping with object type is RECOMMENDED:

variable "security_group" {
  type: object({
    id   = string
  })
  default     = null
}

The advantage of doing so is encapsulating the value which is “known after apply” in an object, and the object itself can be easily found out if it’s null or not. Since the id of a resource cannot be null, this approach can avoid the situation we are facing in the first example, like the following:

resource "azapi_resource" "foo" {
  type      = "Microsoft.Network/networkSecurityGroups@2023-11-01"
  name      = "example-nsg"
  parent_id = "/subscriptions/.../resourceGroups/example-rg"
  location  = "eastus"
  body = {
    properties = {}
  }
  response_export_values = []
}

module "bar" {
  source = "xxxx"
  ...
  security_group = {
    id = azapi_resource.foo.id
  }
}

This technique SHOULD be used under this use case only.




See origin...

ID: TFNFR12 - Category: Code Style - Dynamic for Optional Nested Objects

An example using AzAPI:

resource "azapi_resource" "main" {
  type      = "Microsoft.ContainerService/managedClusters@2024-09-01"
  name      = var.name
  parent_id = var.parent_id
  location  = var.location
  body = {
    properties = { ... }
  }

  dynamic "identity" {
    for_each = var.client_id == "" || var.client_secret == "" ? [1] : []

    content {
      type         = var.identity_type
      identity_ids = var.user_assigned_identity_ids
    }
  }
  response_export_values = []
}

Please refer to the coding style in the example. Nested blocks under conditions, MUST be declared as:

for_each = <condition> ? [<some_item>] : []



See origin...

ID: TFNFR13 - Category: Code Style - Default Values with coalesce/try

The following example shows how "${var.subnet_name}-nsg" SHOULD be used when var.new_network_security_group_name is null or ""

Good examples:

coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
try(coalesce(var.new_network_security_group.name, "${var.subnet_name}-nsg"), "${var.subnet_name}-nsg")

Bad examples:

var.new_network_security_group_name == null ? "${var.subnet_name}-nsg" : var.new_network_security_group_name)



See origin...

ID: TFNFR16 - Category: Code Style - Variable Naming Rules

The naming of a variable SHOULD follow HashiCorp’s naming rule.

variable used as feature switches SHOULD apply a positive statement, use xxx_enabled instead of xxx_disabled. Avoid double negatives like !xxx_disabled.

Please use xxx_enabled instead of xxx_disabled as name of a variable.




See origin...

ID: TFNFR17 - Category: Code Style - Variables with Descriptions

The target audience of description is the module users.

For a newly created variable (Eg. variable for switching dynamic block on-off), it’s description SHOULD precisely describe the input parameter’s purpose and the expected data type. description SHOULD NOT contain any information for module developers, this kind of information can only exist in code comments.

For object type variable, description can be composed in HEREDOC format:

variable "kubernetes_cluster_key_management_service" {
  type: object({
    key_vault_key_id         = string
    key_vault_network_access = optional(string)
  })
  default     = null
  description = <<DESCRIPTION
- `key_vault_key_id` - (Required) Identifier of Azure Key Vault key. See [key identifier format](https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#vault-name-and-object-name) for more details. When Azure Key Vault key management service is enabled, this field is required and must be a valid key identifier. When `enabled` is `false`, leave the field empty.
- `key_vault_network_access` - (Optional) Network access of the key vault Network access of key vault. The possible values are `Public` and `Private`. `Public` means the key vault allows public access from all networks. `Private` means the key vault disables public access and enables private link. Defaults to `Public`.
DESCRIPTION
}

You MUST remove all trailing whitespace so that terraform-docs renders the readme properly.




See origin...

ID: TFNFR18 - Category: Code Style - Variables with Types

type MUST be defined for every variable. type SHOULD be as precise as possible. Authors SHOULD NOT use any.

  • Use bool instead of string or number for true/false
  • Use string for text
  • Use concrete object instead of map(any)



See origin...

ID: TFNFR19 - Category: Code Style - Sensitive Data Variables

If variable’s type is object and contains one or more fields that would be assigned to a sensitive argument, then this whole variable SHOULD be declared as sensitive = true, otherwise you SHOULD extract sensitive field into separated variable block with sensitive = true.




See origin...

ID: TFNFR20 - Category: Code Style - Non-Nullable Defaults for collection values

Nullable SHOULD be set to false for collection values (e.g. sets, maps, lists) when using them in loops. However for scalar values like string and number, a null value MAY have a semantic meaning and as such these values are allowed.




See origin...

ID: TFNFR21 - Category: Code Style - Discourage Nullability by Default

nullable = true MUST be avoided.

Variables MUST be declared with nullable = false whenever the variable’s type has a meaningful zero value ({} for objects/maps, [] for lists/sets, "" for strings where empty has the same meaning as absent, etc.). Consumers should signal “no value” by omitting the input, not by explicitly passing null.

Exception — behavior-toggle inputs

A small, well-defined class of inputs MAY keep the implicit nullable = true (i.e. default = null) where null carries a distinct semantic meaning of “no override — use the underlying provider/AVM defaults”, and where representing that state with the type’s zero value would be ambiguous or wrong. Examples include:

  • var.retry and var.timeouts (per TFFR7) — null means “do not emit a retry/timeouts block; use the AzAPI provider defaults”.
  • var.lock (per the AVM lock interface) — null means “do not create a management lock”.
  • Optional sub-objects that toggle whole feature blocks on/off, where {} would be indistinguishable from “feature enabled with all defaults”.

Where this exception applies, the variable MUST:

  • Use default = null (the implicit nullable = true is permitted only for this purpose).
  • State explicitly in its description what null means.
  • Be consumed with a null-aware pattern (e.g. count = var.lock != null ? 1 : 0, or dynamic "timeouts" { for_each = var.timeouts == null ? [] : [var.timeouts] }).

This exception does not extend to required inputs, to collection-shaped inputs (TFNFR20), or to nested attributes inside an object — those MUST use nullable = false and the type’s zero value.




See origin...

ID: TFNFR22 - Category: Code Style - Avoid sensitive = false

sensitive = false MUST be avoided.




See origin...

ID: TFNFR23 - Category: Code Style - Sensitive Default Value Conditions

A default value MUST NOT be set for a sensitive input, unless it is an empty collection value.

Good example:

variable "example_map" {
  type        = map(string)
  default     = {}
  description = "An example map variable with an empty default value."
  sensitive   = true
}

Bad example:

variable "example_string" {
  type        = string
  default     = "sensitive_value"
  description = "An example string variable with a sensitive default value."
  sensitive   = true
}



See origin...

ID: TFNFR24 - Category: Code Style - Handling Deprecated Variables

Sometimes we will find names for some variable are not suitable anymore, or a change SHOULD be made to the data type. We want to ensure forward compatibility within a major version, so direct changes are strictly forbidden. The right way to do this is move this variable to an independent deprecated_variables.tf file, then redefine the new parameter in variable.tf and make sure it’s compatible everywhere else.

Deprecated variable MUST be annotated as DEPRECATED at the beginning of the description, at the same time the replacement’s name SHOULD be declared. E.g.,

variable "enable_network_security_group" {
  type        = string
  default     = null
  description = "DEPRECATED, use `network_security_group_enabled` instead; Whether to generate a network security group and assign it to the subnet. Changing this forces a new resource to be created."
}

A cleanup of deprecated_variables.tf SHOULD be performed during a major version release.




See origin...

ID: TFNFR25 - Category: Code Style - Verified Modules Requirements

The terraform.tf file MUST only contain one terraform block.

The first line of the terraform block MUST define a required_version property for the Terraform CLI.

The required_version property MUST include a constraint on the minimum version of the Terraform CLI. Previous releases of the Terraform CLI can have unexpected behavior.

The required_version property MUST include a constraint on the maximum major version of the Terraform CLI. Major version releases of the Terraform CLI can introduce breaking changes and MUST be tested.

The required_version property constraint SHOULD use the ~> #.# or the >= #.#.#, < #.#.# format.

Note: You can read more about Terraform version constraints in the documentation.

Example terraform.tf file:

terraform {
  required_version = "~> 1.6"
  required_providers {
    azapi = {
      source  = "Azure/azapi"
      version = "~> 2.9"
    }
  }
}



See origin...

ID: TFNFR26 - Category: Code Style - Providers in required_providers

The terraform block in terraform.tf MUST contain the required_providers block.

Each provider used directly in the module MUST be specified with the source and version properties. Providers in the required_providers block SHOULD be sorted in alphabetical order.

Do not add providers to the required_providers block that are not directly required by this module. If submodules are used then each submodule SHOULD have its own versions.tf file.

The source property MUST be in the format of namespace/name. If this is not explicitly specified, it can cause failure.

The version property MUST include a constraint on the minimum version of the provider. Older provider versions may not work as expected.

The version property MUST include a constraint on the maximum major version. A provider major version release may introduce breaking change, so updates to the major version constraint for a provider MUST be tested.

The version property constraint SHOULD use the ~> #.# or the >= #.#.#, < #.#.# format.

Note: You can read more about Terraform version constraints in the documentation.

Good examples:

terraform {
  required_version = "~> 1.6"
  required_providers {
    azapi = {
      source  = "Azure/azapi"
      version = "~> 2.9"
    }
  }
}
terraform {
  required_version = ">= 1.6.6, < 2.0.0"
  required_providers {
    azapi = {
      source  = "Azure/azapi"
      version = ">= 2.9.0, < 3.0.0"
    }
  }
}
terraform {
  required_version = ">= 1.6, < 2.0"
  required_providers {
    azapi = {
      source  = "Azure/azapi"
      version = ">= 2.9, < 3.0"
    }
  }
}

Acceptable example (but not recommended):

terraform {
  required_version = "1.6"
  required_providers {
    azapi = {
      source  = "Azure/azapi"
      version = "2.9"
    }
  }
}

Bad example:

terraform {
  required_version = ">= 1.6"
  required_providers {
    azapi = {
      source  = "Azure/azapi"
      version = ">= 2.9"
    }
  }
}



See origin...

ID: TFNFR27 - Category: Code Style - Provider Declarations in Modules

By rules, in the module code provider MUST NOT be declared. The only exception is when the module indeed need different instances of the same kind of provider(Eg. manipulating resources across different locations or accounts), you MUST declare configuration_aliases in terraform.required_providers. See details in this document.

provider block declared in the module MUST only be used to differentiate instances used in resource and data. Declaration of fields other than alias in provider block is strictly forbidden. It could lead to module users unable to utilize count, for_each or depends_on. Configurations of the provider instance SHOULD be passed in by the module users.

Good examples:

In verified module:

terraform {
  required_providers {
    azapi = {
      source                = "Azure/azapi"
      version               = "~> 2.9"
      configuration_aliases = [azapi.alternate]
    }
  }
}

In the root module where we call this verified module:

provider "azapi" {}

provider "azapi" {
  alias = "alternate"
}

module "foo" {
  source = "xxx"
  providers = {
    azapi           = azapi
    azapi.alternate = azapi.alternate
  }
}

Bad example:

In verified module:

provider "azapi" {
  # Configuration options
}



See origin...

ID: TFNFR29 - Category: Code Style - Sensitive Data Outputs

An output block that contains confidential data MUST be declared with sensitive = true.




See origin...

ID: TFNFR30 - Category: Code Style - Handling Deprecated Outputs

Sometimes we notice that the name of certain output is not appropriate anymore, however, since we have to ensure forward compatibility in the same major version, its name MUST NOT be changed directly. It MUST be moved to an independent deprecated_outputs.tf file, then redefine a new output in output.tf and make sure it’s compatible everywhere else in the module.

A cleanup SHOULD be performed to deprecated_outputs.tf and other logics related to compatibility during a major version upgrade.




See origin...

ID: TFNFR31 - Category: Code Style - locals.tf for Locals Only

In locals.tf, file we could declare multiple locals blocks, but only locals blocks are allowed.

You MAY declare locals blocks next to a resource block or data block for some advanced scenarios, like making a fake module to execute some light-weight tests aimed at the expressions.




See origin...

ID: TFNFR33 - Category: Code Style - Precise Local Types

Precise local types SHOULD be used.

Good example:

{
  name = "John"
  age  = 52
}

Bad example:

{
  name = "John"
  age  = "52" # age should be number
}



See origin...

ID: TFNFR34 - Category: Code Style - Using Feature Toggles

A toggle variable MUST be used to allow users to avoid the creation of a new resource block by default if it is added in a minor or patch version.

E.g., our previous release was v1.2.1 and next release would be v1.3.0, now we’d like to submit a pull request which contains such new resource:

resource "azapi_resource" "route_table" {
  type      = "Microsoft.Network/routeTables@2023-11-01"
  name      = coalesce(var.new_route_table_name, "${var.subnet_name}-rt")
  parent_id = var.parent_id
  location  = local.location
  body = {
    properties = {}
  }
  response_export_values = []
}

A user who’s just upgraded the module’s version would be surprised to see a new resource to be created in a newly generated plan file.

A better approach is adding a feature toggle to be turned off by default:

variable "create_route_table" {
  type     = bool
  default  = false
  nullable = false
}

resource "azapi_resource" "route_table" {
  count     = var.create_route_table ? 1 : 0
  type      = "Microsoft.Network/routeTables@2023-11-01"
  name      = coalesce(var.new_route_table_name, "${var.subnet_name}-rt")
  parent_id = var.parent_id
  location  = local.location
  body = {
    properties = {}
  }
  response_export_values = []
}



See origin...

ID: TFNFR35 - Category: Code Style - Reviewing Potential Breaking Changes

Potential breaking(surprise) changes introduced by resource block

  1. Adding a new resource without count or for_each for conditional creation, or creating by default
  2. Adding a new argument assignment with a value other than the default value provided by the provider’s schema
  3. Adding a new nested block without making it dynamic or omitting it by default
  4. Renaming a resource block without one or more corresponding moved blocks
  5. Change resource’s count to for_each, or vice versa

Terraform moved block could be your cure.

Potential breaking changes introduced by variable and output blocks

  1. Deleting(Renaming) a variable
  2. Changing type in a variable block
  3. Changing the default value in a variable block
  4. Changing variable’s nullable to false
  5. Changing variable’s sensitive from false to true
  6. Adding a new variable without default
  7. Deleting an output
  8. Changing an output’s value
  9. Changing an output’s sensitive value

These changes do not necessarily trigger breaking changes, but they are very likely to, they MUST be reviewed with caution.




See origin...

ID: TFNFR36 - Category: Code Style - Setting prevent_deletion_if_contains_resources (AzureRM only)

From Terraform AzureRM 3.0, the default value of prevent_deletion_if_contains_resources in provider block is true. This will lead to an unstable test because the test subscription has some policies applied, and they will add some extra resources during the run, which can cause failures during destroy of resource groups.

Since we cannot guarantee our testing environment won’t be applied some Azure Policy Remediation Tasks in the future, for a robust testing environment, prevent_deletion_if_contains_resources SHOULD be explicitly set to false.




See origin...

ID: TFNFR37 - Category: Code Style - Tool Usage by Module Owner

newres is a command-line tool that generates Terraform configuration files for a specified resource type. It automates the process of creating variables.tf and main.tf files, making it easier to get started with Terraform and reducing the time spent on manual configuration.

Module owners MAY use newres when they’re trying to add new resource block, attribute, or nested block. They MAY generate the whole block along with the corresponding variable blocks in an empty folder, then copy-paste the parts they need with essential refactoring.




See origin...

ID: TFNFR39 - Category: Code Style - Standard File Layout

Every Terraform AVM module (root module and every submodule) MUST organize its top-level Terraform code into the following files at the module’s root directory:

FileRequiredContents
terraform.tfMUSTThe single terraform { … } block — required_version, required_providers, and any backend configuration (root module only). Provider configuration blocks MUST NOT appear here.
variables.tfMUSTAll variable blocks for the module. MAY be split into additional variables.<topic>.tf files (see below).
outputs.tfMUSTAll output blocks for the module. MAY be split into additional outputs.<topic>.tf files (see below).
main.tfMUSTThe module’s primary resource, data, and module blocks. MAY be split into additional main.<topic>.tf files (see below).
locals.tfSHOULDAll locals blocks. Required if the module declares any locals. MAY be split into additional locals.<topic>.tf files (see below). MAY be omitted only when the module has no locals at all.

Splitting and naming additional files

For larger modules the contents of main.tf, variables.tf, outputs.tf, and locals.tf MAY each be split into multiple files along logical / topic lines. When this is done:

  • Additional Terraform files MUST use the canonical filename (main, variables, outputs, or locals) as the prefix, followed by a ., a short descriptive topic name, and the .tf extension — for example main.diagnostic_settings.tf, variables.diagnostic_settings.tf, outputs.diagnostic_settings.tf, locals.diagnostic_settings.tf.
  • The topic name MUST be snake_case (per TFNFR3).
  • The same topic name SHOULD be used across the four file types when they describe the same logical concern, so that (for example) main.private_endpoints.tf, variables.private_endpoints.tf, outputs.private_endpoints.tf, and locals.private_endpoints.tf all relate to the same feature.
  • Each split file MUST contain only the block kind matching its prefix:
    • main.<topic>.tf — only resource, data, and module blocks.
    • variables.<topic>.tf — only variable blocks.
    • outputs.<topic>.tf — only output blocks.
    • locals.<topic>.tf — only locals blocks.
  • The terraform { … } block MUST appear exactly once per module, in terraform.tf. It MUST NOT be split.

Files that MUST NOT appear at the module root

  • A providers.tf file — provider requirements belong in terraform.tf; provider configurations belong only in the consumer’s root module, never in an AVM module (per SFR2).
  • A single monolithic module.tf or everything.tf — the canonical filenames above MUST be used.

Rationale

Standardizing file layout means that any reviewer or consumer can find a module’s interface (variables.tf, outputs.tf), provider constraints (terraform.tf), and primary logic (main.tf / main.<topic>.tf) in the same place across every AVM Terraform module, without having to grep. It also makes the cascade rules in TFFR6, TFFR7, and TFRMNFR1 reviewable at a glance.

Notes

  • Submodules (per TFRMNFR1) follow the same layout in their own root directory under modules/<subresource>/. The submodule’s terraform.tf MUST declare the same set of required_providers it actually consumes.
  • Auto-generated documentation files (README.md, _header.md, _footer.md) and tooling configuration files (.terraform-docs.yml, .tflint.hcl, etc.) are out of scope of this rule and follow their own specs.



Inputs / Outputs

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR14Data TypesSHOULDOwnerContributorBAU
2SNFR22Parameters/Variables for Resource IDsMUSTOwnerContributorBAU
3SNFR26Output - Parameters - DecoratorsMUSTOwnerContributorBAU
4TFFR2Additional Terraform OutputsSHOULDOwnerContributorBAU
5TFFR6AzAPI - resource_types variableMUSTOwnerContributorBAU
6TFFR7AzAPI - retry and timeouts variablesMUSTOwnerContributorBAU
7TFNFR14Not allowed variablesMUSTOwnerContributorBAU
8TFNFR38Resource ID Variable ValidationMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

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.




See origin...

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
  }
}



See origin...

ID: SNFR26 - Output-Parameters - Decorators

Output parameters MUST implement:

Output parameters
@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
}



See origin...

ID: TFFR2 - Category: Outputs - Additional Terraform Outputs

Authors SHOULD NOT output entire resource objects as these may contain sensitive outputs and the schema can change with API or provider versions.
Instead, authors SHOULD output the computed attributes of the resource as discreet outputs.
This kind of pattern protects against provider schema changes and is known as an anti-corruption layer.

Remember, you SHOULD NOT output values that are already inputs (other than name).

E.g.,

# Resource output, computed attribute.
output "foo" {
  description = "MyResource foo attribute"
  value = azapi_resource.myresource.output.properties.foo
}

# Resource output for resources that are deployed using `for_each`. Again only computed attributes.
output "childresource_foos" {
  description = "MyResource children's foo attributes"
  value = {
    for key, value in azapi_resource.mychildresource : key => value.output.properties.foo
  }
}

# Output of a sensitive attribute
output "bar" {
  description = "MyResource bar attribute"
  value     = azapi_resource.myresource.output.properties.bar
  sensitive = true
}



See origin...

ID: TFFR6 - Category: Inputs/Outputs - AzAPI - resource_types variable

Authors MUST NOT hard-code the type argument of an azapi_resource (or azapi_data_plane_resource, azapi_resource_action, azapi_update_resource) inline.

Instead, every AzAPI resource type string used by the module MUST be sourced from a single object variable named resource_types. This variable MUST:

  • Have one optional(string, "<provider>/<resource>@<api-version>") field per azapi_resource declared by the module (and by its submodules — see TFRMNFR1).
  • Default each field to the latest API version that the module has been tested against. The default MUST be a stable (non-preview) API version unless the module’s primary resource only ships a preview API.
  • Default the variable itself to {} so consumers only need to supply the keys they wish to override.
  • Be nullable = false.
  • Document each field in the variable’s description, including the resource it controls.

The rationale is to allow consumers to:

  • Target sovereign clouds (e.g., Azure US Government, Azure China) where older API versions may be the latest available.
  • Opt into a newer preview API version without waiting for a module release.
  • Pin a specific API version for compliance or reproducibility reasons.

Parent modules MUST cascade the relevant subset of resource_types to each submodule they instantiate, so that submodule API versions remain consistent with the parent’s chosen versions and a single override at the parent level propagates everywhere.

variable "resource_types" {
  type = object({
    widget = optional(string, "Microsoft.Example/widgets@2024-01-01")
    part   = optional(string, "Microsoft.Example/widgets/parts@2024-01-01")
    lock   = optional(string, "Microsoft.Authorization/locks@2020-05-01")
  })
  default     = {}
  nullable    = false
  description = <<DESCRIPTION
Override the AzAPI `<provider>/<resource>@<api-version>` strings used by this module. Each key defaults to a tested value; supply only the keys you want to override. Useful when targeting a sovereign cloud with older API versions, or when opting into a newer preview API.

- `widget` - The primary resource managed by this module.
- `part`   - Child resources of the primary resource.
- `lock`   - Management lock applied to the primary resource and its private endpoints.
DESCRIPTION
}

resource "azapi_resource" "this" {
  type      = var.resource_types.widget
  name      = var.name
  parent_id = var.parent_id
  body      = { /* ... */ }
}

module "part" {
  source = "./modules/part"

  # Cascade the relevant subset to the submodule.
  resource_types = {
    this = var.resource_types.part
  }

  # ...other arguments...
}



See origin...

ID: TFFR7 - Category: Inputs/Outputs - AzAPI - retry and timeouts variables

The retry and timeouts blocks of every azapi_resource declared by the module MUST be configurable by the consumer. Authors MUST NOT hard-code values inline that the consumer cannot override.

To meet this requirement, the module MUST expose two variables:

  • retry — an object variable controlling the AzAPI retry block.
  • timeouts — an object variable controlling the AzAPI timeouts block.

Both variables:

  • MAY define module-level defaults (e.g., a default error_message_regex such as "ScopeLocked" for resources that race with lock removal, or a default delete = "5m").
  • MUST allow the consumer to override the defaults — either by supplying a non-null value at the variable level, or by allowing per-field overrides through optional(...) attributes.
  • MUST be applied to every azapi_resource (and equivalent AzAPI resources) declared by the module.
  • MUST cascade to submodules — the parent module’s retry and timeouts values MUST be passed through to each submodule it instantiates (see TFRMNFR1). Submodules MAY additionally expose per-item overrides for cases where individual resources need different settings.
variable "retry" {
  type = object({
    error_message_regex  = optional(list(string))
    interval_seconds     = optional(number)
    max_interval_seconds = optional(number)
  })
  default     = null
  description = <<DESCRIPTION
Retry configuration applied to every `azapi` resource managed by the module (root resource and all submodules). Defaults to `null` (no custom retry).

- `error_message_regex`  - (Optional) A list of regex patterns matching error messages that trigger a retry.
- `interval_seconds`     - (Optional) Initial interval between retries in seconds.
- `max_interval_seconds` - (Optional) Maximum interval between retries in seconds.

See <https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/resource#retry> for full semantics.
DESCRIPTION
}

variable "timeouts" {
  type = object({
    create = optional(string)
    read   = optional(string)
    update = optional(string)
    delete = optional(string)
  })
  default     = null
  description = <<DESCRIPTION
Default per-operation timeouts applied to every `azapi` resource managed by the module. Defaults to `null` (provider defaults). Each value is a Go duration string (e.g. `30m`, `1h`).

- `create` - (Optional) Timeout for create operations.
- `read`   - (Optional) Timeout for read operations.
- `update` - (Optional) Timeout for update operations.
- `delete` - (Optional) Timeout for delete operations.
DESCRIPTION
}

resource "azapi_resource" "this" {
  type      = var.resource_types.this
  name      = var.name
  parent_id = var.parent_id
  body      = { /* ... */ }

  # `retry` is an attribute on `azapi_resource`, so the variable can be
  # assigned directly. `timeouts` is a block, so a `dynamic "timeouts"`
  # block is required to honor the variable's `null` default.
  retry = var.retry

  dynamic "timeouts" {
    for_each = var.timeouts == null ? [] : [var.timeouts]
    content {
      create = timeouts.value.create
      read   = timeouts.value.read
      update = timeouts.value.update
      delete = timeouts.value.delete
    }
  }

  response_export_values = []
}

module "child" {
  source = "./modules/child"

  # Cascade retry and timeouts to the submodule.
  retry    = var.retry
  timeouts = var.timeouts

  # ...other arguments...
}



See origin...

ID: TFNFR14 - Category: Inputs - Not allowed variables

Since Terraform 0.13, count, for_each and depends_on are introduced for modules, module development is significantly simplified. Module’s owners MUST NOT add variables like enabled or module_depends_on to control the entire module’s operation. Boolean feature toggles are acceptable however.




See origin...

ID: TFNFR38 - Category: Inputs/Outputs - Resource ID Variable Validation

Every input variable (or nested attribute) that holds an Azure ARM resource ID MUST be validated using the AzAPI provider-defined function provider::azapi::parse_resource_id, called with a literal string naming the expected resource type, and wrapped in can(...).

Hand-rolled regex, startswith, length, or split checks MUST NOT be used to validate resource IDs. The provider function knows the canonical ARM ID grammar for every resource type, is fixed in lockstep with the provider, and produces a single consistent error model — including for IDs whose grammar contains anomalies (such as classic resources, extension resources, or scope-based IDs).

This rule covers, but is not limited to:

  • Top-level scope variables such as parent_id (see TFRMFR1).
  • Variables that reference other Azure resources by ID (e.g. subnet_resource_id, key_vault_resource_id, workspace_resource_id, private_dns_zone_resource_ids, user_assigned_resource_ids).
  • Nested attributes inside object, map(object), set(object), or list(object) types that hold resource IDs.

Rules

  • The resource type passed to parse_resource_id MUST be a literal string (e.g. "Microsoft.Network/virtualNetworks/subnets"). It MUST NOT be a reference to another variable, local, or expression. This keeps each validation block self-contained and avoids requiring cross-variable validation.
  • For optional / nullable variables, the validation MUST short-circuit on null (e.g. var.x == null || can(provider::azapi::parse_resource_id("...", var.x))) so that callers omitting the value do not trip validation.
  • For collection-valued variables (set(string), list(string), map(string)), the validation MUST iterate the collection with alltrue([for v in ... : can(...)]).
  • For nested attributes within object types, the validation MUST iterate the parent collection (or reference the object directly) and validate each nested resource ID, again handling null for optional nested attributes.
  • Where a variable can legitimately hold IDs of more than one resource type (rare — e.g. marketplace_partner_resource_id in the diagnostic-settings interface), this rule does not apply and the variable SHOULD be left without resource-ID validation rather than validated against a single arbitrary type.

Examples

A required, single-value resource ID:

variable "key_vault_resource_id" {
  type     = string
  nullable = false

  validation {
    condition     = can(provider::azapi::parse_resource_id("Microsoft.KeyVault/vaults", var.key_vault_resource_id))
    error_message = "`key_vault_resource_id` must be a valid Azure Key Vault resource ID."
  }

  description = "The resource ID of the Key Vault that holds the customer-managed key."
}

An optional, single-value resource ID:

variable "workspace_resource_id" {
  type     = string
  default  = null
  nullable = true

  validation {
    condition     = var.workspace_resource_id == null || can(provider::azapi::parse_resource_id("Microsoft.OperationalInsights/workspaces", var.workspace_resource_id))
    error_message = "`workspace_resource_id` must be a valid Log Analytics workspace resource ID, or `null`."
  }

  description = "The resource ID of the Log Analytics workspace to send diagnostics to."
}

A collection of resource IDs:

variable "user_assigned_resource_ids" {
  type     = set(string)
  default  = []
  nullable = false

  validation {
    condition = alltrue([
      for id in var.user_assigned_resource_ids :
      can(provider::azapi::parse_resource_id("Microsoft.ManagedIdentity/userAssignedIdentities", id))
    ])
    error_message = "Each entry in `user_assigned_resource_ids` must be a valid user-assigned managed identity resource ID."
  }

  description = "A set of user-assigned managed identity resource IDs to attach to the resource."
}

A nested resource ID inside a map(object(...)):

variable "private_endpoints" {
  type = map(object({
    subnet_resource_id            = string
    private_dns_zone_resource_ids = optional(set(string), [])
    # ...other attributes...
  }))
  default  = {}
  nullable = false

  validation {
    condition = alltrue([
      for _, v in var.private_endpoints :
      can(provider::azapi::parse_resource_id("Microsoft.Network/virtualNetworks/subnets", v.subnet_resource_id))
    ])
    error_message = "Each `private_endpoints[*].subnet_resource_id` must be a valid subnet resource ID."
  }

  validation {
    condition = alltrue(flatten([
      for _, v in var.private_endpoints : [
        for id in v.private_dns_zone_resource_ids :
        can(provider::azapi::parse_resource_id("Microsoft.Network/privateDnsZones", id))
      ]
    ]))
    error_message = "Each entry in `private_endpoints[*].private_dns_zone_resource_ids` must be a valid private DNS zone resource ID."
  }
}

Notes

  • The rule applies regardless of whether the resource ID is required or optional, single-valued or collection-valued, top-level or nested.
  • parse_resource_id errors when (a) the input is not a well-formed ARM ID, or (b) the input does not parse as the supplied resource type. Wrapping in can(...) converts both failure modes into a single boolean suitable for a validation block’s condition.
  • This rule supersedes any older guidance suggesting startswith(var.x, "/") or hand-written regex for resource ID validation.



Testing

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR1Prescribed TestsMUSTOwnerContributorBAU
2SNFR2E2E TestingMUSTOwnerContributorBAU
3SNFR3AVM Compliance TestsMUSTOwnerContributorInitial
4SNFR4Unit TestsSHOULDOwnerContributorBAU
5SNFR5Upgrade TestsSHOULDOwnerContributorBAU
6SNFR6Static Analysis/Linting TestsMUSTOwnerContributorBAU
7SNFR7Idempotency TestsMUSTOwnerContributorBAU
8TFNFR5Test ToolingMUSTOwnerContributorBAU
9TFNFR15Variable Definition OrderSHOULDOwnerContributorBAU
➕ See Specifications for this category
See origin...

ID: SNFR1 - Category: Testing - Prescribed Tests

Modules MUST use the prescribed tooling and testing frameworks defined in the language specific specs.




See origin...

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

To see a directory and file structure for a module, see the language specific contribution guide.

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.
➕ 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.

Details for use in E2E tests

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>).




See origin...

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/avm-core-team-technical-bicep, OR @Azure/avm-core-team-technical-terraform.




See origin...

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.




See origin...

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.




See origin...

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.




See origin...

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.




See origin...

ID: TFNFR5 - Category: Testing - Test Tooling

Module owners MUST use the below test script for unit/linting/static/security analysis tests.

  • ./avm pr-check



See origin...

ID: TFNFR15 - Category: Code Style - Variable Definition Order

Input variables SHOULD follow this order:

  1. All required fields, in alphabetical order
  2. All optional fields, in alphabetical order

A variable without default value is a required field, otherwise it’s an optional one.




Documentation

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR15Automatic Documentation GenerationMUSTOwnerContributorBAU
2SNFR16Examples/E2EMUSTOwnerContributorBAU
3TFNFR1DescriptionsMUSTOwnerContributorBAU
4TFNFR2Module Documentation GenerationMUSTOwnerContributorBAU
➕ See Specifications for this category
See origin...

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.




See origin...

ID: SNFR16 - Category: Documentation - Examples/E2E

An examples/e2e directory MUST exist to provide named scenarios for module deployment.




See origin...

ID: TFNFR1 - Category: Documentation - Descriptions

Where descriptions for variables and outputs spans multiple lines. The description MAY provide variable input examples for each variable using the HEREDOC format and embedded markdown.

Example:

  variable "my_complex_input" {
    type = map(object({
      param1 = string
      param2 = optional(number, null)
    }))
    description = <<DESCRIPTION
  A complex input variable that is a map of objects.
  Each object has two attributes:
  
  - `param1`: A required string parameter.
  - `param2`: (Optional) An optional number parameter.
  
  Example Input:
  
  ```terraform
  my_complex_input = {
    "object1" = {
      param1 = "value1"
      param2 = 2
    }
    "object2" = {
      param1 = "value2"
    }
  }
  ```
  DESCRIPTION
  }
  



See origin...

ID: TFNFR2 - Category: Documentation - Module Documentation Generation

Terraform modules documentation MUST be automatically generated via Terraform Docs.

A file called .terraform-docs.yml MUST be present in the root of the module and have the following content:

  ---
  ### To generate the output file to partially incorporate in the README.md,
  ### Execute this command in the Terraform module's code folder:
  # terraform-docs -c .terraform-docs.yml .
  
  formatter: "markdown document" # this is required
  
  version: "0.16.0"
  
  header-from: "_header.md"
  footer-from: "_footer.md"
  
  recursive:
    enabled: false
    path: modules
  
  sections:
    hide: []
    show: []
  
  content: |-
    {{ .Header }}    
  
    <!-- markdownlint-disable MD033 -->
    {{ .Requirements }}
  
    {{ .Providers }}
  
    {{ .Resources }}
  
    <!-- markdownlint-disable MD013 -->
    {{ .Inputs }}
  
    {{ .Outputs }}
  
    {{ .Modules }}
  
    {{ .Footer }}
  
  output:
    file: README.md
    mode: replace
    template: |-
      <!-- BEGIN_TF_DOCS -->
      {{ .Content }}
      <!-- END_TF_DOCS -->      
  output-values:
    enabled: false
    from: ""
  
  sort:
    enabled: true
    by: required
  
  settings:
    anchor: true
    color: true
    default: true
    description: false
    escape: true
    hide-empty: false
    html: true
    indent: 2
    lockfile: true
    read-comments: true
    required: true
    sensitive: true
    type: true
  



Release / Publishing

The content below is listed based on the following tags
#IDTitleSeverityPersonaLifecycle
1SNFR17Semantic VersioningMUSTOwnerContributorBAU
2SNFR18Breaking ChangesSHOULDOwnerContributorBAU
3SNFR19Registries TargetedMUSTOwnerContributorBAU
4SNFR21Cross Language CollaborationSHOULDOwnerContributorBAU
➕ See Specifications for this category
See origin...

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 Version
  • Y == Minor Version
  • Z == Patch Version

Module versioning before first Major version release 1.0.0

  • Initially modules MUST be released as version 0.1.0 and 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 version 1.0.0.

    Note

    Releasing as version 0.1.0 initially 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.0 is released, given a version number X.Y.Z:

    • X Major version MUST NOT be bumped.
    • Y Minor version MUST be bumped when introducing breaking changes (which would normally bump Major after 1.0.0 release) or feature updates (same as it will be after 1.0.0 release).
    • Z Patch version MUST be bumped when introducing non-breaking, backward compatible bug fixes (same as it will be after 1.0.0 release).



See origin...

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.




See origin...

ID: SNFR19 - Category: Publishing - Registries Targeted

Modules MUST be published to their respective language public registries.

Tip

See the language specific contribution guides for detailed guidance and sample code to use in AVM modules to achieve this requirement.




See origin...

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.