Azure Verified Modules
Glossary GitHub GitHub Issues Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

Last updated: 11 Dec 2024

Bicep Interfaces

Below are the interfaces/schemas for the AVM Resource Modules features/extension resources as detailed in RMFR4 and RMFR5

Diagnostic Settings

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.

  // ============== //
  //   Parameters   //
  // ============== //
  
  import { diagnosticSettingFullType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('Optional. The diagnostic settings of the service.')
  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 ?? [ { category: 'AllMetrics' } ]): {
        category: group.category
        enabled: group.?enabled ?? true
        timeGrain: null
      }]
      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'
        }
      ]
      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}'
    }
  ]
  
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.?kind == 'CanNotDelete' ? 'Cannot delete resource or child resources.' : 'Cannot delete or modify the resource or child resources.'
    }
    scope: >singularMainResourceType<
  }
  
lock: '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


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

Private Endpoints


  // ============== //
  //   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[]?
  
  // ============= //
  //   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(privateEndpoint.?resourceGroupName ?? '')
    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: privateEndpoint.?enableTelemetry ?? enableTelemetry
      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
    }
  }]
  
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'
      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'
        }
      ]
      resourceGroupName: 'mySecondaryRg'
    }
  }
  

  // ============== //
  //   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[]?
  
  // ============= //
  //   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(privateEndpoint.?resourceGroupName ?? '')
    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: privateEndpoint.?enableTelemetry ?? enableTelemetry
      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
    }
  }]
  
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'
      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'
        }
      ]
      resourceGroupName: 'mySecondaryRg'
    }
  }
  

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

// ============== //
  //   Parameters   //
  // ============== //
  
  import { customerManagedKeyType } from 'br/public:avm/utl/types/avm-common-types:>version<'
  @description('Optional. The customer managed key definition.')
  param customerManagedKey customerManagedKeyType?
  
  // ============= //
  //   Resources   //
  // ============= //
  
  resource cMKKeyVault 'Microsoft.KeyVault/vaults@2023-02-01' existing = if (!empty(customerManagedKey.?keyVaultResourceId)) {
    name: last(split((customerManagedKey.?keyVaultResourceId ?? 'dummyVault'), '/'))
    scope: resourceGroup(
      split((customerManagedKey.?keyVaultResourceId ?? '//'), '/')[2],
      split((customerManagedKey.?keyVaultResourceId ?? '////'), '/')[4]
    )
  
    resource cMKKey 'keys@2023-02-01' existing = if (!empty(customerManagedKey.?keyVaultResourceId) && !empty(customerManagedKey.?keyName)) {
      name: customerManagedKey.?keyName ?? 'dummyKey'
    }
  }
  
  resource cMKUserAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = if (!empty(customerManagedKey.?userAssignedIdentityResourceId)) {
    name: last(split(customerManagedKey.?userAssignedIdentityResourceId ?? 'dummyMsi', '/'))
    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: cMKKeyVault.properties.vaultUri
              keyName: customerManagedKey!.keyName
              keyVersion: !empty(customerManagedKey.?keyVersion ?? '')
                ? customerManagedKey!.keyVersion
                : last(split(cMKKeyVault::cMKKey.properties.keyUriWithVersion, '/'))
              keyIdentifier: !empty(customerManagedKey.?keyVersion ?? '')
                ? '${cMKKeyVault::cMKKey.properties.keyUri}/${customerManagedKey!.keyVersion}'
                : cMKKeyVault::cMKKey.properties.keyUriWithVersion
              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?
  
  // ============= //
  //   Resources   //
  // ============= //
  
  resource cMKKeyVault 'Microsoft.KeyVault/vaults@2023-02-01' existing = if (!empty(customerManagedKey.?keyVaultResourceId)) {
    name: last(split((customerManagedKey.?keyVaultResourceId ?? 'dummyVault'), '/'))
    scope: resourceGroup(
      split((customerManagedKey.?keyVaultResourceId ?? '//'), '/')[2],
      split((customerManagedKey.?keyVaultResourceId ?? '////'), '/')[4]
    )
  
    resource cMKKey 'keys@2023-02-01' existing = if (!empty(customerManagedKey.?keyVaultResourceId) && !empty(customerManagedKey.?keyName)) {
      name: customerManagedKey.?keyName ?? 'dummyKey'
    }
  }
  
  resource cMKUserAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = if (!empty(customerManagedKey.?userAssignedIdentityResourceId)) {
    name: last(split(customerManagedKey.?userAssignedIdentityResourceId ?? 'dummyMsi', '/'))
    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: cMKKeyVault.properties.vaultUri
              keyName: customerManagedKey!.keyName
              keyVersion: !empty(customerManagedKey.?keyVersion ?? '')
                ? customerManagedKey!.keyVersion
                : (customerManagedKey.?autoRotationEnabled ?? true)
                  ? null
                  : last(split(cMKKeyVault::cMKKey.properties.keyUriWithVersion, '/'))
              keyIdentifier: !empty(customerManagedKey.?keyVersion ?? '')
                ? '${cMKKeyVault::cMKKey.properties.keyUri}/${customerManagedKey!.keyVersion}'
                : (customerManagedKey.?autoRotationEnabled ?? true)
                  ? cMKKeyVault::cMKKey.properties.keyUri
                  : cMKKeyVault::cMKKey.properties.keyUriWithVersion
              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: {autoRotationEnabled}
    userAssignedIdentityResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{uamiName}'
  }
  

Secrets export

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.

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.

  // ============== //
  //   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?
  
    // (...)
  }
  
secretsExportConfiguration: {
    keyVaultResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.KeyVault/vaults/{keyVaultName}'
    >secretToExport1<Name: 'myPrimarySecret'
    >secretToExport2<Name: 'mySecondarySecret'
    // (...)
  }
  

  // ============== //
  //   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
    }
  ]
  

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


  // Get all exported secret references
  output exportedSecrets object = >deploymentReference<.outputs.exportedSecrets
  
  // Get the resource Id of just the secret with name >secretToExport1<Name
  output specificSecret string = >deploymentReference<.outputs.exportedSecrets.>secretToExport1<Name.secretResourceId
  
  // Get the resource Ids of all secrets set
  output exportedSecretResourceIds array = map(
    items(>deploymentReference<.outputs.exportedSecrets),
    item => item.value.secretResourceId
  )
  

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

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