Nested Loops In Azure Bicep - 4 Use Cases, For-Loop, Solutions & Workarounds

Loops are one of the fundamental programming constructs which are common in many programming languages. However, Azure Bicep loops are a bit limited, and there could be some challenges in introducing nested loops in particular. In this post, we will see such use cases and how to handle them.

In Azure Bicep there is only for-loop which can be used with resources, modules, variables, and outputs. For example, using for-loop allows defining multiple resources in the same resource declaration which removes code duplication and provides more flexibility.

Another common use case for (nested) loops is to pass complex configuration as a parameter and apply this config in the main template using loops and other constructs. This way, to add a new resource/instance, we only need to modify our parameter value without changing the main template.

Throughout this post we are going to use Azure Traffic Manager resource in the code snippets because it is a good example where we can apply nested loops. In addition to that, traffic manager profile’s ARM template Microsoft.Network/trafficManagerProfiles is relatively small and easy to understand.

Contents:

Overview

In this post we will discuss different use cases when one might need to use a nested loop.

To make it easier to understand, we are going to use traffic manager profile in our examples because it has endpoints property which is an array, and each endpoint object expects a customHeaders array property:
Microsoft.Network/trafficManagerProfiles → endpoints → customHeaders

Next sections cover four use cases where nested loops come in handy and how to apply them or work around the limitation:

  1. Resource Loop [] → Nested Loop [] - very common use case, easy to implement
    resource trafficManagerProfiles[] → property endpoints[]
  2. Resource → Property Loop [] → Nested Loop [] - currently, there is a limitation in Bicep and ARM template that does not allow nested loop like this one, but we discuss two options how to work around it!
    resource trafficManagerProfile → property endpoints[] → property customHeaders[]
  3. Resource Loop [] → Property Loop [] → Nested Loop [] - this pattern becomes straightforward once we know how to implement use case #2.
    resource trafficManagerProfiles[] → property endpoints[] → property customHeaders[]
  4. Resource → Children Loop [] → Nested Loop [] - this section discusses how to reduce this pattern to the use case #1 by leveraging the fact that these are child resources.

1. Resource Loop [] → Nested Loop []

Example: resource trafficManagerProfiles[] → property endpoints[]

This is the basic case when we want to declare multiple resources in a for-loop and each resource instance has an array property. For example, multiple traffic managers each with multiple endpoints.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// Config of two traffic managers with endpoints
param trafficManagerConfigs array = [
  {
    name: 'contoso1'
    endpoints: [
      {
        name: 'endpointA'
        target: 'aaa.ochzhen.com'
      }
      {
        name: 'endpointB'
        target: 'bbb.ochzhen.com'
      }
    ]
  }
  {
    name: 'contoso2'
    endpoints: [
      {
        name: 'endpointC'
        target: 'ccc.ochzhen.com'
      }
    ]
  }
]

// Resource loop
resource trafficManagerProfiles 'Microsoft.Network/trafficManagerProfiles@2018-08-01' = [for trafficManagerConfig in trafficManagerConfigs: {
  name: trafficManagerConfig.name
  location: 'global'
  properties: {
    trafficRoutingMethod: 'Weighted'
    ...
    // Property loop
    endpoints: [for endpoint in trafficManagerConfig.endpoints: {
      type: 'Microsoft.Network/trafficManagerProfiles/externalEndpoints'
      id: '${resourceId('Microsoft.Network/trafficManagerProfiles', trafficManagerConfig.name)}/externalEndpoints/${endpoint.name}'
      name: endpoint.name
      properties: {
        endpointStatus: 'Enabled'
        target: endpoint.target
      }
    }]
  }
}]

2. Resource → Property Loop [] → Nested Loop []

Example: resource trafficManagerProfile → property endpoints[] → property customHeaders[]

Here things become more complicated. While having a nested loop inside of resource loop is fine, having nested loop inside of a property loop is not supported as of October 2021 (see github issue: Is there plans to support nested loop on resources?).

In this case, there are a few ways how to approach this problem. Note that customHeaders property is the one that causes problems for us since we cannot define a nested loop for it.

[Option 1 (easier)] Avoid Nested Loop By Passing Complete Array Property

Since there is no Azure Bicep or ARM template support for nested loops inside of a property, the easiest thing we can do is to avoid the need for the nested loop.

A simple way to eliminate nested loop is by passing the valid array which is of the needed format. This will allow to eliminate the mapping which nested loop does.

In the case of traffic manager endpoints, this means that customHeaders is passed as is from the parameter. As always, it’s better to see an example, pay attention to the use of customHeaders array on lines 10 and 64.

NOTE: This works well on the arrays whose elements are quite small, for example, customHeaders array consists of objects with only two properties: name and value. Now imagine that each element has 10 properties many of which are static, this would lead to a lot of duplication (see [Option 2 (harder)] Use Module To Implement Nested Loop for an alternative solution).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// ========== trafficmanager.bicep ==========

param trafficManagerConfigs array = [
  {
    name: 'contoso1'
    endpoints: [
      {
        name: 'endpointA'
        target: 'aaa.ochzhen.com'
        customHeaders: [
          {
            name: 'header1'
            value: 'value1'
          }
          {
            name: 'header2'
            value: 'value2'
          }
        ]
      }
      {
        name: 'endpointB'
        target: 'bbb.ochzhen.com'
        customHeaders: [
          {
            name: 'header3'
            value: 'value3'
          }
        ]
      }
    ]
  }
  {
    name: 'contoso2'
    endpoints: [
      {
        name: 'endpointC'
        target: 'ccc.ochzhen.com'
        customHeaders: [
          {
            name: 'header4'
            value: 'value4'
          }
        ]
      }
    ]
  }
]

