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: 18 Apr 2024

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   //
  // ============== //
  
  @description('Optional. The diagnostic settings of the service.')
  param diagnosticSettings diagnosticSettingType
  
  // ============= //
  //   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<
  }]
  
  // =============== //
  //   Definitions   //
  // =============== //
  
  type diagnosticSettingType = {
    @description('Optional. The name of diagnostic setting.')
    name: string?
  
    @description('Optional. The name of logs that will be streamed. "allLogs" includes all possible logs for the resource. Set to `[]` to disable log collection.')
    logCategoriesAndGroups: {
      @description('Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here.')
      category: string?
  
      @description('Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs.')
      categoryGroup: string?
  
      @description('Optional. Enable or disable the category explicitly. Default is `true`.')
      enabled: bool?
    }[]?
  
    @description('Optional. The name of metrics that will be streamed. "allMetrics" includes all possible metrics for the resource. Set to `[]` to disable metric collection.')
    metricCategories: {
      @description('Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics.')
      category: string
  
      @description('Optional. Enable or disable the category explicitly. Default is `true`.')
      enabled: bool?
    }[]?
  
    @description('Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type.')
    logAnalyticsDestinationType: ('Dedicated' | 'AzureDiagnostics')?
  
    @description('Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub.')
    workspaceResourceId: string?
  
    @description('Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub.')
    storageAccountResourceId: string?
  
    @description('Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to.')
    eventHubAuthorizationRuleResourceId: string?
  
    @description('Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub.')
    eventHubName: string?
  
    @description('Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs.')
    marketplacePartnerResourceId: string?
  }[]?
  
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}'
    }
  ]
  
variable "diagnostic_settings" {
    type = map(object({
      name                                     = optional(string, null)
      log_categories                           = optional(set(string), [])
      log_groups                               = optional(set(string), ["allLogs"])
      metric_categories                        = optional(set(string), ["AllMetrics"])
      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 :
          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."
    }
    description = <<DESCRIPTION
  A map of diagnostic settings 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 diagnostic setting. One will be generated if not set, however this will not be unique if you want to create multiple diagnostic setting resources.
  - `log_categories` - (Optional) A set of log categories to send to the log analytics workspace. Defaults to `[]`.
  - `log_groups` - (Optional) A set of log groups to send to the log analytics workspace. Defaults to `["allLogs"]`.
  - `metric_categories` - (Optional) A set of metric categories to send to the log analytics workspace. Defaults to `["AllMetrics"]`.
  - `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 LogsLogs.
  DESCRIPTION
  }
  
  # Sample resource
  resource "azurerm_monitor_diagnostic_setting" "this" {
    for_each                       = var.diagnostic_settings
    name                           = each.value.name != null ? each.value.name : "diag-${var.name}"
    target_resource_id             = azurerm_<MY_RESOURCE>.this.id
    storage_account_id             = each.value.storage_account_resource_id
    eventhub_authorization_rule_id = each.value.event_hub_authorization_rule_resource_id
    eventhub_name                  = each.value.event_hub_name
    partner_solution_id            = each.value.marketplace_partner_resource_id
    log_analytics_workspace_id     = each.value.workspace_resource_id
    log_analytics_destination_type = each.value.log_analytics_destination_type
  
    dynamic "enabled_log" {
      for_each = each.value.log_categories
      content {
        category = enabled_log.value
      }
    }
  
    dynamic "enabled_log" {
      for_each = each.value.log_groups
      content {
        category_group = enabled_log.value
      }
    }
  
    dynamic "metric" {
      for_each = each.value.metric_categories
      content {
        category = metric.value
      }
    }
  }
  
diagnostic_settings = {
    diag_setting_1 = {
      name                                     = "diagSetting1"
      log_groups                               = ["allLogs"]
      metric_categories                        = ["AllMetrics"]
      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}"
    }
  }
  
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   //
  // ============== //
  
  @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<
    }
  ]
  
  // =============== //
  //   Definitions   //
  // =============== //
  
  type roleAssignmentType = {
    @description('Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated.')
    name: string?
  
    @description('Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: \'/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11\'.')
    roleDefinitionIdOrName: string
  
    @description('Required. The principal ID of the principal (user/group/identity) to assign the role to.')
    principalId: string
  
    @description('Optional. The principal type of the assigned principal ID.')
    principalType: ('ServicePrincipal' | 'Group' | 'User' | 'ForeignGroup' | 'Device')?
  
    @description('Optional. The description of the role assignment.')
    description: string?
  
    @description('Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase "foo_storage_container".')
    condition: string?
  
    @description('Optional. Version of the condition.')
    conditionVersion: '2.0'?
  
    @description('Optional. The Resource Id of the delegated managed identity resource.')
    delegatedManagedIdentityResourceId: string?
  }[]?
  
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'
    }
  ]
  
variable "role_assignments" {
    type = map(object({
      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.
  
  - `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
  }
  
  locals {
    role_definition_resource_substring = "providers/Microsoft.Authorization/roleDefinitions"
  }
  
  # Example resource declaration
  resource "azurerm_role_assignment" "this" {
    for_each                               = var.role_assignments
    scope                                  = azurerm_MY_RESOURCE.this.id
    role_definition_id                     = strcontains(lower(each.value.role_definition_id_or_name), lower(local.role_definition_resource_substring)) ? each.value.role_definition_id_or_name : null
    role_definition_name                   = strcontains(lower(each.value.role_definition_id_or_name), lower(local.role_definition_resource_substring)) ? null : each.value.role_definition_id_or_name
    principal_id                           = each.value.principal_id
    condition                              = each.value.condition
    condition_version                      = each.value.condition_version
    skip_service_principal_aad_check       = each.value.skip_service_principal_aad_check
    delegated_managed_identity_resource_id = each.value.delegated_managed_identity_resource_id
    principal_type                         = each.value.principal_type
  }
  
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


  // ============== //
  //   Parameters   //
  // ============== //
  
  @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<
  }
  
  // =============== //
  //   Definitions   //
  // =============== //
  
  type lockType = {
    @description('Optional. Specify the name of lock.')
    name: string?
  
    @description('Optional. Specify the type of lock.')
    kind: ('CanNotDelete' | 'ReadOnly' | 'None')?
  }?
  
lock: 'CanNotDelete'
  
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\"`."
    }
  }
  
  # Example resource implementation
  resource "azurerm_management_lock" "this" {
    count = var.lock != null ? 1 : 0
  
    lock_level = var.lock.kind
    name       = coalesce(var.lock.name, "lock-${var.lock.kind}")
    scope      = azurerm_MY_RESOURCE.this.id
    notes      = var.lock.kind == "CanNotDelete" ? "Cannot delete the resource or its child resources." : "Cannot delete or modify the resource or its child resources."
  }
  
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

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

