Bicep 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.
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 //
// ============== //
import { roleAssignmentType } from 'br/public:avm/utl/types/avm-common-types:>version<'
@description('Optional. Array of role assignments to create.')
param roleAssignments roleAssignmentType[]?
// ============= //
// Variables //
// ============= //
var builtInRoleNames = {
// Add other relevant built-in roles here for your resource as per BCPNFR5
Contributor: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')
Owner: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')
Reader: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')
'Role Based Access Control Administrator (Preview)': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')
'User Access Administrator': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')
}
var formattedRoleAssignments = [
for (roleAssignment, index) in (roleAssignments ?? []): union(roleAssignment, {
roleDefinitionId: builtInRoleNames[?roleAssignment.roleDefinitionIdOrName] ?? (contains(roleAssignment.roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/')
? roleAssignment.roleDefinitionIdOrName
: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleAssignment.roleDefinitionIdOrName))
})
]
// ============= //
// Resources //
// ============= //
resource >singularMainResourceType<_roleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [
for (roleAssignment, index) in (formattedRoleAssignments ?? []): {
name: roleAssignment.?name ?? guid(>singularMainResourceType<.id, roleAssignment.principalId, roleAssignment.roleDefinitionId)
properties: {
roleDefinitionId: roleAssignment.roleDefinitionId
principalId: roleAssignment.principalId
description: roleAssignment.?description
principalType: roleAssignment.?principalType
condition: roleAssignment.?condition
conditionVersion: !empty(roleAssignment.?condition) ? (roleAssignment.?conditionVersion ?? '2.0') : null // Must only be set if condtion is set
delegatedManagedIdentityResourceId: roleAssignment.?delegatedManagedIdentityResourceId
}
scope: >singularMainResourceType<
}
]
roleAssignments: [
{
roleDefinitionIdOrName: 'Owner'
principalId: nestedDependencies.outputs.managedIdentityPrincipalId
principalType: 'ServicePrincipal'
}
{
roleDefinitionIdOrName: 'b24988ac-6180-42a0-ab88-20f7382dd24c'
principalId: nestedDependencies.outputs.managedIdentityPrincipalId
principalType: 'ServicePrincipal'
}
{
roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')
principalId: nestedDependencies.outputs.managedIdentityPrincipalId
principalType: 'ServicePrincipal'
}
{
name: guid('Custom role assignment name seed')
roleDefinitionIdOrName: 'Storage Blob Data Reader'
principalId: '00000000-0000-0000-0000-000000000000'
principalType: 'Group'
description: 'Group with read-only access'
condition: '@Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase 'foo_storage_container''
conditionVersion: '2.0'
}
]
Details on child, extension and cross-referenced resources:
- Modules MUST support Role Assignments on child, extension and cross-referenced resources as well as the primary resource via parameters/variables
// ============== //
// Parameters //
// ============== //
import { lockType } from 'br/public:avm/utl/types/avm-common-types:>version<'
@description('Optional. The lock settings of the service.')
param lock lockType?
// ============= //
// Resources //
// ============= //
resource >singularMainResourceType<_lock 'Microsoft.Authorization/locks@2020-05-01' = if (!empty(lock ?? {}) && lock.?kind != 'None') {
name: lock.?name ?? 'lock-${name}'
properties: {
level: lock.?kind ?? ''
notes: lock.?kind == 'CanNotDelete' ? 'Cannot delete resource or child resources.' : 'Cannot delete or modify the resource or child resources.'
}
scope: >singularMainResourceType<
}
lock: 'CanNotDelete'
Details on child and extension resources:
- Locks SHOULD be able to be set for child resources of the primary resource in resource modules
Details on cross-referenced resources:
- Locks MUST be automatically applied to cross-referenced resources if the primary resource has a lock applied.
- This MUST also be able to be turned off for each of the cross-referenced resources by the module consumer via a parameter/variable if they desire
An example of this is a Key Vault module that has a Private Endpoints enabled. If a lock is applied to the Key Vault via the lock
parameter/variable then the lock should also be applied to the Private Endpoint automatically, unless the privateEndpointLock/private_endpoint_lock
(example name) parameter/variable is set to None
@description('Optional. Tags of the resource.')
param tags object?
tags: {
key: 'value'
'another-key': 'another-value'
integers: 123
}
Details on child, extension and cross-referenced resources:
- Tags MUST be automatically applied to child, extension and cross-referenced resources, if tags are applied to the primary resource.
- By default, all tags set for the primary resource will automatically be passed down to child, extension and cross-referenced resources.
- This MUST be able to be overridden by the module consumer so they can specify alternate tags for child, extension and cross-referenced resources, if they desire via a parameter/variable
- If overridden by the module consumer, no merge/union of tags will take place from the primary resource and only the tags specified for the child, extension and cross-referenced resources will be applied
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
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
Secrets used inside a module can be exported to a Key Vault reference provided as per the below schema. This implementation provides a secure way around the current limitation of Bicep on providing a secure template output (that can be used for secrets).
The user must
- provide the resource Id to a Key Vault. The principal used for the deployment must be allowed to set secrets in this Key Vault.
- provide a name for each secret they want to store (opt-in). The module will suggest which secrets are available via the implemented user-defined type.
The module returns an output table where the key is the name of the secret the user provided, and the value contains both the secret’s resource Id and URI.
The feature must be implemented as per the below schema. Diversions are only allowed in places marked as>text<
to ensure a consistent user experience across modules.
// ============== //
// Parameters //
// ============== //
@description('Optional. Key vault reference and secret settings for the module\'s secrets export.')
param secretsExportConfiguration secretsExportConfigurationType?
// ============= //
// Resources //
// ============= //
module secretsExport 'modules/keyVaultExport.bicep' = if (secretsExportConfiguration != null) {
name: '${uniqueString(deployment().name, location)}-secrets-kv'
scope: resourceGroup(
split((secretsExportConfiguration.?keyVaultResourceId ?? '//'), '/')[2],
split((secretsExportConfiguration.?keyVaultResourceId ?? '////'), '/')[4]
)
params: {
keyVaultName: last(split(secretsExportConfiguration.?keyVaultResourceId ?? '//', '/'))
secretsToSet: union(
[],
contains(secretsExportConfiguration!, '>secretToExport1<Name')
? [
{
name: secretsExportConfiguration!.>secretToExport1<Name
value: >secretReference1< // e.g., >singularMainResourceType<.listKeys().primaryMasterKey
}
]
: [],
contains(secretsExportConfiguration!, '>secretToExport2<Name')
? [
{
name: secretsExportConfiguration!.>secretToExport2<Name
value:>secretReference2< // e.g., >singularMainResourceType<.listKeys().secondaryMasterKey
}
]
: []
// (...)
)
}
}
// =========== //
// Outputs //
// =========== //
import { secretsOutputType } from 'br/public:avm/utl/types/avm-common-types:>version<'
@description('A hashtable of references to the secrets exported to the provided Key Vault. The key of each reference is each secret\'s name.')
output exportedSecrets secretsOutputType = (secretsExportConfiguration != null)
? toObject(secretsExport.outputs.secretsSet, secret => last(split(secret.secretResourceId, '/')), secret => secret)
: {}
// =============== //
// Definitions //
// =============== //
@export()
type secretsExportConfigurationType = {
@description('Required. The resource ID of the key vault where to store the secrets of this module.')
keyVaultResourceId: string
@description('Optional. The >secretToExport1< secret name to create.')
>secretToExport1<Name: string?
@description('Optional. The >secretToExport2< secret name to create.')
>secretToExport2<Name: string?
// (...)
}
secretsExportConfiguration: {
keyVaultResourceId: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.KeyVault/vaults/{keyVaultName}'
>secretToExport1<Name: 'myPrimarySecret'
>secretToExport2<Name: 'mySecondarySecret'
// (...)
}
// ============== //
// Parameters //
// ============== //
@description('Required. The name of the Key Vault to set the secrets in.')
param keyVaultName string
import { secretToSetType } from 'br/public:avm/utl/types/avm-common-types:>version<'
@description('Required. The secrets to set in the Key Vault.')
param secretsToSet secretToSetType[]
// ============= //
// Resources //
// ============= //
resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = {
name: keyVaultName
}
resource secrets 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = [
for secret in secretsToSet: {
name: secret.name
parent: keyVault
properties: {
value: secret.value
}
}
]
// =========== //
// Outputs //
// =========== //
import { secretSetOutputType } from 'br/public:avm/utl/types/avm-common-types:>version<'
@description('The references to the secrets exported to the provided Key Vault.')
output secretsSet secretSetOutputType[] = [
#disable-next-line outputs-should-not-contain-secrets // Only returning the references, not a secret value
for index in range(0, length(secretsToSet ?? [])): {
secretResourceId: secrets[index].id
secretUri: secrets[index].properties.secretUri
secretUriWithVersion: secrets[index].properties.secretUriWithVersion
}
]
When using a module that implements the above interface, you can access its outputs for example in the following ways:
// Get all exported secret references
output exportedSecrets object = >deploymentReference<.outputs.exportedSecrets
// Get the resource Id of just the secret with name >secretToExport1<Name
output specificSecret string = >deploymentReference<.outputs.exportedSecrets.>secretToExport1<Name.secretResourceId
// Get the resource Ids of all secrets set
output exportedSecretResourceIds array = map(
items(>deploymentReference<.outputs.exportedSecrets),
item => item.value.secretResourceId
)
Which returns a JSON-formatted output like
{
"exportedSecrets": {
"Type": "Object",
"Value": {
">secretToExportName1<": {
"secretResourceId": "/subscriptions/<subId>/resourceGroups/<rgName>providers/Microsoft.KeyVault/vaults/<vaultName>/secrets/>secretToExportName1<",
"secretUri": "https://<vaultName>.vault.azure.net/secrets/>secretToExportName1<"
},
">secretToExportName2<": {
"secretResourceId": "/subscriptions/<subId>/resourceGroups/<rgName>providers/Microsoft.KeyVault/vaults/<vaultName>/secrets/>secretToExportName2<",
"secretUri": "https://<vaultName>.vault.azure.net/secrets/>secretToExportName2<"
}
}
},
"specificSecret": {
"Type": "String",
"Value": "/subscriptions/<subId>/resourceGroups/<rgName>providers/Microsoft.KeyVault/vaults/<vaultName>/secrets/>secretToExportName1<"
},
"exportedSecretResourceIds": {
"Type": "Array",
"Value": [
"/subscriptions/<subId>/resourceGroups/<rgName>providers/Microsoft.KeyVault/vaults/<vaultName>/secrets/>secretToExportName1<",
"/subscriptions/<subId>/resourceGroups/<rgName>providers/Microsoft.KeyVault/vaults/<vaultName>/secrets/>secretToExportName2<"
]
}
}
This interface is a SHOULD instead of a MUST and therefore the AVM core team have not mandated a interface schema to use.