Using Loops in Bicep
I've been using Azure to test virtual machines and applications for quite a few years now and I've realized that although I mostly use two or three solution templates - like an Active Directory environment for example - in the majority of the cases I need more server or client machines. This is the main reason I decided to adjust my deployments and include loops so that I can control the number of machines using parameters and variables.
In this post, I'll describe how I've used loops and hopefully how you can benefit from them. I'm a big fan of organizing resources in modules, so all of the examples will be based on a deployment of virtual networks and virtual machines from a main bicep file using the respective modules, one for each resource type.
Multiple Resources/Modules
Since we're talking about modules, what could be better than having the ability to deploy the same module multiple times! The first thing we need is a variable that will hold the custom fields for each module deployment:
var vms = [ { name: 'AppServer' } { name: 'DBServer' } ]
Here, we have an array of objects whose only property is name and we intend to use this name for each of the machines to be deployed. In the main bicep file the below syntax will deploy a virtual machine for each of the objects in the variable, using a different name every time:
module vm 'modules/vm.bicep' = [for vm in vms: { name: vm.name scope: rg params: { location: location adminUsername: administratorUsername adminPassword: administratorPassword vmName: vm.name subnetId: '${vnet.outputs.Id}/subnets/default' } }]
In each iteration, the variable vm will be instantiated to an object in our array. To access the properties of the object, we use the classic period syntax. It goes without saying that you could add more properties to the array and use them for other resource properties. The result of the deployment is two virtual machines, named according to our array:
The same syntax and approach can be used when having resources instead of modules.
Using the loop index
There are also cases where you might just want to deploy a number of resources - say five virtual machines - and you do not want to create an array with names as we did before. When looping through resources, we can take advantage of the index and adjust the names and configuration of our resources. Take the following code segment for example:
module vm 'modules/vm.bicep' = [for i in range(1,count): { name: 'VM-${i}' scope: rg params: { location: location adminUsername: administratorUsername adminPassword: administratorPassword vmName: 'Server-${i}' subnetId: '${vnet.outputs.Id}/subnets/default' } }]
A parameter named count is used in order to create a specific set of iterations. During each iteration, the name and vmName attributes are updated with the addition of i which is the loop index. The first iteration will create Server-1, the second Server-2, and so on:
Creating multiple sub-resources
Apart from deploying a module or resource multiple times, loops can be used to create multiple properties on a resource deployment, like multiple disks for a virtual machine. Taking a closer look at the below code, we can see that it will create as many data disks for a virtual machine as specified in the disks parameter:
dataDisks: [for i in range(1,disks) : { diskSizeGB: 64 lun: i name: '${vmName}-DataDisk-${i}' createOption: 'Empty' managedDisk: { storageAccountType: 'Premium_LRS' }
}]
Loop Result
The result of a loop can be saved in a variable and used in a resource deployment. This time, we'll use a virtual network and its subnets:
// A variable that contains the names of the subnets var subnets = [ 'Networking' 'AzureBastionSubnet' 'Application' 'Database' ] // Subnets var subnetsArray = [for (name, i) in subnets: { name: 'Subnet-${i}-${name}' properties:{ addressPrefix: '10.0.${i}.0/24' } }] // Virtual Network resource vnet 'Microsoft.Network/virtualNetworks@2020-11-01' = { name: vnetName location: location properties:{ addressSpace:{ addressPrefixes:[ vnetPrefix ] } subnets: subnetsArray // The entire subnets configuration is in the variable } }
First, we create an array with the names of the subnets to configure on the virtual network. Then we save the output of the loop to a variable that is used in the definition of the virtual network as the subnets property. Notice that when each object in the subnetsArray is created, the required properties name and addressPrefix are configured for each object.
Outputs
It's nice to be able to create multiple resources using a loop, but what happens if we need the value of a property from each of the resources, like its resource id? Fortunately, loops are also supported in the output section of bicep:
module vNetResources 'modules/vnet.bicep' = [for vnet in vnets: { name: 'vNet-${vnet.name}' scope: rg params: { vnetName: 'vNet-${vnet.name}-${uniqueString(rg.id)}' vnetPrefix: vnet.prefix location: location } }] output vnets array = [for (vnet, i) in vnets: { name: vNetResources[i].outputs.Name resourceId: vNetResources[i].outputs.Id }]
When done with the deployments, an array containing the resource ids of the virtual networks will be returned as part of the deployment's outputs:
Parallelism
By default, the resources deployed as part of a loop are deployed in parallel. This means that in a multiple virtual machine deployment like in the second example above, all the virtual machine deployments will be submitted at the same time:
You can control the level of parallelism using the batchSize decorator:
@batchSize(2) module vm 'modules/vm.bicep' = [for i in range(1,count): { name: 'VM-${i}' scope: rg params: { location: location adminUsername: administratorUsername adminPassword: administratorPassword vmName: 'Server-${i}' subnetId: '${vnet.outputs.Id}/subnets/default' } }]
Using two as the size of each batch, the virtual machines will be deployed in pairs:
The full code for each of the above examples is available in my Github repo over here.
Happy coding!