@description('Optional. Tags of the resource.')
  param tags object?
  
tags: {
    key: 'value'
    'another-key': 'another-value'
    integers: 123
  }
  
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


  // ============== //
  //   Parameters   //
  // ============== //
  
  @description('Optional. The managed identity definition for this resource.')
  param managedIdentities managedIdentitiesType
  
  // ============= //
  //   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 ?? ''
  
  // =============== //
  //   Definitions   //
  // =============== //
  
  type managedIdentitiesType = {
    @description('Optional. Enables system assigned managed identity on the resource.')
    systemAssigned: bool?
  
    @description('Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption.')
    userAssignedResourceIds: string[]?
  }?
  
managedIdentities: {
    systemAssigned: true
    userAssignedResourceIds: [
      '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}'
      '/subscriptions/{subscriptionId2}/resourceGroups/{resourceGroupName2}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName2}'
    ]
  }
  
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
  }
  
  # Helper locals to make the dynamic block more readable
  # There are three attributes here to cater for resources that
  # support both user and system MIs, only system MIs, and only user MIs
  locals {
    managed_identities = {
      system_assigned_user_assigned = (var.managed_identities.system_assigned || length(var.managed_identities.user_assigned_resource_ids) > 0) ? {
        this = {
          type                       = var.managed_identities.system_assigned && length(var.managed_identities.user_assigned_resource_ids) > 0 ? "SystemAssigned, UserAssigned" : length(var.managed_identities.user_assigned_resource_ids) > 0 ? "UserAssigned" : "SystemAssigned"
          user_assigned_resource_ids = var.managed_identities.user_assigned_resource_ids
        }
      } : {}
      system_assigned = var.managed_identities.system_assigned ? {
        this = {
          type = "SystemAssigned"
        }
      } : {}
      user_assigned = length(var.managed_identities.user_assigned_resource_ids) > 0 ? {
        this = {
          type                       = "UserAssigned"
          user_assigned_resource_ids = var.managed_identities.user_assigned_resource_ids
        }
      } : {}
    }
  }
  
  ## Resources supporting both SystemAssigned and UserAssigned
  dynamic "identity" {
    for_each = local.managed_identities.system_assigned_user_assigned
    content {
      type         = identity.value.type
      identity_ids = identity.value.user_assigned_resource_ids
    }
  }
  
  ## Resources that only support SystemAssigned
  dynamic "identity" {
    for_each = identity.managed_identities.system_assigned
    content {
      type = identity.value.type
    }
  }
  
  ## Resources that only support UserAssigned
  dynamic "identity" {
    for_each = local.managed_identities.user_assigned
    content {
      type         = identity.value.type
      identity_ids = identity.value.user_assigned_resource_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

Please note that you will need to ensure that the User-Defined Types for Role Assignments & Locks also are present in your module file for this interface to work correctly.

  // ============== //
  //   Parameters   //
  // ============== //
  
  @description('Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible.')
  param privateEndpoints privateEndpointType
  
  // ============= //
  //   Resources   //
  // ============= //
  
  module >singularMainResourceType<_privateEndpoints 'br/public:avm/res/network/private-endpoint:X.Y.Z' = [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
    }
  }]
  
  // =============== //
  //   Definitions   //
  // =============== //
  
  type privateEndpointType = {
    @description('Optional. The name of the private endpoint.')
    name: string?
  
    @description('Optional. The location to deploy the private endpoint to.')
    location: string?
  
    @description('Optional. The name of the private link connection to create.')
    privateLinkServiceConnectionName: string?
  
    // Variant 1: A default service can be assumed (i.e., for services that only have one private endpoint type)
    @description('Optional. The subresource to deploy the private endpoint for. For example "vault", "mysqlServer" or "dataFactory".')
    service: string?
  
    @description('Required. Resource ID of the subnet where the endpoint needs to be created.')
    subnetResourceId: string
  
    @description('Optional. The private DNS zone group to configure for the private endpoint.')
    privateDnsZoneGroup: {
      @description('Optional. The name of the Private DNS Zone Group.')
      name: string?
  
      @description('Required. The private DNS zone groups to associate the private endpoint. A DNS zone group can support up to 5 DNS zones.')
      privateDnsZoneGroupConfigs: {
        @description('Optional. The name of the private DNS zone group config.')
        name: string?
  
        @description('Required. The resource id of the private DNS zone.')
        privateDnsZoneResourceId: string
      }[]
    }?
  
    @description('Optional. If Manual Private Link Connection is required.')
    isManualConnection: bool?
  
    @description('Optional. A message passed to the owner of the remote resource with the manual connection request.')
    @maxLength(140)
    manualConnectionRequestMessage: string?
  
    @description('Optional. Custom DNS configurations.')
    customDnsConfigs: {
      @description('Optional. FQDN that resolves to private endpoint IP address.')
      fqdn: string?
  
      @description('Required. A list of private IP addresses of the private endpoint.')
      ipAddresses: string[]
    }[]?
  
    @description('Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints.')
    ipConfigurations: {
      @description('Required. The name of the resource that is unique within a resource group.')
      name: string
  
      @description('Required. Properties of private endpoint IP configurations.')
      properties: {
        @description('Required. The ID of a group obtained from the remote resource that this private endpoint should connect to.')
        groupId: string
  
        @description('Required. The member name of a group obtained from the remote resource that this private endpoint should connect to.')
        memberName: string
  
        @description('Required. A private IP address obtained from the private endpoint\'s subnet.')
        privateIPAddress: string
      }
    }[]?
  
    @description('Optional. Application security groups in which the private endpoint IP configuration is included.')
    applicationSecurityGroupResourceIds: string[]?
  
    @description('Optional. The custom name of the network interface attached to the private endpoint.')
    customNetworkInterfaceName: string?
  
    @description('Optional. Specify the type of lock.')
    lock: lockType?
  
    @description('Optional. Array of role assignments to create.')
    roleAssignments: roleAssignmentType[]?
  
    @description('Optional. Tags to be applied on all resources/resource groups in this deployment.')
    tags: object?
  
    @description('Optional. Enable/Disable usage telemetry for module.')
    enableTelemetry: bool?
  
    @description('Optional. Specify if you want to deploy the Private Endpoint into a different resource group than the main resource.')
    resourceGroupName: string?
  }[]?
  

  // ============== //
  //   Parameters   //
  // ============== //
  
  @description('Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible.')
  param privateEndpoints privateEndpointType
  
  // ============= //
  //   Resources   //
  // ============= //
  
  module >singularMainResourceType<_privateEndpoints 'br/public:avm/res/network/private-endpoint:X.Y.Z' = [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
    }
  }]
  
  // =============== //
  //   Definitions   //
  // =============== //
  
  type privateEndpointType = {
    @description('Optional. The name of the private endpoint.')
    name: string?
  
    @description('Optional. The location to deploy the private endpoint to.')
    location: string?
  
    @description('Optional. The name of the private link connection to create.')
    privateLinkServiceConnectionName: string?
  
    // Variant 2: A default subresource cannot be assumed (i.e., for services that have more than one subresource, like Storage Account with Blob (blob, table, queue, file, ...)
    @description('Required. The subresource to deploy the private endpoint for. For example "blob", "table", "queue" or "file".')
    service: string
  
    @description('Required. Resource ID of the subnet where the endpoint needs to be created.')
    subnetResourceId: string
  
    @description('Optional. The private DNS zone group to configure for the private endpoint.')
    privateDnsZoneGroup: {
      @description('Optional. The name of the Private DNS Zone Group.')
      name: string?
  
      @description('Required. The private DNS zone groups to associate the private endpoint. A DNS zone group can support up to 5 DNS zones.')
      privateDnsZoneGroupConfigs: {
        @description('Optional. The name of the private DNS zone group config.')
        name: string?
  
        @description('Required. The resource id of the private DNS zone.')
        privateDnsZoneResourceId: string
      }[]
    }?
  
    @description('Optional. If Manual Private Link Connection is required.')
    isManualConnection: bool?
  
    @description('Optional. A message passed to the owner of the remote resource with the manual connection request.')
    @maxLength(140)
    manualConnectionRequestMessage: string?
  
    @description('Optional. Custom DNS configurations.')
    customDnsConfigs: {
      @description('Optional. FQDN that resolves to private endpoint IP address.')
      fqdn: string?
  
      @description('Required. A list of private IP addresses of the private endpoint.')
      ipAddresses: string[]
    }[]?
  
    @description('Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints.')
    ipConfigurations: {
      @description('Required. The name of the resource that is unique within a resource group.')
      name: string
  
      @description('Required. Properties of private endpoint IP configurations.')
      properties: {
        @description('Required. The ID of a group obtained from the remote resource that this private endpoint should connect to.')
        groupId: string
  
        @description('Required. The member name of a group obtained from the remote resource that this private endpoint should connect to.')
        memberName: string
  
        @description('Required. A private IP address obtained from the private endpoint\'s subnet.')
        privateIPAddress: string
      }
    }[]?
  
    @description('Optional. Application security groups in which the private endpoint IP configuration is included.')
    applicationSecurityGroupResourceIds: string[]?
  
    @description('Optional. The custom name of the network interface attached to the private endpoint.')
    customNetworkInterfaceName: string?
  
    @description('Optional. Specify the type of lock.')
    lock: lockType?
  
    @description('Optional. Array of role assignments to create.')
    roleAssignments: roleAssignmentType[]?
  
    @description('Optional. Tags to be applied on all resources/resource groups in this deployment.')
    tags: object?
  
    @description('Optional. Enable/Disable usage telemetry for module.')
    enableTelemetry: bool?
  
    @description('Optional. Specify if you want to deploy the Private Endpoint into a different resource group than the main resource.')
    resourceGroupName: string?
  }[]?
  
