Bicep Specific Specification
Legacy content
The content on this website has been deprecated and will be removed in the future.
Please refer to the new documentation under the Bicep Spefications chapter for the most up-to-date information.
This page contains the Bicep specific requirements for AVM modules (Resource and Pattern modules) that ALL Bicep AVM modules MUST meet. These requirements are in addition to the Shared Specification requirements that ALL AVM modules MUST meet.
The following table summarizes the category identification codes used in this specification:
Scope | Functional requirements | Non-functional requirements |
---|---|---|
Shared requirements (resource & pattern modules) | BCPFR | BCPNFR |
Resource module level requirements | N/A | BCPRMNFR |
Pattern module level requirements | N/A | N/A |
Listed below are both functional and non-functional requirements for Bicep AVM modules (Resource and Pattern).
Module owners MAY cross-references other modules to build either Resource or Pattern modules.
However, they MUST be referenced only by a public registry reference to a pinned version e.g. br/public:avm/[res|ptn|utl]/<publishedModuleName>:>version<
. They MUST NOT use local parent path references to a module e.g. ../../xxx/yyy.bicep
.
The only exception to this rule are child modules as documented in BCPFR6 .
Modules MUST NOT contain references to non-AVM modules.
Parent templates MUST reference all their direct child-templates to allow for an end-to-end deployment experience. For example, the SQL server template must reference its child database module and encapsulate it in a loop to allow for the deployment of multiple databases.
@description('Optional. The databases to create in the server')
param databases databaseType[]?
resource server 'Microsoft.Sql/servers@(...)' = { (...) }
module server_databases 'database/main.bicep' = [for (database, index) in (databases ?? []): {
name: '${uniqueString(deployment().name, location)}-Sql-DB-${index}'
params: {
serverName: server.name
(...)
}
}]
Module owners MAY define common RBAC Role Definition names and IDs within a variable to allow consumers to define a RBAC Role Definition by their name rather than their ID, this should be self contained within the module themselves.
However, they MUST use only the official RBAC Role Definition name within the variable and nothing else.
To meet the requirements of BCPFR2 , BCPNFR5 and BCPNFR6 you MUST use the below code sample in your AVM Modules to achieve this.
@description('''Required. You can provide either the display name (note not all roles are supported, check module documentation) of the role definition, or its fully qualified ID in the following format: `/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11`.''')
param roleDefinitionIdOrName string
var builtInRbacRoleNames = {
Owner: '/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635'
Contributor: '/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c'
Reader: '/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7'
'Role Based Access Control Administrator (Preview)': '/providers/Microsoft.Authorization/roleDefinitions/f58310d9-a9f6-439a-9e8d-f62e7b41a168'
'User Access Administrator': '/providers/Microsoft.Authorization/roleDefinitions/18d7d88d-d35e-4fb5-a5c3-7773c20a72d9'
//Other RBAC Role Definitions Names & IDs can be added here as needed for your module
}
var roleDefinitionIdMappedResult = (contains(builtInRbacRoleNames, roleDefinitionIdOrName) ? builtInRbacRoleNames[roleDefinitionIdOrName] : roleDefinitionIdOrName)
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
//Other properties removed for ease of reading
properties: {
roleDefinitionId: roleDefinitionIdMappedResult
//Other properties removed for ease of reading
}
}
To comply with specifications outlined in
SFR3
&
SFR4
you MUST incorporate the following code snippet into your modules. Place this code sample in the “top level” main.bicep
file; it is not necessary to include it in any nested Bicep files (child modules).
@description('Optional. Location for all resources.')
param location string = resourceGroup().location
@description('Optional. Enable/Disable usage telemetry for module.')
param enableTelemetry bool = true
#disable-next-line no-deployments-resources
resource avmTelemetry 'Microsoft.Resources/deployments@2024-03-01' = if (enableTelemetry) {
name: take('46d3xbcp.res.compute-virtualmachine.${replace('-..--..-', '.', '-')}.${substring(uniqueString(deployment().name, location), 0, 4)}', 64)
properties: {
mode: 'Incremental'
template: {
'$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#'
contentVersion: '1.0.0.0'
resources: []
outputs: {
telemetry: {
type: 'String'
value: 'For more information, see https://aka.ms/avm/TelemetryInfo'
}
}
}
}
}
To implement requirement SFR5 , the following convention SHOULD apply:
In this case, the parameter should be implemented like
@description('Optional. The Availability Zones to place the resources in.')
@allowed([
1
2
3
])
param zones int[] = [
1
2
3
]
resource myResource (...) {
(...)
properties: {
(...)
zones: map(zones, zone => string(zone))
}
}
In this case, the parameter should be implemented using a singular-named zone
parameter of type int
like
@description('Required. The Availability Zone to place the resource in. If set to 0, then Availability Zone is not set.')
@allowed([
0
1
2
3
])
param zone int
resource myResource (...) {
(...)
properties: {
(...)
zones: zone != 0 ? [ string(zone) ] : null
}
}
To simplify the consumption experience for module consumers when interacting with complex data types input parameters, mainly objects and arrays, the Bicep feature of User-Defined Types MUST be used and declared.
User-Defined Types are GA in Bicep as of version v0.21.1, please ensure you have this version installed as a minimum.
User-Defined Types allow intellisense support in supported IDEs (e.g. Visual Studio Code) for complex input parameters using arrays and objects.
While the transition of CARML modules into AVM is complete, retrofitting User-Defined Types for all modules will take a considerable amount of time.
Therefore, the addition of User-Defined Types is currently NOT mandated/enforced. However, past their initial release, all modules MUST implement User-Defined Types prior to the release of their next version.
User-defined types (UDTs) MUST always be singular and non-nullable. The configuration of either should instead be done directly at the parameter or output that uses the type.
For example, instead of
param subnets subnetsType
type subnetsType = { ... }[]?
the type should be defined like
param subnets subnetType[]?
type subnetType = { ... }
The primary reason for this requirement is clarity. If not defined directly at the parameter or output, a user would always be required to check the type to understand how e.g., a parameter is expected.
User-defined types (UDTs) MUST always end with the suffix (...)Type
to make them obvious to users. In addition it is recommended to extend the suffix to (...)OutputType
if a UDT is exclusively used for outputs.
type subnet = { ... } // Wrong
type subnetType = { ... } // Correct
type subnetOutputType = { ... } // Correct, if used only for outputs
Since User-defined types (UDTs) MUST always be singular as per BCPNFR18 , their naming should reflect this and also be singular.
type subnetsType = { ... } // Wrong
type subnetType = { ... } // Correct
User-defined types (UDTs) SHOULD always be exported via the @export()
annotation in every template they’re implemented in.
@export()
type subnetType = { ... }
Doing so has the benefit that other (e.g., parent) modules can import them and as such reduce code duplication. Also, if the module itself is published, users of the Public Bicep Registry can import the types independently of the module itself. One example where this can be useful is a pattern module that may re-use the same interface when referencing a module from the registry.
Similar to
BCPNFR9
, User-defined types (UDTs) MUST implement
decorators
such as description
& secure
(if sensitive). This is true for every property of the UDT, as well as the UDT itself.
Further, User-defined types SHOULD implement decorators like allowed
, minValue
, maxValue
, minLength
& maxLength
(and others if available) as they have a big positive impact on the module’s usability.
@description('My type''s description.')
type myType = {
@description('Optional. The threshold of your resource.')
@minValue(1)
@maxValue(10)
threshold: int?
@description('Required. The SKU of your resource.')
sku: ('Basic' | 'Premium' | 'Standard')
}
Similar to
BCPNFR21
, input parameters MUST implement
decorators
such as description
& secure
(if sensitive).
Further, input parameters SHOULD implement decorators like allowed
, minValue
, maxValue
, minLength
& maxLength
(and others if available) as they have a big positive impact on the module’s usability.
@description('Optional. The threshold of your resource.')
@minValue(1)
@maxValue(10)
param threshold: int?
@description('Required. The SKU of your resource.')
@allowed([
'Basic'
'Premium'
'Standard'
])
param sku string
Modules will have lots of parameters that will differ in their requirement type (required, optional, etc.). To help consumers understand what each parameter’s requirement type is, module owners MUST add the requirement type to the beginning of each parameter’s description. Below are the requirement types with a definition and example for the description decorator:
Parameter Requirement Type | Definition | Example Description Decorator |
---|---|---|
Required | The parameter value must be provided. The parameter does not have a default value and hence the module expects and requires an input. | @description('Required. <PARAMETER DESCRIPTION HERE...>') |
Conditional | The parameter value can be optional or required based on a condition, mostly based on the value provided to other parameters. Should contain a sentence starting with ‘Required if (…).’ to explain the condition. | @description('Conditional. <PARAMETER DESCRIPTION HERE...>') |
Optional | The parameter value is not mandatory. The module provides a default value for the parameter. | @description('Optional. <PARAMETER DESCRIPTION HERE...>') |
Generated | The parameter value is generated within the module and should not be specified as input in most cases. A common example of this is the utcNow() function that is only supported as the input for a parameter value, and not inside a variable. | @description('Generated. <PARAMETER DESCRIPTION HERE...>') |
This script/tool is currently being developed by the AVM team and will be made available very soon.
Bicep modules documentation MUST be automatically generated via the provided script/tooling from the AVM team, providing the following headings:
- Title
- Description
- Navigation
- Resource Types
- Usage Examples
- Parameters
- Outputs
- Cross-referenced modules
Usage examples for Bicep modules MUST be provided in the following formats:
Bicep file (orchestration module style) -
.bicep
module <resourceName> 'br/public:avm/[res|ptn|utl]/<publishedModuleName>:>version<' = { name: '${uniqueString(deployment().name, location)}-test-<uniqueIdentifier>' params: { (...) } }
JSON / ARM Template Parameter Files -
.json
{ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", "contentVersion": "1.0.0.0", "parameters": { (...) } }
The above formats are currently automatically taken & generated from thetests/e2e
tests. It is enough to run theSet-ModuleReadMe
orSet-AVMModule
functions (from theutilities
folder) to update the usage examples in the readme(s).
Bicep Parameter Files (.bicepparam
) are being reviewed and considered by the AVM team for the usability and features at this time and will likely be added in the future.
Bicep modules MAY provide parameter input examples for parameters using the metadata.example
property via the @metadata()
decorator.
Example:
@metadata({
example: 'uksouth'
})
@description('Optional. Location for all resources.')
param location string = resourceGroup().location
@metadata({
example: '''
{
keyName: 'myKey'
keyVaultResourceId: '/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/my-rg/providers/Microsoft.KeyVault/vaults/myvault'
keyVersion: '6d143c1a0a6a453daffec4001e357de0'
userAssignedIdentityResourceId '/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/my-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myIdentity'
}
'''
})
@description('Optional. The customer managed key definition.')
param customerManagedKey customerManagedKeyType
It is planned that these examples are automatically added to the module readme’s parameter descriptions when running either the Set-ModuleReadMe
or Set-AVMModule
scripts (available in the utilities folder).
As per BCPFR2 , module owners MAY define common RBAC Role Definition names and IDs within a variable to allow consumers to define a RBAC Role Definition by their name rather than their ID.
Module owners SHOULD NOT map every RBAC Role Definition within this variable as it can cause the module to bloat in size and cause consumption issues later when stitched together with other modules due to the 4MB ARM Template size limit.
Therefore module owners SHOULD only map the most applicable and common RBAC Role Definition names for their module and SHOULD NOT exceed 15 RBAC Role Definitions in the variable.
Remember if the RBAC Role Definition name is not included in the variable this does not mean it cannot be declared, used and assigned to an identity via an RBAC Role Assignment as part of a module, as any RBAC Role Definition can be specified via its ID without being in the variable.
Review the Bicep Contribution Guide’s ‘RBAC Role Definition Name Mapping’ section for a code sample to achieve this requirement.
Module owners MUST include the following roles in the variable for RBAC Role Definition names:
- Owner - ID:
8e3af657-a8ff-443c-a75c-2fe8c4bcb635
- Contributor - ID:
b24988ac-6180-42a0-ab88-20f7382dd24c
- Reader - ID:
acdd72a7-3385-48ef-bd42-f606fba81ae7
- User Access Administrator - ID:
18d7d88d-d35e-4fb5-a5c3-7773c20a72d9
- Role Based Access Control Administrator (Preview) - ID:
f58310d9-a9f6-439a-9e8d-f62e7b41a168
Review the Bicep Contribution Guide’s ‘RBAC Role Definition Name Mapping’ section for a code sample to achieve this requirement.
Module owners SHOULD use lower camelCasing for naming the following:
- Parameters
- Variables
- Outputs
- User Defined Types
- Resources (symbolic names)
- Modules (symbolic names)
For example: camelCasingExample
(lowercase first word (entirely), with capital of first letter of all other words and rest of word in lowercase)
To meet
SNFR17
and depending on the changes you make, you may need to bump the version in the version.json
file.
{
"$schema": "https://aka.ms/bicep-registry-module-version-file-schema#",
"version": "0.1",
"pathFilters": [
"./main.json"
]
}
The version
value is in the form of MAJOR.MINOR
. The PATCH version will be incremented by the CI automatically when publishing the module to the Public Bicep Registry once the corresponding pull request is merged. Therefore, contributions that would only require an update of the patch version, can keep the version.json
file intact.
For example, the version
value should be:
0.1
for new modules, so that they can be released asv0.1.0
.1.0
once the module owner signs off the module is stable enough for it’s first Major release ofv1.0.0
.0.x
for all feature updates between the first releasev0.1.0
and the first Major release ofv1.0.0
.
Module owners MUST name their test .bicep
files in the /tests/e2e/<defaults/waf-aligned/max/etc.>
directories: main.test.bicep
as the test framework (CI) relies upon this name.
Module owners MUST use the below tooling for unit/linting/static/security analysis tests. These are also used in the AVM Compliance Tests.
- PSRule for Azure
- Pester
- Some tests are provided as part of the AVM Compliance Tests, but you are free to also use Pester for your own tests.
Module owners MUST invoke the module in their test using the syntax:
module testDeployment '../../../main.bicep' =
Example 1: Working example with a single deployment
module testDeployment '../../../main.bicep' = {
scope: resourceGroup
name: '${uniqueString(deployment().name, location)}-test-${serviceShort}'
params: {
(...)
}
}
Example 2: Working example using a deployment loop
@batchSize(1)
module testDeployment '../../main.bicep' = [for iteration in [ 'init', 'idem' ]: {
scope: resourceGroup
name: '${uniqueString(deployment().name, location)}-test-${serviceShort}-${iteration}'
params: {
(...)
}
}]
The syntax is used by the ReadMe-generating utility to identify, pull & format usage examples.
By default, the ReadMe-generating utility will create usage examples headers based on each e2e
folder’s name.
Module owners MAY provide a custom name & description by specifying the metadata blocks name
& description
in their main.test.bicep
test files.
For example:
metadata name = 'Using Customer-Managed-Keys with System-Assigned identity'
metadata description = 'This instance deploys the module using Customer-Managed-Keys using a System-Assigned Identity. This required the service to be deployed twice, once as a pre-requisite to create the System-Assigned Identity, and once to use it for accessing the Customer-Managed-Key secret.'
would lead to a header in the module’s readme.md
file along the lines of
### Example 1: _Using Customer-Managed-Keys with System-Assigned identity_
This instance deploys the module using Customer-Managed-Keys using a System-Assigned Identity. This required the service to be deployed twice, once as a pre-requisite to create the System-Assigned Identity, and once to use it for accessing the Customer-Managed-Key secret.
As part of the “initial Pull Request” (that publishes the first version of the module), module owners MUST add an entry to the AVM Module Issue template
file in the BRM repository (
here
).
Through this approach, the AVM core team will allow raising a bug or feature request for a module, only after the module gets merged to the BRM repository.
The module name entry MUST be added to the dropdown list with id module-name-dropdown
as an option, in alphabetical order.
Module owners MUST ensure that the module name is added in alphabetical order, to simplify selecting the right module name when raising an AVM module issue.
Example - AVM Module Issue template
module name entry for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network
):
- type: dropdown
id: module-name-dropdown
attributes:
label: Module Name
description: Which existing AVM module is this issue related to?
options:
...
- "avm/res/network/virtual-network"
...
For each test case in the e2e
folder, you can optionally add post-deployment Pester tests that are executed once the corresponding deployment completed and before the removal logic kicks in.
To leverage the feature you MUST:
Use Pester as a test framework in each test file
Name the file with the suffix
"*.tests.ps1"
Place each test file the
e2e
test’s folder or any subfolder (e.g.,e2e/max/myTest.tests.ps1
ore2e/max/tests/myTest.tests.ps1
)Implement an input parameter
TestInputData
in the following way:param ( [Parameter(Mandatory = $false)] [hashtable] $TestInputData = @{} )
Through this parameter you can make use of every output the
main.test.bicep
file returns, as well as the path to the test template file in case you want to extract data from it directly.For example, with an output such as
output resourceId string = testDeployment[1].outputs.resourceId
defined in themain.test.bicep
file, the$TestInputData
would look like:$TestInputData = @{ DeploymentOutputs = @{ resourceId = @{ Type = "String" Value = "/subscriptions/***/resourceGroups/dep-***-keyvault.vaults-kvvpe-rg/providers/Microsoft.KeyVault/vaults/***kvvpe001" } } ModuleTestFolderPath = "/home/runner/work/bicep-registry-modules/bicep-registry-modules/avm/res/key-vault/vault/tests/e2e/private-endpoint" }
A full test file may look like:
To improve the usability of primitive module properties declared as strings, you SHOULD declare them using a type which better represents them, and apply any required casting in the module on behalf of the user.
For reference, please refer to the following examples:
@allowed([
'false'
'true'
])
param myParameterValue string = 'false'
resource myResource '(...)' = {
(...)
properties: {
myParameter: myParameterValue
}
}
param myParameterValue string = false
resource myResource '(...)' = {
(...)
properties: {
myParameter: string(myParameterValue)
}
}
@allowed([
'1'
'2'
'3'
])
param zones array
resource myResource '(...)' = {
(...)
properties: {
zones: zones
}
}
@allowed([
1
2
3
])
param zones int[]
resource myResource '(...)' = {
(...)
properties: {
zones: map(zones, zone => string(zone))
}
}
Listed below are both functional and non-functional requirements for Bicep AVM Resource Modules .
Module owners MUST create the defaults
, waf-aligned
folders within their /tests/e2e/
directory in their resource module source code and SHOULD create a max
folder also. Module owners CAN create additional folders as required. Each folder will be used as described for various test cases.
The defaults
folder contains a test instance that deploys the module with the minimum set of required parameters.
This includes input parameters of type Required
plus input parameters of type Conditional
marked as required for WAF compliance.
This instance has heavy reliance on the default values for other input parameters. Parameters of type Optional
SHOULD NOT be used.
The waf-aligned
folder contains a test instance that deploys the module in alignment with the best-practices of the Azure Well-Architected Framework.
This includes input parameters of type Required
, parameters of type Conditional
marked as required for WAF compliance, and parameters of type Optional
useful for WAF compliance.
Parameters and dependencies which are not needed for WAF compliance, SHOULD NOT be included.
The max
folder contains a test instance that deploys the module using a large parameter set, enabling most of the modules’ features.
The purpose of this instance is primarily parameter validation and not necessarily to serve as a real example scenario. Ideally, all features, extension resources and child resources should be enabled in this test, unless not possible due to conflicts, e.g., in case parameters are mutually exclusive.
Please note that this test is not mandatory to have, but recommended for bulk parameter validation. It can be skipped in case the module parameter validation is covered already by additional, more scenario-specific tests.
Additional folders CAN
be created by module owners as required.
For example, to validate parameters not covered by the max
test due to conflicts, or to provide a real example scenario for a specific use case.
If a module can deploy varying styles of the same resource, e.g., VMs can be Linux or Windows, each style should be tested as both defaults
and waf-aligned
. These names should be used as suffixes in the directory name to denote the style, e.g., for a VM we would expect to see:
/tests/e2e/defaults.linux/main.test.bicep
/tests/e2e/waf-aligned.linux/main.test.bicep
/tests/e2e/defaults.windows/main.test.bicep
/tests/e2e/waf-aligned.windows/main.test.bicep
When implementing any of the shared or Bicep-specific AVM interface variants you MUST import their User-defined type (UDT) via the published AVM-Common-Types module.
When doing so, each type MUST be imported separately, right above the parameter or output that uses it.
import { roleAssignmentType } from 'br/public:avm/utl/types/avm-common-types:*.*.*'
@description('Optional. Array of role assignments to create.')
param roleAssignments roleAssignmentType[]?
import { diagnosticSettingFullType } from 'br/public:avm/utl/types/avm-common-types:*.*.*'
@description('Optional. The diagnostic settings of the service.')
param diagnosticSettings diagnosticSettingFullType[]?
Importing them individually as opposed to one common block has several benefits such as
- Individual versioning of types
- If you must update the version for one type, you’re not exposed to unexpected changes to other types
Theimport (...)
block MUST not be added in between a parameter’s definition and its metadata. Doing so breaks the metadata’s binding to the parameter in question.
Finally, you should check for version updates regularly to ensure the resource module stays consistent with the specs. If the used AVM-Common-Types runs stale, the CI may eventually fail the module’s static tests.
Child resource modules MUST be stored in a subfolder of their parent resource module and named after the child resource’s singular name (
ref
), so that the path to the child resource folder is consistent with the hierarchy of its resource type.
For example, Microsoft.Sql/servers
may have dedicated child resources of type Microsoft.Sql/servers/databases
. Hence, the SQL server database child module is stored in a database
subfolder of the server
parent folder.
sql
└─ server [module]
└─ database [child-module/resource]
In this folder, we recommend to place the child resource-template alongside a ReadMe & compiled JSON (to be generated via the default Set-AVMModule utility) and optionally further nest additional folders for its child resources.
There are several reasons to structure a module in this way. For example
- It allows a separation of concerns where each module can focus on its own properties and logic, while delegating most of a child-resource’s logic to its separate child module
- It’s consistent with the provider namespace structure and makes modules easier to understand not only because they’re more aligned with set structure, but also are aligned with one another
- As each module is its own ‘deployment’, it reduces limitations around nested loops
- Once the feature is enabled, it will enable module owners to publish set child-modules as separate modules to the public registry, allowing consumers to make use of them directly.
In full transparency: The drawbacks of these additional deployments is an extended deployment period & a contribution to the 800 deployments limit. However, for AVM resource modules it was agreed that the advantages listed above outweigh these limitations.