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.

Module – resourceGroup.bicep
targetScope = 'subscription'
param resourceGroupName string = 'labRG'
param location string = 'eastus'

resource resGroup 'Microsoft.Resources/resourceGroups@2022-09-01' = {
  name: resourceGroupName
  location: location
}
BICEP

The 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.

main.bicep
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
   }
}

BICEP

Use 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.

Bicep Parameters
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.

Loopy
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

}]
BICEP

Use 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.

Implicit Dependencies
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
        }
       }
    ]
  }
}
BICEP

An 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.

Explicit Dependency
@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.

appServicePlan.bicep
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
testParameters.json
{
    "$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
prdParameters.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
        }
    }
}
JSON

Use 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.

getsecret()
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')
  }
}
JSON

The resourceId function returns the ID of an existing resource. It simplifies the traditional syntax and streamlines the codeset.

resourceId()
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)
    }
  }
}]
JSON

The 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.

Sample Output
//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.

environment()
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
  }
BICEP

The 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.