privateEndpoints: {
    {
      name: 'myPeName'
      privateLinkServiceConnectionName: 'myPrivateLinkConnectionName'
      lock: 'CanNotDelete'
      tags: {
        'hidden-title': 'This is visible in the resource name'
      }
      service: 'vault'
      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'
    }
  }
  
# 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.
  
  # This variable is used to determine if the private_dns_zone_group block should be included,
  # or if it is to be managed externally, e.g. using Azure Policy.
  # https://github.com/Azure/terraform-azurerm-avm-res-keyvault-vault/issues/32
  # Alternatively you can use AzAPI, which does not have this issue.
  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({
        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   = string  # NOTE: `subresource_name` can be excluded if the resource does not support multiple sub resource types (e.g. storage account supports blob, queue, etc)
      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
      })), {})
    }))
    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.
    - `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` - 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 where the resources will be deployed. Defaults to the resource group of the Key Vault.
  - `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.
  DESCRIPTION
  }
  
  # The PE resource when we are managing the private_dns_zone_group block:
  resource "azurerm_private_endpoint" "this" {
    for_each                      = { for k, v in var.private_endpoints : k => v if var.private_endpoints_manage_dns_zone_group }
    name                          = each.value.name != null ? each.value.name : "pep-${var.name}"
    location                      = each.value.location != null ? each.value.location : var.location
    resource_group_name           = each.value.resource_group_name != null ? each.value.resource_group_name : var.resource_group_name
    subnet_id                     = each.value.subnet_resource_id
    custom_network_interface_name = each.value.network_interface_name
    tags                          = each.value.tags
  
    private_service_connection {
      name                           = each.value.private_service_connection_name != null ? each.value.private_service_connection_name : "pse-${var.name}"
      private_connection_resource_id = azurerm_key_vault.this.id
      is_manual_connection           = false
      subresource_names              = ["MYSERVICE"] # map to each.value.subresource_name if there are multiple services.
    }
  
    dynamic "private_dns_zone_group" {
      for_each = length(each.value.private_dns_zone_resource_ids) > 0 ? ["this"] : []
  
      content {
        name                 = each.value.private_dns_zone_group_name
        private_dns_zone_ids = each.value.private_dns_zone_resource_ids
      }
    }
  
    dynamic "ip_configuration" {
      for_each = each.value.ip_configurations
  
      content {
        name               = ip_configuration.value.name
        subresource_name   = "MYSERVICE" # map to each.value.subresource_name if there are multiple services.
        member_name        = "MYSERVICE" # map to each.value.subresource_name if there are multiple services.
        private_ip_address = ip_configuration.value.private_ip_address
      }
    }
  }
  
  # The PE resource when we are managing **not** the private_dns_zone_group block:
  resource "azurerm_private_endpoint" "this_unmanaged_dns_zone_groups" {
    for_each = { for k, v in var.private_endpoints : k => v if !var.private_endpoints_manage_dns_zone_group }
  
    # ... repeat configuration above
    # **omitting the private_dns_zone_group block**
    # then add the following lifecycle block to ignore changes to the private_dns_zone_group block
  
    lifecycle {
      ignore_changes = [private_dns_zone_group]
    }
  }
  
  # Private endpoint application security group associations.
  # We merge the nested maps from private endpoints and application security group associations into a single map.
  locals {
    private_endpoint_application_security_group_associations = { for assoc in flatten([
      for pe_k, pe_v in var.private_endpoints : [
        for asg_k, asg_v in pe_v.application_security_group_associations : {
          asg_key         = asg_k
          pe_key          = pe_k
          asg_resource_id = asg_v
        }
      ]
    ]) : "${assoc.pe_key}-${assoc.asg_key}" => assoc }
  }
  
  resource "azurerm_private_endpoint_application_security_group_association" "this" {
    for_each                      = local.private_endpoint_application_security_group_associations
    private_endpoint_id           = azurerm_private_endpoint.this[each.value.pe_key].id
    application_security_group_id = each.value.asg_resource_id
  }
  
  # You need an additional resource when not managing private_dns_zone_group with this module:
  
  # In your output you need to select the correct resource based on the value of var.private_endpoints_manage_dns_zone_group:
  output "private_endpoints" {
    value       = var.private_endpoints_manage_dns_zone_group ? azurerm_private_endpoint.this_managed_dns_zone_groups : azurerm_private_endpoint.this_unmanaged_dns_zone_groups
    description = <<DESCRIPTION
  A map of the private endpoints created.
  DESCRIPTION
  }
  
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


  // ============== //
  //   Parameters   //
  // ============== //
  
  @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
    }
  }
  
  // =============== //
  //   Definitions   //
  // =============== //
  
  type customerManagedKeyType = {
    @description('Required. The resource ID of a key vault to reference a customer managed key for encryption from.')
    keyVaultResourceId: string
  
    @description('Required. The name of the customer managed key to use for encryption.')
    keyName: string
  
    @description('Optional. The version of the customer managed key to reference for encryption. If not provided, using \'latest\'.')
    keyVersion: string?
  
    @description('Optional. User assigned identity to use when fetching the customer managed key. Required if no system assigned identity is available for use.')
    userAssignedIdentityResourceId: string?
  }?
  
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}'
  }
  
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
  }
  
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

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