resource trafficManagerProfiles 'Microsoft.Network/trafficManagerProfiles@2018-08-01' = [for trafficManagerConfig in trafficManagerConfigs: {
  name: trafficManagerConfig.name
  location: 'global'
  properties: {
    trafficRoutingMethod: 'Weighted'
    ...
    endpoints: [for endpoint in trafficManagerConfig.endpoints: {
      type: 'Microsoft.Network/trafficManagerProfiles/externalEndpoints'
      id: '${resourceId('Microsoft.Network/trafficManagerProfiles', trafficManagerConfig.name)}/externalEndpoints/${endpoint.name}'
      name: endpoint.name
      properties: {
        endpointStatus: 'Enabled'
        target: endpoint.target
        // The customHeaders array in the config has the same format as property expects as input
        customHeaders: endpoint.customHeaders
      }
    }]
  }
}]

[Option 2 (harder)] Use Module To Implement Nested Loop

Modules in Azure Bicep are a very useful construct which can help solve the problem with a nested loop, read more about modules in Learn Modules In Azure Bicep - Basics To Advanced, How It Works, Nested Modules, Outputs, Scopes.

NOTE: The format of the customHeaders array in config is changed from {"name": "", "value": ""} to {"key": "", "val": ""}. This is just to illustrate the need for mapping from one format to another so that the problem cannot be solved using [Option 1 (easier)] Avoid Nested Loop By Passing Complete Array Property.

The idea is to perform customHeaders mapping inside of a module invocation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// ========== trafficmanager.bicep ==========

param trafficManagerConfig object = {
  name: 'contoso1'
  endpoints: [
    {
      name: 'endpointA'
      target: 'aaa.ochzhen.com'
      customHeaders: [
        {
          key: 'header1'
          val: 'value1'
        }
        {
          key: 'header2'
          val: 'value2'
        }
      ]
    }
    {
      name: 'endpointB'
      target: 'bbb.ochzhen.com'
      customHeaders: [
        {
          key: 'header3'
          val: 'value3'
        }
      ]
    }
  ]
}

module mappedHeadersForEndpoints 'mapped-headers.bicep' = [for endpoint in trafficManagerConfig.endpoints: {
  name: endpoint.name
  params: {
    customHeaders: endpoint.customHeaders
  }
}]

resource trafficManagerProfiles 'Microsoft.Network/trafficManagerProfiles@2018-08-01' = {
  name: trafficManagerConfig.name
  location: 'global'
  properties: {
    trafficRoutingMethod: 'Weighted'
    ...
    endpoints: [for (endpoint, idx) in trafficManagerConfig.endpoints: {
      type: 'Microsoft.Network/trafficManagerProfiles/externalEndpoints'
      id: '${resourceId('Microsoft.Network/trafficManagerProfiles', trafficManagerConfig.name)}/externalEndpoints/${endpoint.name}'
      name: endpoint.name
      properties: {
        endpointStatus: 'Enabled'
        target: endpoint.target
        customHeaders: mappedHeadersForEndpoints[idx].outputs.mappedHeaders
      }
    }]
  }
}

The helper module simply performs mapping and returns the result as an output.

1
2
3
4
5
6
7
8
9
10
// ========== mapped-headers.bicep ==========

param customHeaders array

var mappedHeaders = [for header in customHeaders: {
  name: header.key
  value: header.val
}]

output mappedHeaders array = mappedHeaders

3. Resource Loop [] → Property Loop [] → Nested Loop []

Example: resource trafficManagerProfiles[] → property endpoints[] → property customHeaders[]

Now, let’s take the solution for Resource → Property Loop → Nested Loop and extend it to work with multiple resources at once.

Luckily, this is extremely simply to achieve. Remember that we have trafficmanager.bicep template from the previous section (either from Option 1 or Option 2).

Let’s add a new file named main.bicep which accepts an array of traffic manager configurations and for each item invokes trafficmanager.bicep. This is all we need to do, here are a few notes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ========== main.bicep ==========

param trafficManagerConfigs array = [
  {
    name: 'contoso1'
    endpoints: [
      ...
    ]
  }
  {
    name: 'contoso2'
    endpoints: [
      ...
    ]
  }
]

module trafficManagers 'trafficmanager.bicep' = [for trafficManagerConfig in trafficManagerConfigs: {
  name: trafficManagerConfig.name
  params: {
    trafficManagerConfig: trafficManagerConfig
  }
}]

4. Resource → Children Loop [] → Nested Loop []

As we have already seen in the previous sections, having a nested loop (on customHeaders) inside of a property loop (on endpoints) is not currently supported by ARM templates and Bicep. And as a result, we had to do some workarounds.

However, if we are working with an array of child resources, then the solution is much simpler. The reason is that child resources can be declared outside of its parent resource.

So, the problem actually is getting simplified to the Resource Loop → Nested Loop case where we first deploy a parentResource and then an array of child childResources[].

resource parentResource
resource childResources[] → property nestedArray[]

Read more about Child Resources In Azure Bicep - 3 Ways To Declare, Loops, Conditions.