Interfaces
Below are the interfaces/schemas for the AVM Resource Modules features/extension resources as detailed in RMFR4 and RMFR5
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.
// ============== //
// 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
// ============== //
// 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 suitabledepends_on
values should be set. Note that, during adestroy
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.
@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
// ============== //
// 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
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.
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
- A module owner MAY chose to expose additional properties of the Private Endpoint resource
- 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
- 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
// ============== //
// 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}"
}
This interface is a SHOULD instead of a MUST and therefore the AVM core team have not mandated a interface schema to use.