Terraform Interfaces
This chapter details the interfaces/schemas for the AVM Resource Modules features/extension resources as referenced in RMFR4 and RMFR5.
Diagnostic Settings
Important
Allowed values for logs and metric categories or category groups MUST NOT be specified to keep the module implementation evergreen for any new categories or category groups added by RPs, without module owners having to update a list of allowed values and cut a new release of their module.
variable "diagnostic_settings" {
type = map(object({
name = optional(string, null)
logs = optional(set(object({
category = optional(string, null)
category_group = optional(string, null)
enabled = optional(bool, true)
retention_policy = optional(object({
days = optional(number, 0)
enabled = optional(bool, false)
}), {})
})), [])
metrics = optional(set(object({
category = optional(string, null)
enabled = optional(bool, true)
retention_policy = optional(object({
days = optional(number, 0)
enabled = optional(bool, false)
}), {})
})), [])
log_analytics_destination_type = optional(string, "Dedicated")
workspace_resource_id = optional(string, null)
storage_account_resource_id = optional(string, null)
event_hub_authorization_rule_resource_id = optional(string, null)
event_hub_name = optional(string, null)
marketplace_partner_resource_id = optional(string, null)
}))
default = {}
nullable = false
validation {
condition = alltrue([for _, v in var.diagnostic_settings : contains(["Dedicated", "AzureDiagnostics"], v.log_analytics_destination_type)])
error_message = "Log analytics destination type must be one of: 'Dedicated', 'AzureDiagnostics'."
}
validation {
condition = alltrue([
for _, v in var.diagnostic_settings : alltrue([
for l in v.logs : (l.category != null) != (l.category_group != null)
])
])
error_message = "Each log entry must set exactly one of `category` or `category_group`."
}
validation {
condition = alltrue(
[
for _, v in var.diagnostic_settings :
v.workspace_resource_id != null || v.storage_account_resource_id != null || v.event_hub_authorization_rule_resource_id != null || v.marketplace_partner_resource_id != null
]
)
error_message = "At least one of `workspace_resource_id`, `storage_account_resource_id`, `marketplace_partner_resource_id`, or `event_hub_authorization_rule_resource_id`, must be set."
}
validation {
condition = alltrue([
for _, v in var.diagnostic_settings :
v.workspace_resource_id == null || can(provider::azapi::parse_resource_id("Microsoft.OperationalInsights/workspaces", v.workspace_resource_id))
])
error_message = "Each `workspace_resource_id` must be a valid Log Analytics workspace resource ID, or null."
}
validation {
condition = alltrue([
for _, v in var.diagnostic_settings :
v.storage_account_resource_id == null || can(provider::azapi::parse_resource_id("Microsoft.Storage/storageAccounts", v.storage_account_resource_id))
])
error_message = "Each `storage_account_resource_id` must be a valid storage account resource ID, or null."
}
validation {
condition = alltrue([
for _, v in var.diagnostic_settings :
v.event_hub_authorization_rule_resource_id == null || can(provider::azapi::parse_resource_id("Microsoft.EventHub/namespaces/authorizationRules", v.event_hub_authorization_rule_resource_id))
])
error_message = "Each `event_hub_authorization_rule_resource_id` must be a valid Event Hub namespace authorization rule resource ID, or null."
}
description = <<DESCRIPTION
A map of diagnostic settings to create on the resource. The map key is deliberately arbitrary to avoid issues where map keys maybe unknown at plan time.
- `name` - (Optional) The name of the diagnostic setting. One will be generated if not set, however this will not be unique if you want to create multiple diagnostic setting resources.
- `logs` - (Optional) A set of log entries to send to the destination. Each entry has the following attributes:
- `category` - (Optional) The name of a specific log category to enable. Mutually exclusive with `category_group`.
- `category_group` - (Optional) The name of a log category group to enable (for example, `allLogs` or `audit`). Mutually exclusive with `category`.
- `enabled` - (Optional) Whether the log entry is enabled. Defaults to `true`.
- `retention_policy` - (Optional) The retention policy for the log entry.
- `days` - (Optional) The retention period in days. Defaults to `0` (retain indefinitely).
- `enabled` - (Optional) Whether the retention policy is enabled. Defaults to `false`.
- `metrics` - (Optional) A set of metric entries to send to the destination. Each entry has the following attributes:
- `category` - (Optional) The name of the metric category to enable.
- `enabled` - (Optional) Whether the metric entry is enabled. Defaults to `true`.
- `retention_policy` - (Optional) The retention policy for the metric entry, with the same `days` and `enabled` attributes as `logs.retention_policy`.
- `log_analytics_destination_type` - (Optional) The destination type for the diagnostic setting. Possible values are `Dedicated` and `AzureDiagnostics`. Defaults to `Dedicated`.
- `workspace_resource_id` - (Optional) The resource ID of the log analytics workspace to send logs and metrics to.
- `storage_account_resource_id` - (Optional) The resource ID of the storage account to send logs and metrics to.
- `event_hub_authorization_rule_resource_id` - (Optional) The resource ID of the event hub authorization rule to send logs and metrics to.
- `event_hub_name` - (Optional) The name of the event hub. If none is specified, the default event hub will be selected.
- `marketplace_partner_resource_id` - (Optional) The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs.
DESCRIPTION
}
module "avm_interfaces" {
source = "Azure/avm-utl-interfaces/azure"
version = "0.6.0" # check latest version at the time of use
diagnostic_settings_v2 = var.diagnostic_settings
diagnostic_settings_scope = azapi_resource.this.id
}
# Sample resource
resource "azapi_resource" "diagnostic_settings" {
for_each = module.avm_interfaces.diagnostic_settings_azapi_v2
type = each.value.type
name = each.value.name
parent_id = each.value.parent_id
body = each.value.body
}
diagnostic_settings = {
diag_setting_1 = {
name = "diagSetting1"
logs = [
{
category_group = "allLogs"
enabled = true
}
]
metrics = [
{
category = "AllMetrics"
enabled = true
}
]
log_analytics_destination_type = "Dedicated"
workspace_resource_id = "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.OperationalInsights/workspaces/{workspaceName}"
storage_account_resource_id = "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{storageAccountName}"
event_hub_authorization_rule_resource_id = "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.EventHub/namespaces/{namespaceName}/eventhubs/{eventHubName}/authorizationrules/{authorizationRuleName}"
event_hub_name = "{eventHubName}"
marketplace_partner_resource_id = "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{partnerResourceProvider}/{partnerResourceType}/{partnerResourceName}"
}
}
Note
In the provided example for Diagnostic Settings, both logs and metrics are enabled for the associated resource. However, it is IMPORTANT to note that certain resources may not support both diagnostic setting types/categories. In such cases, the resource configuration MUST be modified accordingly to ensure proper functionality and compliance with system requirements.
Role Assignments
variable "role_assignments" {
type = map(object({
name = optional(string, null)
role_definition_id_or_name = string
principal_id = string
description = optional(string, null)
skip_service_principal_aad_check = optional(bool, false)
condition = optional(string, null)
condition_version = optional(string, null)
delegated_managed_identity_resource_id = optional(string, null)
principal_type = optional(string, null)
}))
default = {}
nullable = false
description = <<DESCRIPTION
A map of role assignments to create on the <RESOURCE>. The map key is deliberately arbitrary to avoid issues where map keys maybe unknown at plan time.
- `name` - (Optional) The name of the role assignment. If not set, a random UUID will be generated. Changing this forces the creation of a new resource.
- `role_definition_id_or_name` - The ID or name of the role definition to assign to the principal.
- `principal_id` - The ID of the principal to assign the role to.
- `description` - (Optional) The description of the role assignment.
- `skip_service_principal_aad_check` - (Optional) If set to true, skips the Azure Active Directory check for the service principal in the tenant. Defaults to false.
- `condition` - (Optional) The condition which will be used to scope the role assignment.
- `condition_version` - (Optional) The version of the condition syntax. Leave as `null` if you are not using a condition, if you are then valid values are '2.0'.
- `delegated_managed_identity_resource_id` - (Optional) The delegated Azure Resource Id which contains a Managed Identity. Changing this forces a new resource to be created. This field is only used in cross-tenant scenario.
- `principal_type` - (Optional) The type of the `principal_id`. Possible values are `User`, `Group` and `ServicePrincipal`. It is necessary to explicitly set this attribute when creating role assignments if the principal creating the assignment is constrained by ABAC rules that filters on the PrincipalType attribute.
> Note: only set `skip_service_principal_aad_check` to true if you are assigning a role to a service principal.
DESCRIPTION
validation {
condition = alltrue([
for _, v in var.role_assignments :
v.delegated_managed_identity_resource_id == null || can(provider::azapi::parse_resource_id("Microsoft.ManagedIdentity/userAssignedIdentities", v.delegated_managed_identity_resource_id))
])
error_message = "Each `role_assignments[*].delegated_managed_identity_resource_id` must be a valid user-assigned managed identity resource ID, or null."
}
}
module "avm_interfaces" {
source = "Azure/avm-utl-interfaces/azure"
version = "0.6.0" # check latest version at the time of use
role_assignments = var.role_assignments
role_assignment_definition_scope = azapi_resource.this.id
}
# Example resource declaration
resource "azapi_resource" "role_assignments" {
for_each = module.avm_interfaces.role_assignments_azapi
type = each.value.type
name = each.value.name
parent_id = each.value.parent_id
body = each.value.body
retry = {
error_message_regex = ["ScopeLocked"] # retry if a lock is in place on the scope and has only just been removed
interval_seconds = 15
max_interval_seconds = 60
}
timeouts {
delete = "5m"
}
}
role_assignments = {
role_assignment_1 = {
role_definition_id_or_name = "Contributor"
principal_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
skip_service_principal_aad_check = true
},
role_assignment_2 = {
role_definition_id_or_name = "Storage Blob Data Reader"
principal_id = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
description = "Example role assignment 2 of reader role"
skip_service_principal_aad_check = false
condition = "@Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase 'foo_storage_container'"
condition_version = "2.0"
}
}
Details on child, extension and cross-referenced resources:
- Modules MUST support Role Assignments on child, extension and cross-referenced resources as well as the primary resource via parameters/variables
Resource Locks
variable "lock" {
type = object({
kind = string
name = optional(string, null)
})
default = null
description = <<DESCRIPTION
Controls the Resource Lock configuration for this resource. The following properties can be specified:
- `kind` - (Required) The type of lock. Possible values are `\"CanNotDelete\"` and `\"ReadOnly\"`.
- `name` - (Optional) The name of the lock. If not specified, a name will be generated based on the `kind` value. Changing this forces the creation of a new resource.
DESCRIPTION
validation {
condition = var.lock != null ? contains(["CanNotDelete", "ReadOnly"], var.lock.kind) : true
error_message = "Lock kind must be either `\"CanNotDelete\"` or `\"ReadOnly\"`."
}
}
module "avm_interfaces" {
source = "Azure/avm-utl-interfaces/azure"
version = "0.6.0" # check latest version at the time of use
lock = var.lock
lock_scope = azapi_resource.this.id
}
# Example resource implementation
resource "azapi_resource" "lock" {
count = var.lock != null ? 1 : 0
type = module.avm_interfaces.lock_azapi.type
name = module.avm_interfaces.lock_azapi.name
parent_id = module.avm_interfaces.lock_azapi.parent_id
body = module.avm_interfaces.lock_azapi.body
}
lock = {
name = "lock-{resourcename}" # optional
type = "CanNotDelete"
}
Details on child and extension resources:
- Locks SHOULD be able to be set for child resources of the primary resource in resource modules
Details on cross-referenced resources:
- Locks MUST be automatically applied to cross-referenced resources if the primary resource has a lock applied.
- This MUST also be able to be turned off for each of the cross-referenced resources by the module consumer via a parameter/variable if they desire
An example of this is a Key Vault module that has a Private Endpoints enabled. If a lock is applied to the Key Vault via the lock parameter/variable then the lock should also be applied to the Private Endpoint automatically, unless the privateEndpointLock/private_endpoint_lock (example name) parameter/variable is set to None
Important
In Terraform, locks become part of the resource graph and suitable depends_on values should be set. Note that, during a destroy operation, Terraform will remove the locks before removing the resource itself, reducing the usefulness of the lock somewhat. Also note, due to eventual consistency in Azure, use of locks can cause destroy operations to fail as the lock may not have been fully removed by the time the destroy operation is executed.
Tags
variable "tags" {
type = map(string)
default = null
description = "(Optional) Tags of the resource."
}
tags = {
key = "value"
"another-key" = "another-value"
integers = 123
}
Details on child, extension and cross-referenced resources:
- Tags MUST be automatically applied to child, extension and cross-referenced resources, if tags are applied to the primary resource.
- By default, all tags set for the primary resource will automatically be passed down to child, extension and cross-referenced resources.
- This MUST be able to be overridden by the module consumer so they can specify alternate tags for child, extension and cross-referenced resources, if they desire via a parameter/variable
- If overridden by the module consumer, no merge/union of tags will take place from the primary resource and only the tags specified for the child, extension and cross-referenced resources will be applied
Managed Identities
variable "managed_identities" {
type = object({
system_assigned = optional(bool, false)
user_assigned_resource_ids = optional(set(string), [])
})
default = {}
nullable = false
description = <<DESCRIPTION
Controls the Managed Identity configuration on this resource. The following properties can be specified:
- `system_assigned` - (Optional) Specifies if the System Assigned Managed Identity should be enabled.
- `user_assigned_resource_ids` - (Optional) Specifies a list of User Assigned Managed Identity resource IDs to be assigned to this resource.
DESCRIPTION
validation {
condition = alltrue([
for id in var.managed_identities.user_assigned_resource_ids :
can(provider::azapi::parse_resource_id("Microsoft.ManagedIdentity/userAssignedIdentities", id))
])
error_message = "Each entry in `managed_identities.user_assigned_resource_ids` must be a valid user-assigned managed identity resource ID."
}
}
module "avm_interfaces" {
source = "Azure/avm-utl-interfaces/azure"
version = "0.6.0" # check latest version at the time of use
managed_identities = var.managed_identities
}
# Example identity block on the parent azapi_resource. The avm_interfaces
# module returns a single object with the correct `type` and `identity_ids`
# values, including the case when no identity is configured (in which case
# the for_each is empty and no identity block is rendered).
#
# Note: AzAPI accepts a single `identity` block. The dynamic block below
# renders zero or one block depending on whether a managed identity is
# configured. The same pattern works for resources that only support
# SystemAssigned or only UserAssigned identities.
resource "azapi_resource" "this" {
# ...other arguments...
dynamic "identity" {
for_each = module.avm_interfaces.managed_identities_azapi != null ? [module.avm_interfaces.managed_identities_azapi] : []
content {
type = identity.value.type
identity_ids = identity.value.identity_ids
}
}
}
managed_identities = {
system_assigned = true
user_assigned_resource_ids = [
"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}",
"/subscriptions/{subscriptionId2}/resourceGroups/{resourceGroupName2}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName2}"
]
}
Reason for differences in User Assigned data type in languages:
- We do not forsee the Managed Identity Resource Provider team to ever add additional properties within the empty object (
{}) value required on the input of a User Assigned Managed Identity. - In Bicep we therefore have removed the need for this to be declared and just converted it to a simple array of Resource IDs
- However, in Terraform we have left it as a object/map as this simplifies
for_eachand other loop mechanisms and provides more consistency in plan, apply, destroy operations- Especially when adding, removing or changing the order of the User Assigned Managed Identities as they are declared
Private Endpoints
# In this example we only support one service, e.g. Key Vault.
# If your service has multiple private endpoint services, then expose the service name.
variable "private_endpoints_manage_dns_zone_group" {
type = bool
default = true
nullable = false
description = "Whether to manage private DNS zone groups with this module. If set to false, you must manage private DNS zone groups externally, e.g. using Azure Policy."
}
variable "private_endpoints" {
type = map(object({
name = optional(string, null)
role_assignments = optional(map(object({
name = optional(string, null)
role_definition_id_or_name = string
principal_id = string
description = optional(string, null)
skip_service_principal_aad_check = optional(bool, false)
condition = optional(string, null)
condition_version = optional(string, null)
delegated_managed_identity_resource_id = optional(string, null)
principal_type = optional(string, null)
})), {})
lock = optional(object({
kind = string
name = optional(string, null)
}), null)
tags = optional(map(string), null)
subnet_resource_id = string
subresource_name = optional(string, null) # only required if the parent resource exposes more than one private endpoint sub-resource
private_dns_zone_group_name = optional(string, "default")
private_dns_zone_resource_ids = optional(set(string), [])
application_security_group_associations = optional(map(string), {})
private_service_connection_name = optional(string, null)
network_interface_name = optional(string, null)
location = optional(string, null)
resource_group_name = optional(string, null)
ip_configurations = optional(map(object({
name = string
private_ip_address = string
member_name = optional(string)
})), {})
}))
default = {}
nullable = false
description = <<DESCRIPTION
A map of private endpoints to create on the Key Vault. The map key is deliberately arbitrary to avoid issues where map keys maybe unknown at plan time.
- `name` - (Optional) The name of the private endpoint. One will be generated if not set.
- `role_assignments` - (Optional) A map of role assignments to create on the private endpoint. The map key is deliberately arbitrary to avoid issues where map keys maybe unknown at plan time. See `var.role_assignments` for more information.
- `name` - (Optional) The name of the role assignment. If not set, a random UUID will be generated. Changing this forces the creation of a new resource.
- `role_definition_id_or_name` - The ID or name of the role definition to assign to the principal.
- `principal_id` - The ID of the principal to assign the role to.
- `description` - (Optional) The description of the role assignment.
- `skip_service_principal_aad_check` - (Optional) If set to true, skips the Azure Active Directory check for the service principal in the tenant. Defaults to false.
- `condition` - (Optional) The condition which will be used to scope the role assignment.
- `condition_version` - (Optional) The version of the condition syntax. Leave as `null` if you are not using a condition, if you are then valid values are '2.0'.
- `delegated_managed_identity_resource_id` - (Optional) The delegated Azure Resource Id which contains a Managed Identity. Changing this forces a new resource to be created. This field is only used in cross-tenant scenario.
- `principal_type` - (Optional) The type of the `principal_id`. Possible values are `User`, `Group` and `ServicePrincipal`. It is necessary to explicitly set this attribute when creating role assignments if the principal creating the assignment is constrained by ABAC rules that filters on the PrincipalType attribute.
- `lock` - (Optional) The lock level to apply to the private endpoint. Default is `None`. Possible values are `None`, `CanNotDelete`, and `ReadOnly`.
- `kind` - (Required) The type of lock. Possible values are `\"CanNotDelete\"` and `\"ReadOnly\"`.
- `name` - (Optional) The name of the lock. If not specified, a name will be generated based on the `kind` value. Changing this forces the creation of a new resource.
- `tags` - (Optional) A mapping of tags to assign to the private endpoint.
- `subnet_resource_id` - The resource ID of the subnet to deploy the private endpoint in.
- `subresource_name` (Optional) - The name of the sub resource for the private endpoint.
- `private_dns_zone_group_name` - (Optional) The name of the private DNS zone group. One will be generated if not set.
- `private_dns_zone_resource_ids` - (Optional) A set of resource IDs of private DNS zones to associate with the private endpoint. If not set, no zone groups will be created and the private endpoint will not be associated with any private DNS zones. DNS records must be managed external to this module.
- `application_security_group_resource_ids` - (Optional) A map of resource IDs of application security groups to associate with the private endpoint. The map key is deliberately arbitrary to avoid issues where map keys maybe unknown at plan time.
- `private_service_connection_name` - (Optional) The name of the private service connection. One will be generated if not set.
- `network_interface_name` - (Optional) The name of the network interface. One will be generated if not set.
- `location` - (Optional) The Azure location where the resources will be deployed. Defaults to the location of the resource group.
- `resource_group_name` - (Optional) The resource group resource ID where the private endpoint resources will be deployed. Defaults to the resource group of the parent resource.
- `ip_configurations` - (Optional) A map of IP configurations to create on the private endpoint. If not specified the platform will create one. The map key is deliberately arbitrary to avoid issues where map keys maybe unknown at plan time.
- `name` - The name of the IP configuration.
- `private_ip_address` - The private IP address of the IP configuration.
- `member_name` - (Optional) The private IP configuration member name.
DESCRIPTION
validation {
condition = alltrue([
for _, v in var.private_endpoints :
can(provider::azapi::parse_resource_id("Microsoft.Network/virtualNetworks/subnets", v.subnet_resource_id))
])
error_message = "Each `private_endpoints[*].subnet_resource_id` must be a valid subnet resource ID."
}
validation {
condition = alltrue(flatten([
for _, v in var.private_endpoints : [
for id in v.private_dns_zone_resource_ids :
can(provider::azapi::parse_resource_id("Microsoft.Network/privateDnsZones", id))
]
]))
error_message = "Each entry in `private_endpoints[*].private_dns_zone_resource_ids` must be a valid private DNS zone resource ID."
}
validation {
condition = alltrue(flatten([
for _, v in var.private_endpoints : [
for _, asg in v.application_security_group_associations :
can(provider::azapi::parse_resource_id("Microsoft.Network/applicationSecurityGroups", asg))
]
]))
error_message = "Each value in `private_endpoints[*].application_security_group_associations` must be a valid application security group resource ID."
}
validation {
condition = alltrue(flatten([
for _, v in var.private_endpoints : [
for _, ra in v.role_assignments :
ra.delegated_managed_identity_resource_id == null || can(provider::azapi::parse_resource_id("Microsoft.ManagedIdentity/userAssignedIdentities", ra.delegated_managed_identity_resource_id))
]
]))
error_message = "Each `private_endpoints[*].role_assignments[*].delegated_managed_identity_resource_id` must be a valid user-assigned managed identity resource ID, or null."
}
}
module "avm_interfaces" {
source = "Azure/avm-utl-interfaces/azure"
version = "0.6.0" # check latest version at the time of use
private_endpoints = var.private_endpoints
private_endpoints_scope = azapi_resource.this.id
role_assignment_definition_scope = azapi_resource.this.id
}
resource "azapi_resource" "private_endpoints" {
for_each = module.avm_interfaces.private_endpoints_azapi
location = azapi_resource.this.location
name = each.value.name
parent_id = coalesce(var.private_endpoints[each.key].resource_group_name, azapi_resource.this.parent_id)
type = each.value.type
body = each.value.body
retry = {
error_message_regex = ["ScopeLocked"] # This will retry if a lock is in place on the resource group, and has only just been removed
}
timeouts {
delete = "5m"
}
}
resource "azapi_resource" "private_endpoint_locks" {
for_each = module.avm_interfaces.lock_private_endpoint_azapi
name = each.value.name
parent_id = azapi_resource.private_endpoints[each.value.pe_key].id
type = each.value.type
body = each.value.body
depends_on = [
azapi_resource.private_dns_zone_groups,
azapi_resource.private_endpoint_role_assignments
]
}
resource "azapi_resource" "private_dns_zone_groups" {
for_each = module.avm_interfaces.private_dns_zone_groups_azapi
name = each.value.name
parent_id = azapi_resource.private_endpoints[each.key].id
type = each.value.type
body = each.value.body
retry = {
error_message_regex = ["ScopeLocked"] # This will retry if a lock is in place on the resource group, and has only just been removed
interval_seconds = 15
max_interval_seconds = 60
}
timeouts {
delete = "5m"
}
}
resource "azapi_resource" "private_endpoint_role_assignments" {
for_each = module.avm_interfaces.role_assignments_private_endpoint_azapi
name = each.value.name
parent_id = azapi_resource.private_endpoints[each.value.pe_key].id
type = each.value.type
body = each.value.body
retry = {
error_message_regex = ["ScopeLocked"]
interval_seconds = 15
max_interval_seconds = 60
}
timeouts {
delete = "5m"
}
}
private_endpoints = {
pe1 = {
role_assignments = {} # see interfaces/role assignments
lock = {} # see interfaces/resource locks
tags = {} # see interfaces/tags
subnet_resource_id = "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/virtualNetworks/{vnetName}/subnets/{subnetName}"
private_dns_zone_resource_ids = [
"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/privateDnsZones/{dnsZoneName}"
]
application_security_group_associations = {
asg1 = "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/applicationSecurityGroups/{asgName}"
}
network_interface_name = "nic1"
ip_configurations = {
ipconfig1 = {
name = "ipconfig1"
group_id = "vault"
member_name = "default"
private_ip_address = "10.0.0.7"
}
}
}
}
Notes:
- The properties defined in the schema above are the minimum amount of properties expected to be exposed for Private Endpoints in AVM Resource Modules.
- A module owner MAY chose to expose additional properties of the Private Endpoint resource.
- However, module owners considering this SHOULD contact the AVM core team first to consult on how the property should be exposed to avoid future breaking changes to the schema that may be enforced upon them.
- 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.
Customer Managed Keys
variable "customer_managed_key" {
type = object({
key_vault_resource_id = string
key_name = string
key_version = optional(string, null)
user_assigned_identity = optional(object({
resource_id = string
}), null)
})
default = null
validation {
condition = var.customer_managed_key == null || can(provider::azapi::parse_resource_id("Microsoft.KeyVault/vaults", var.customer_managed_key.key_vault_resource_id))
error_message = "`customer_managed_key.key_vault_resource_id` must be a valid Azure Key Vault resource ID."
}
validation {
condition = var.customer_managed_key == null || var.customer_managed_key.user_assigned_identity == null || can(provider::azapi::parse_resource_id("Microsoft.ManagedIdentity/userAssignedIdentities", var.customer_managed_key.user_assigned_identity.resource_id))
error_message = "`customer_managed_key.user_assigned_identity.resource_id` must be a valid user-assigned managed identity resource ID."
}
}
customer_managed_key = {
key_vault_resource_id: "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.KeyVault/vaults/{keyVaultName}"
key_name: "{keyName}"
key_version: "{keyVersion}"
user_assigned_identity_resource_id: "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{uamiName}"
}
Azure Monitor Alerts
Note
This interface is a SHOULD instead of a MUST and therefore the AVM core team have not mandated a interface schema to use.
AzAPI resource types
Important
Each resource_types key MUST be the snake_case form of the ARM resource type with the Microsoft. prefix dropped (for example Microsoft.Example/widgets/parts \u2192 example_widgets_parts). Each module MUST declare one optional(string, "...") field per azapi_resource (or equivalent AzAPI resource) it owns, defaulting each field to the latest tested API version. See TFFR6.
# `resource_types` keys vs Terraform resource labels
# -----------------------------------------------------------------------------
# These are two unrelated concepts:
#
# - Keys in `var.resource_types` name the AzAPI resource TYPE (e.g.
# `example_widgets`). They are derived from the ARM resource type by
# the naming rule below.
# - The Terraform resource LABEL (e.g. `azapi_resource.this`) names the
# graph node. The primary resource label MUST be `this` per TFRMNFR2.
#
# A primary-resource declaration therefore reads:
#
# resource "azapi_resource" "this" { # label per TFRMNFR2
# type = var.resource_types.example_widgets # key per the naming rule
# }
#
# The two MUST NOT be conflated. `this` is never a valid `resource_types` key.
#
# Naming rule for `resource_types` keys
# -----------------------------------------------------------------------------
# Each key MUST be the snake_case form of the ARM resource type with the
# `Microsoft.` prefix dropped. The provider namespace is rendered as a single
# lowercase token (no internal split) and each path segment after the
# provider is converted from camelCase to snake_case. Segments are joined
# with `_`:
#
# Microsoft.Example/widgets -> example_widgets
# Microsoft.Example/widgets/parts -> example_widgets_parts
# Microsoft.Example/widgets/parts/components -> example_widgets_parts_components
# Microsoft.Authorization/locks -> authorization_locks
# Microsoft.Authorization/roleAssignments -> authorization_role_assignments
# Microsoft.Insights/diagnosticSettings -> insights_diagnostic_settings
# Microsoft.KeyVault/vaults/secrets -> keyvault_vaults_secrets
# Microsoft.Network/virtualNetworks/subnets -> network_virtual_networks_subnets
#
# Submodules in the variable
# -----------------------------------------------------------------------------
# Every submodule the module instantiates gets a nested `optional(object({...}), {})`
# slot in `resource_types`, keyed by the submodule's primary ARM resource type
# (same naming rule). The slot's shape MUST match the submodule's own
# `resource_types` variable exactly. The parent MUST NOT repeat the submodule's
# defaults: the inner string attributes are declared as `optional(string)`
# with no default, so the submodule remains the single source of truth for
# its own tested API versions. Passing `null` (or omitting the key) yields
# the submodule's default.
# Root module example: manages `Microsoft.Example/widgets`, owns one extension
# resource (a lock), and instantiates a `parts` submodule that itself
# instantiates a `component` sibling submodule (per TFRMNFR1).
variable "resource_types" {
type = object({
example_widgets = optional(string, "Microsoft.Example/widgets@2024-01-01")
authorization_locks = optional(string, "Microsoft.Authorization/locks@2020-05-01")
example_widgets_parts = optional(object({
example_widgets_parts = optional(string)
example_widgets_parts_components = optional(object({
example_widgets_parts_components = optional(string)
}), {})
}), {})
})
default = {}
nullable = false
description = <<DESCRIPTION
Override the AzAPI `<provider>/<resource>@<api-version>` strings used by this module and its submodules. Each key defaults to a tested value; supply only the keys you want to override. Useful when targeting a sovereign cloud with older API versions, or when opting into a newer preview API.
- `example_widgets` - The primary widget managed by this module.
- `authorization_locks` - Management lock applied to the widget and its private endpoints.
- `example_widgets_parts` - Override slot for the `parts` submodule. Defaults live in the submodule; supply only the keys you want to override.
- `example_widgets_parts` - The part resource managed by the `parts` submodule.
- `example_widgets_parts_components` - Override slot for the grandchild `components` submodule. Defaults live in that submodule.
- `example_widgets_parts_components` - The component resource managed by the `components` submodule.
DESCRIPTION
}
# `type =` of every `azapi_resource` MUST come from `var.resource_types`,
# never a hard-coded string. The resource label (`this`) and the
# `resource_types` key (`example_widgets`) are independent concerns.
resource "azapi_resource" "this" {
type = var.resource_types.example_widgets
name = var.name
parent_id = var.parent_id
body = { /* ... */ }
response_export_values = []
}
# Cascade the nested slot through to the submodule unchanged. The submodule's
# `resource_types` variable has exactly the shape of the slot, so no
# repacking or renaming is required.
module "part" {
source = "./modules/part"
for_each = var.parts
name = each.value.name
parent_id = azapi_resource.this.id
resource_types = var.resource_types.example_widgets_parts
}
# Consumers override only the keys they need; defaults supply the rest.
# Passing `null` for any attribute (or omitting it) yields the default
# declared on the owning module's variable.
resource_types = {
# Pin the primary widget to a newer preview API version.
example_widgets = "Microsoft.Example/widgets@2025-06-01-preview"
# Override an API version inside the `parts` submodule.
example_widgets_parts = {
example_widgets_parts = "Microsoft.Example/widgets/parts@2023-01-01"
# Override an API version inside the grandchild `components` submodule
# of `parts` — the nested slot mirrors the submodule tree.
example_widgets_parts_components = {
example_widgets_parts_components = "Microsoft.Example/widgets/parts/components@2023-01-01"
}
}
}
Notes:
resource_typeskeys name the AzAPI resource type and are derived deterministically from the ARM type. They are independent of the Terraform resource label (see TFRMNFR2) \u2014thisis never a validresource_typeskey.- Submodules MUST declare their own
resource_typesvariable using the same naming rule for the resources they own. The parent MUST declare one nestedoptional(object({...}), {})slot per submodule it instantiates, shaped exactly like that submodule’s variable, and MUST cascade the slot through unchanged (see TFRMNFR1). The parent MUST NOT repeat the submodule’s defaults \u2014 the submodule remains the source of truth for its own tested API versions. - Defaults MUST be a stable (non-preview) API version unless the module’s primary resource only ships a preview API.
AzAPI retry
variable "retry" {
type = object({
error_message_regex = optional(list(string))
interval_seconds = optional(number)
max_interval_seconds = optional(number)
})
default = null
description = <<DESCRIPTION
Retry configuration applied to every `azapi` resource managed by the module (root resource and all submodules). Defaults to `null` (no custom retry).
- `error_message_regex` - (Optional) A list of regex patterns matching error messages that trigger a retry.
- `interval_seconds` - (Optional) Initial interval between retries in seconds.
- `max_interval_seconds` - (Optional) Maximum interval between retries in seconds.
See <https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/resource#retry> for full semantics.
DESCRIPTION
}
# Example resource implementation. `retry` is an attribute on `azapi_resource`,
# so the variable is assigned directly. The same pattern applies to every
# `azapi_resource` declared by the module, including those in submodules.
resource "azapi_resource" "this" {
type = var.resource_types.example_widgets
name = var.name
parent_id = var.parent_id
body = { /* ... */ }
retry = var.retry
response_export_values = []
}
# Cascade `retry` to every submodule the parent module instantiates so that a
# single override at the parent level propagates everywhere.
module "child" {
source = "./modules/child"
retry = var.retry
# ...other arguments...
}
retry = {
error_message_regex = ["ScopeLocked", "RetryableError"]
interval_seconds = 15
max_interval_seconds = 60
}
Notes:
- The
retryvariable MUST be applied to everyazapi_resource(and equivalent AzAPI resources) declared by the module. - Parent modules MUST cascade
retryto each submodule they instantiate (see TFFR7 and TFRMNFR1). - Module owners MAY ship module-level defaults when the resource it manages benefits from them. To do so, set the variable’s overall
defaultto{}(notnull) and provide per-field defaults inside theoptional(...)wrappers. Consumers MUST still be able to override any individual field.
# Module-level defaults example: a hypothetical module retries
# on common transient replication errors and tunes the back-off interval. The
# overall variable default is `{}` (not `null`) so the per-field defaults take
# effect, and consumers can still override any individual field.
variable "retry" {
type = object({
error_message_regex = optional(list(string), ["AnotherOperationInProgress", "TooManyRequests"])
interval_seconds = optional(number, 30)
max_interval_seconds = optional(number, 300)
})
default = {}
description = <<DESCRIPTION
Retry configuration applied to every `azapi` resource managed by the module. This module ships defaults tuned for Storage Account replication; consumers **MAY** override any field.
- `error_message_regex` - (Optional) A list of regex patterns matching error messages that trigger a retry.
- `interval_seconds` - (Optional) Initial interval between retries in seconds.
- `max_interval_seconds` - (Optional) Maximum interval between retries in seconds.
DESCRIPTION
}
AzAPI timeouts
variable "timeouts" {
type = object({
create = optional(string)
read = optional(string)
update = optional(string)
delete = optional(string)
})
default = null
description = <<DESCRIPTION
Default per-operation timeouts applied to every `azapi` resource managed by the module. Defaults to `null` (provider defaults). Each value is a Go duration string (e.g. `30m`, `1h`).
- `create` - (Optional) Timeout for create operations.
- `read` - (Optional) Timeout for read operations.
- `update` - (Optional) Timeout for update operations.
- `delete` - (Optional) Timeout for delete operations.
DESCRIPTION
}
# Example resource implementation. `timeouts` is a block on `azapi_resource`,
# so a `dynamic "timeouts"` block is required to honour the variable's `null`
# default. The same pattern applies to every `azapi_resource` declared by
# the module, including those in submodules.
resource "azapi_resource" "this" {
type = var.resource_types.example_widgets
name = var.name
parent_id = var.parent_id
body = { /* ... */ }
dynamic "timeouts" {
for_each = var.timeouts == null ? [] : [var.timeouts]
content {
create = timeouts.value.create
read = timeouts.value.read
update = timeouts.value.update
delete = timeouts.value.delete
}
}
response_export_values = []
}
# Cascade `timeouts` to every submodule the parent module instantiates so that
# a single override at the parent level propagates everywhere.
module "child" {
source = "./modules/child"
timeouts = var.timeouts
# ...other arguments...
}
timeouts = {
create = "30m"
read = "5m"
update = "30m"
delete = "1h"
}
Notes:
timeoutsis a block onazapi_resource(not an attribute), so adynamic "timeouts"block is required to honor the variable’snulldefault.- The
timeoutsvariable MUST be applied to everyazapi_resource(and equivalent AzAPI resources) declared by the module. - Parent modules MUST cascade
timeoutsto each submodule they instantiate (see TFFR7 and TFRMNFR1). Submodules MAY additionally expose per-item overrides for cases where individual resources need different settings. - Module owners MAY ship module-level defaults when the resource it manages benefits from them (for example, longer create / delete timeouts for slow-provisioning resources). To do so, set the variable’s overall
defaultto{}(notnull) and provide per-field defaults inside theoptional(...)wrappers. Consumers MUST still be able to override any individual field.
# Module-level defaults example: a hypothetical SQL Database module ships
# longer create / delete timeouts because provisioning and dropping large
# databases can exceed the provider defaults. The overall variable default
# is `{}` (not `null`) so the per-field defaults take effect, and consumers
# can still override any individual field.
variable "timeouts" {
type = object({
create = optional(string, "1h")
read = optional(string, "5m")
update = optional(string, "1h")
delete = optional(string, "45m")
})
default = {}
description = <<DESCRIPTION
Default per-operation timeouts applied to every `azapi` resource managed by the module. This module ships defaults tuned for SQL Database provisioning latency; consumers **MAY** override any field.
- `create` - (Optional) Timeout for create operations.
- `read` - (Optional) Timeout for read operations.
- `update` - (Optional) Timeout for update operations.
- `delete` - (Optional) Timeout for delete operations.
DESCRIPTION
}