Insight Tech APAC Blog Logo

Loops, Conditions and Conditional Loops

stephentulp
December 7, 2023

7 minutes to read

Bicep Advent Calendar

Introduction

In the previous posts we went through the anatomy and structure of a Bicep file and also learnt about using an external parameter file using the new .bicepparams extension. In this post we will go through the different ways we can use loops and conditions in Bicep to provide greater flexibility in our deployments.

Loops

You can use loops to 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.

To use loops to create multiple resources or modules, each instance must have a unique value for the name property. You can use the index value or unique values in arrays or collections to create the names.

Loops can be declared in a couple of ways.

Integer Index

Using an integer index. This option works when your scenario is: “I want to create this many instances.” The range function creates an array of integers that begins at the start index and contains the number of specified elements. Within the loop, you can use the integer index to modify values.

  [for <index> in range(<startIndex>, <numberOfElements>): {
    ...
  }]

Items in an Array

Using items in an array. This option works when your scenario is: “I want to create an instance for each element in an array.” Within the loop, you can use the value of the current array element to modify values.

  [for <item> in <collection>: {
    ...
  }]

Items in a Dictionary Object

Using items in a dictionary object. This option works when your scenario is: “I want to create an instance for each item in an object.” The items function converts the object to an array. Within the loop, you can use properties from the object to create values.

  [for <item> in items(<object>): {
    ...
  }]

Integer Index and Items in an Array

Using integer index and items in an array. This option works when your scenario is: “I want to create an instance for each element in an array, but I also need the current index to create another value.”

  [for (<item>, <index>) in <collection>: {
    ...
  }]

Loop Examples

The following example creates a resource group for each name provided in the commonResourceGroups array parameter.

param location string = resourceGroup().location

param commonResourceGroups array = [
  'alertsRG'
  'networkWatcherRG'
]

module sharedResourceGroups '../resourceGroup/resourceGroups.bicep' = [for item in commonResourceGroups: {
  name: item
  scope: subscription()
  params: {
    resourceGroupNames: commonResourceGroups
    location: location
  }
}]

Conditions

To optionally deploy a resource or module in Bicep, use the if expression. An if expression includes a condition that resolves to true or false. When the if condition is true, the resource is deployed. When the value is false, the resource isn’t created. The value can only be applied to the whole resource or module.

Condition Examples

The following example conditionally deploys a resource group for networking resources. When virtualNetworkEnabled is true, it deploys the resource group. When virtualNetworkEnabled is false, it skips the deployment.

param virtualNetworkEnabled bool

module resourceGroupForNetwork '../CARML/resources/resource-group/main.bicep' = if (virtualNetworkEnabled) {
  name: 'resourceGroupForNetwork-${guid(deployment().name)}'
  scope: subscription(subscriptionId)
  params: {
    name: resourceGroups.network
    location: location
    tags: tags
  }
}

We can also use !, which is the not operator, this will then deploy the Subscription Tags module if the tags parameter is not empty.

module subscriptionTags '../CARML/resources/tags/main.bicep' = if (!empty(tags)) {
  scope: subscription(subscriptionId)
  name: 'subTags-${guid(deployment().name)}'
  params: {
    subscriptionId: subscriptionId
    location: location
    onlyUpdate: true
    tags: tags
  }
}

Lastly, we can also combine both of them to give additional flexibility. Here we are deploying the Subscription Placement module if the subscriptionManagementGroupAssociationEnabled parameter is true and the subscriptionMgPlacement parameter is not empty. Using &&, which is the and operator makes it so that both conditions need to be true. If they are both true then it will deploy. Otherwise, it will skip the deployment.

module subscriptionPlacement '../../modules/subscriptionPlacement/subscriptionPlacement.bicep' = if (subscriptionManagementGroupAssociationEnabled && !empty(subscriptionMgPlacement)) {
  scope: managementGroup(subscriptionMgPlacement)
  name: 'subscriptionPlacement-${guid(deployment().name)}'
  params: {
    targetManagementGroupId: subscriptionMgPlacement
    subscriptionIds: [
      subscriptionId
    ]
  }
}

Conditional Loops

Conditional Loop deployments works when your scenario is: “I want to create multiple instances, but for each instance I want to deploy only when a condition is true.”

  [for <item> in <collection>: if(<condition>) {
    ...
  }]

The example below will only deploy the role assignments if the roleAssignmentEnabled parameter is true and the roleAssignments parameter is not empty. It will loop through each of the assignments (principalId, definition, principalType, relativeScope) in the RoleAssignments array and assign it at the Subscription scope /.

@description('Whether to create role assignments or not. If true, supply the array of role assignment objects in the parameter called `roleAssignments`.')
param roleAssignmentEnabled bool = true

@description('Supply an array of objects containing the details of the role assignments to create.')
param roleAssignments array = [
    {
      principalId: '2b33ff60-edf0-4216-b2a6-66ec07050fd4'
      definition: 'Reader'
      principalType: 'Group'
      relativeScope: '/'
    }
    {
      principalId: '20bbeee1-e70c-43d3-8c2c-b66fefa31acf'
      definition: 'Owner'
      principalType: 'ServicePrincipal'
      relativeScope: '/'
    }
]

module roleAssignment '../CARML/authorization/role-assignment/main.bicep' = [for assignment in roleAssignments: if (roleAssignmentEnabled && !empty(roleAssignments)) {
  name: take('roleAssignments-${uniqueString(assignment.principalId)}', 64)
  params: {
    location: location
    principalId: assignment.principalId
    roleDefinitionIdOrName: assignment.definition
    subscriptionId: subscriptionId
    resourceGroupName: (contains(assignment.relativeScope, '/resourceGroups/') ? split(assignment.relativeScope, '/')[2] : '')
  }
}]

Conclusion

By using conditions and loops we have a lot of flexibility and options for our deployments. We can now deploy resources and modules conditionally and also loop through arrays and collections to deploy multiple instances of resources and modules. These allow us to provide options for our deployments and also optimize our deployments by only deploying what we need.

Further Reading

Some further reading on the topics covered in this post: