Posted: Saturday, May 6, 2023
Word Count: 2419
Reading Time: 11 minutes
Azure Bicep is a domain-specific language (DSL) that simplifies the authoring and management of Azure Resource Manager (ARM) templates. With Bicep, you can write reusable, modular, and version-controlled infrastructure-as-code (IaC) that makes it easier to deploy and manage your Azure resources. Additionally, it simplifies the syntax as compared to a JSON template, making it easier to modify, diagnose and migrate.
For those who are not fans of JSON’s rigid syntax and monolithic structure, then Azure Bicep may be perfect for you. However unlike arm templates, Bicep should not be leveraged in a similar fashion. Although possible, creating large monolithic bicep files is not optimal. As you create Bicep code, consider the following best practices:
Use modules to abstract and reuse resource definitions
Modules are a powerful feature that allow you to compartmentalize resource definitions. This reduces the need to repeat syntax and greatly streamlines you code set. Use modules to define common patterns or infrastructure components that can be shared across different deployments. I’ve created a simple resource Group module as an example. The module has a few parameters with default values.
targetScope = 'subscription'
param resourceGroupName string = 'labRG'
param location string = 'eastus'
resource resGroup 'Microsoft.Resources/resourceGroups@2022-09-01' = {
name: resourceGroupName
location: location
}
BICEPThe Module below calls the module twice to create two resource groups. As you can see, the parameters can override the default values set in the module itself. In this case the module will deploy three unique resource groups.
targetScope = 'subscription'
param location string = 'eastus'
module labRg 'resourceGroup.bicep' = {
name: 'labRg'
}
module ProdResourceGroup 'resourceGroup.bicep' = {
name: 'ProductionRg'
params: {
location: 'westus'
resourceGroupName:'ProductionRg'
}
}
module TestResourceGroup 'resourceGroup.bicep' = {
name: 'TestRg'
params: {
resourceGroupName:'TestRg'
location: location
}
}
BICEPUse parameters and variables to make your templates more flexible and reusable: Parameters and variables provide input values to your resource definitions. You can use variables to define expressions or values that can be reused across resource definitions or modules.
targetScope = 'subscription'
param environmentName string = 'Lab'
param date string = utcNow('d')
param applicationName string = 'Awesome-O'
param resourceGroupName string = 'ResourceGroup1'
param location string = 'eastus'
param tags object = {
Environment : environmentName
CreationDate : date
Application: applicationName
}
resource resourceGroup 'Microsoft.Resources/resourceGroups@2022-09-01' = {
name: resourceGroupName
location: location
tags: tags
}
Use loops and conditions to simplify and automate template authoring: Bicep provides built-in support for loops and conditions, allowing you to simplify and automate the authoring of your templates. Loops define multiple copies of a resource, module, variable, property, or output. Use loops to avoid repeating syntax in your Bicep file and to dynamically set the number of copies to create during deployment. The example below used a for loop
to create resource groups from an array.
targetScope = 'subscription'
param defaultLocation string = 'eastus'
param environmentPrefix string = 't'
param date string = utcNow('d')
param environmentName string = 'Test'
param sharedSvcRgName string = 'Shared-Services'
param appSvcsRgName string = 'App-Services'
param dbstRGName string = 'dbst-Services'
param resGroups array = [
{
name: '${sharedSvcRgName}${environmentPrefix}'
location: defaultLocation
tags: {
Environment : environmentName
LastModified : date
Application: 'App'
}
}
{
name: '${appSvcsRgName}${environmentPrefix}'
location: 'westus'
tags: {
Environment : 'SandBox'
LastModified : date
Application: 'Toys'
}
}
{
name: '${dbstRGName}${environmentPrefix}'
location: defaultLocation
tags: {
Environment : environmentName
LastModified : date
Application: 'Something Here'
}
}
]
resource resourceGroupDeployment 'Microsoft.Resources/resourceGroups@2022-09-01' = [for (group,i) in resGroups: {
name: group.name
location: group.location
tags: group.tags
}]
BICEPUse resource dependencies to ensure proper resource sequencing: Resource dependencies in Bicep allow you to define the order in which resources should be created. Use dependencies to ensure that resources are created in the correct sequence to avoid deployment errors. There resource deployment order can be either implicit or explicit.
An implicit dependency occurs when a resource in the same deployment references another. Implicit dependencies do not require the dependsOn
property for proper sequencing. The example below, line 61 and 64 are implicit dependencies. The values are dependent on the Log Analytics and servicebus resources. In this example, the dependsOn property is not required for the diagnosticLinuxAppSettings
resource on line 59.
param logAnalyticsWorkspaceName string
@minValue(7)
param categoryRetentionDays int = 7
param categoryRetentionEnabled bool = false
param categoryLogsEnabled bool = false
param categoryMetricsEnabled bool = true
@minValue(7)
param categoryMetricsRetentionDays int = 7
param categoryMetricsRetentionEnabled bool = true
@allowed([
'Free'
'Standalone'
'PerNode'
'Premium'
'PerGB2018'
])
param logAnalyticsSku string = 'PerGB2018'
param servicebusNameppace string = 'servicebusTheta01'
param location string = 'westus'
param topicName string = '${servicebusNameppace}-topic1'
//Deployments
resource servicebus 'Microsoft.ServiceBus/namespaces@2022-10-01-preview' = {
name: servicebusNameppace
location: location
}
resource topic 'Microsoft.ServiceBus/namespaces/topics@2022-10-01-preview' = {
name: topicName
parent: servicebus
properties: {
status: 'Active'
}
}
resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-06-01' = {
name: logAnalyticsWorkspaceName
location: location
properties: {
sku: {
name: logAnalyticsSku
}
retentionInDays: 30
publicNetworkAccessForIngestion: 'Disabled'
publicNetworkAccessForQuery: 'Disabled'
}
}
resource diagnosticLinuxAppSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {
scope: servicebus
name: '${servicebus.name}AuditEvents'
properties: {
workspaceId: logAnalyticsWorkspace.id
logs: [
{
enabled: categoryLogsEnabled
category: 'OperationalLogs'
retentionPolicy: {
days: categoryRetentionDays
enabled: categoryRetentionEnabled
}
}
{
enabled: categoryLogsEnabled
category: 'VNetAndIPFilteringLogs'
retentionPolicy: {
days: categoryRetentionDays
enabled: categoryRetentionEnabled
}
}
{
enabled: categoryLogsEnabled
category: 'RuntimeAuditLogs'
retentionPolicy: {
days: categoryRetentionDays
enabled: categoryRetentionEnabled
}
}
{
enabled: categoryLogsEnabled
category: 'ApplicationMetricsLogs'
retentionPolicy: {
days: categoryRetentionDays
enabled: categoryRetentionEnabled
}
}
]
metrics: [
{
enabled: categoryMetricsEnabled
category: 'AllMetrics'
retentionPolicy: {
days: categoryMetricsRetentionDays
enabled: categoryMetricsRetentionEnabled
}
}
]
}
}
BICEPAn explicit dependency leverages the dependsOn
property. The dependsOn property accepts several resource identifiers, and can accept multiple resource Identifiers. In the example below, we leverage the resource() function and use the parameter instead of referencing directly from the vnet resource. The dependsOn is added to ensure that the resources are not deployed in parallel.
Note: This is only an example to show the how the dependsOn module works. It would be more efficient to leverage vnet.id on line 72.
@description('VNet name')
param vnetName string = 'VNet1'
@description('Address prefix')
param vnetAddressPrefix string = '10.0.0.0/16'
@description('Subnet 1 Prefix')
param subnet1Prefix string = '10.0.0.0/24'
@description('Subnet 1 Name')
param subnet1Name string = 'Subnet1'
@description('Subnet 2 Prefix')
param subnet2Prefix string = '10.0.1.0/24'
@description('Subnet 2 Name')
param subnet2Name string = 'Subnet2'
@description('Location for all resources.')
param location string = resourceGroup().location
targetScope = 'resourceGroup'
param privateDnsZones array = [
{
name: 'privatelink${environment().suffixes.sqlServerHostname}'
}
]
resource vnet 'Microsoft.Network/virtualNetworks@2021-08-01' = {
name: vnetName
location: location
properties: {
addressSpace: {
addressPrefixes: [
vnetAddressPrefix
]
}
subnets: [
{
name: subnet1Name
properties: {
addressPrefix: subnet1Prefix
}
}
{
name: subnet2Name
properties: {
addressPrefix: subnet2Prefix
}
}
]
}
}
resource prvDnsZones 'Microsoft.Network/privateDnsZones@2020-06-01' = [for (privateDnsZone, i) in privateDnsZones:{
name: privateDnsZone.name
location: 'global'
}]
resource dnsVirtualNetworkLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = [for (privateDnsZone, i) in privateDnsZones :{
name: '${prvDnsZones[i].name}link'
parent: prvDnsZones[i]
location: 'global'
dependsOn: [
vnet
]
properties: {
registrationEnabled: false
virtualNetwork: {
id: resourceId('Microsoft.Network/virtualNetworks',vnetName)
}
}
}]
Use parameters files to manage deployment-specific values: Parameter files in Bicep allow you to manage deployment-specific values such as environment-specific settings, application configuration values, or user-defined parameters. Use parameter files to separate deployment-specific values from your templates and make your deployments more flexible.
In the example below, parameters are declared without values. The values to the parameters are defined by the parameter files below. Leveraging this methodology allows you to leverage the same code but define different settings. For example, a parameters file can be defined for the test and production environment.
param location string
param appServicePlanName string
@allowed([
'B1'
'B2'
'B3'
'S1'
'S2'
'S3'
])
@description('Name of the resource SKU')
param skuSize string
@description('If true, apps assigned to this App Service plan can be scaled independently.')
param perSiteScaling bool
@description('Apps in this plan will scale as if the ServerFarm was ElasticPremium sku')
param elasticScaleEnabled bool
@description('Scaling worker count.')
param targetWorkerCount int
@description('Scaling worker size Id.')
param targetWorkerSizeId int
@description('If true, this App Service Plan will perform availability zone balancing.')
param zoneRedundant bool
resource appSvcPln 'Microsoft.Web/serverfarms@2022-03-01' = {
name: appServicePlanName
location: location
sku: {
size: skuSize
name: skuSize
}
kind: 'Linux'
properties: {
perSiteScaling: perSiteScaling
elasticScaleEnabled: elasticScaleEnabled
targetWorkerCount: targetWorkerCount
targetWorkerSizeId: targetWorkerSizeId
zoneRedundant: zoneRedundant
}
}
output id string = appSvcPln.id
BICEP{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"location": {
"value": "eastus"
},
"appServicePlanName ": {
"value": "testAppPlan"
},
"skuSize": {
"value": "B1"
},
"perSiteScaling": {
"value": false
},
"elasticScaleEnabled": {
"value": false
},
"targetWorkerCount": {
"value": 0
},
"targetWorkerSizeId": {
"value": 0
},
"zoneRedundant": {
"value": false
}
}
}
JSON{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"location": {
"value": "eastus"
},
"appServicePlanName ": {
"value": "ProdAppPlan"
},
"skuSize": {
"value": "B1"
},
"perSiteScaling": {
"value": false
},
"elasticScaleEnabled": {
"value": false
},
"targetWorkerCount": {
"value": 1
},
"targetWorkerSizeId": {
"value": 0
},
"zoneRedundant": {
"value": true
}
}
}
JSONUse Git or other version control systems to manage and track changes to your templates: Bicep integrates seamlessly with Git, Azure DevOps Repos (which is basically Git), or other version control systems, to allow management and tracking of changes to your Bicep files over time. Leveraging a version control system may also enable team collaborate with other developers, and rollback changes when necessary.
Use built-in functions and expressions to simplify template authoring: Bicep provides a rich set of built-in functions and expressions that simplify the authoring of your templates. Use functions to perform operations on values, and use expressions to define complex values or conditions.
The getSecret function can be used to obtain Key Vault secrets and pass the value to a parameter of a module. You can only use the getSecret
function from within the params
section of a module. You can only use it with a Microsoft.KeyVault/vaults
resource.
param sharedServicesKeyVaultName string = 'labkeyVault001'
resource kv 'Microsoft.KeyVault/vaults@2022-11-01' existing = {
name: sharedServicesKeyVaultName
}
module sql './sql.bicep' = {
name: 'deploySQL'
params: {
adminPassword: kv.getSecret('vmAdminPassword')
}
}
JSONThe resourceId
function returns the ID of an existing resource. It simplifies the traditional syntax and streamlines the codeset.
resource dnsVirtualNetworkLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = [for (privateDnsZone, i) in privateDnsZones :{
name: '${prvDnsZones[i].name}link'
parent: prvDnsZones[i]
location: 'global'
dependsOn: [
vnet
]
properties: {
registrationEnabled: false
virtualNetwork: {
id: resourceId('Microsoft.Network/virtualNetworks',vnetName)
}
}
}]
JSONThe environment
function returns properties, primarily URLs and suffixes, for the current Azure environment. The following example shows a module leveraging the environment function to append the key vault suffix to the name.
Outputting the environment function provides a list of properties that you can leverage within your bicep code. Review the sample output below.
//You can run the following commands to completely output the environment function
param environmentFunction object = environment()
output results object = environmentFunction
// Example Output
{
"id": "/subscriptions/11111111-0000-0000-0000-111111111111/providers/Microsoft.Resources/deployments/<deploymentName>",
"location": <currentLocation>,
"name": "<deploymentName>",
"properties": {
"correlationId": "11111111-0000-0000-0000-111111111111",
"debugSetting": null,
"dependencies": [],
"duration": "PT0.4623444S",
"error": null,
"mode": "Incremental",
"onErrorDeployment": null,
"outputResources": [],
"outputs": {
"results": {
"type": "Object",
"value": {
"activeDirectoryDataLake": "https://datalake.azure.net/",
"authentication": {
"audiences": [
"https://management.core.windows.net/",
"https://management.azure.com/"
],
"identityProvider": "AAD",
"loginEndpoint": "https://login.microsoftonline.com/",
"tenant": "common"
},
"batch": "https://batch.core.windows.net/",
"gallery": "https://gallery.azure.com/",
"graph": "https://graph.windows.net/",
"graphAudience": "https://graph.windows.net/",
"media": "https://rest.media.azure.net",
"name": "AzureCloud",
"portal": "https://portal.azure.com",
"resourceManager": "https://management.azure.com/",
"sqlManagement": "https://management.core.windows.net:8443/",
"suffixes": {
"acrLoginServer": ".azurecr.io",
"azureDatalakeAnalyticsCatalogAndJob": "azuredatalakeanalytics.net",
"azureDatalakeStoreFileSystem": "azuredatalakestore.net",
"azureFrontDoorEndpointSuffix": "azurefd.net",
"keyvaultDns": ".vault.azure.net",
"sqlServerHostname": ".database.windows.net",
"storage": "core.windows.net"
},
"vmImageAliasDoc": "https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/arm-compute/quickstart-templates/aliases.json"
}
}
},
"parameters": {
"environmentFunction": {
"type": "Object",
"value": {
"activeDirectoryDataLake": "https://datalake.azure.net/",
"authentication": {
"audiences": [
"https://management.core.windows.net/",
"https://management.azure.com/"
],
"identityProvider": "AAD",
"loginEndpoint": "https://login.microsoftonline.com/",
"tenant": "common"
},
"batch": "https://batch.core.windows.net/",
"gallery": "https://gallery.azure.com/",
"graph": "https://graph.windows.net/",
"graphAudience": "https://graph.windows.net/",
"media": "https://rest.media.azure.net",
"name": "AzureCloud",
"portal": "https://portal.azure.com",
"resourceManager": "https://management.azure.com/",
"sqlManagement": "https://management.core.windows.net:8443/",
"suffixes": {
"acrLoginServer": ".azurecr.io",
"azureDatalakeAnalyticsCatalogAndJob": "azuredatalakeanalytics.net",
"azureDatalakeStoreFileSystem": "azuredatalakestore.net",
"azureFrontDoorEndpointSuffix": "azurefd.net",
"keyvaultDns": ".vault.azure.net",
"sqlServerHostname": ".database.windows.net",
"storage": "core.windows.net"
},
"vmImageAliasDoc": "https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/arm-compute/quickstart-templates/aliases.json"
}
}
},
"parametersLink": null,
"providers": [],
"provisioningState": "Succeeded",
"templateHash": <irrelevant>,
"templateLink": null,
"timestamp": <irrelevant>,
"validatedResources": null
},
"tags": null,
"type": "Microsoft.Resources/deployments"
}
Additionally, the example below leverages the environment function to complete the key vault connection string.
module WebApps 'Modules/appServicePlan/webApp.bicep' = {
scope: resourceGroup(appResourceGroup.name)
name: 'webApps${prdServicePlan.name}'
params: {
appServicePlanId: prdServicePlan.outputs.id
location: location
appDnsZoneId:WebAppPrivateLinkId
webAppSubnetId: spoke01.outputs.appsvcSubnetID
vnetIntegrationSubnetId: spoke01.outputs.appVnetIntHostID
environmentName: environmentName
environmentPrefix: environmentPrefix
serviceBusName: serviceBusName
appInsightConnectionString: appInsights.outputs.connectionString
storageConnectionString: primaryStorage.outputs.connectionString
bindWebDomain: bindWebDomain
environmentLabel: toLower(environmentLabel)
keyVaultConnectionString: 'https://${keyVaultName}${environment().suffixes.keyvaultDns}'
sqlDBArray: sqlDatabase.outputs.sqlDbs
sqlServerNameEus: sqlServerNameEus
logAnalyticsId: logAnalyticsResourceId
LegacyMonarchServiceBusConnection: LegacyMonarchServiceBusConnection
}
BICEPThe guidelines listed above will allow you to write cleaner, more modular, and reusable infrastructure-as-code using Azure Bicep. Finally, ensure that bicep is updated regularly. Staying up to date will ensure you have the latest functions and features available.