Azure, Bicep, Professional

Nested Loops In Azure Bicep with an RBAC Example

Background

Did you know that “technically” nested loops in Azure Bicep are supported? Neither did I until really digging into it. The scenario I was trying to tackle was the ability to effectively manage/configure subscription level RBAC permissions via Bicep. This seems simple enough; however, how can this be done efficiently and positioned to be scaled in the future? Enter nested loops in Azure Bicep with an RBAC Example.

Anyone can create a parameter file with an entry for each role/roleId combo. This could work; however, what if some groups only should have access to lower environments…well time to do if conditions…..or what if the same group needs multiple roles…..time to repeat the id multiple times.

Wouldn’t the best solution be to provide an object that has all the information? Then Bicep can just loop through 1, 2, 10 Azure AD Objects. Now let’s take it down to another level and each Azure AD object defines the principal type and lists of Azure RoleIDs to be assigned. This can help with scalability and maintainability. Abstractly this would mean that each parameter file can have any amount of RoleIDs and Assignments in them. This could look like:

assignments:
{
    principalID: ADAdminsGroupObjectID
    principalType: Group
    RoleIds:
        "b24988ac-6180-42a0-ab88-20f7382dd24c", // Contributors
        "00482a5a-887f-4fb3-b363-3b7fe8e74483" // Key Vault Admin
},
{
    principalID: ServicePrincipalKeyVaultObjectID
    principalType: ServicePrincipal
    RoleIds:
        "b86a8fe4-44ce-4948-aee5-eccb2c155cd7" // Key Vault Secrets Officer
}

Still with me? How can this be achieved in Bicep? At the time of this writing the official documentation shows you cannot loop on multiple levels of properties.

Snippet from Bicep Documentation on Looping

The key here is understanding each loop resource substantiation can’t access the multiple levels. So, what if we passed the object to a module and that module then looped through the lower levels?

PreReqs

Whatever account will be deploying the Bicep file will need to have access to provision RBAC access for this walkthrough to work. In this example this deployment will occur via an Azure DevOps (ADO) service principal assigned Owner and will be executing the bicep as part of a CI/CD Software Delivery Lifecycle (SDLC)

The minimum version of Bicep required is version v.0.3.1 which supports looping.

Parameters File

The parameters file will be laid out similar to our pseudo code above.

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "roleAssignmentInfo":{
          "value":{
            "assignments":[
              {
                "principalID": "99999999-8888-7777-6666-555555555555", 
                "principalType": "Group",
                "roleIDs": [
                    "b24988ac-6180-42a0-ab88-20f7382dd24c", // Contributors
                    "00482a5a-887f-4fb3-b363-3b7fe8e74483" // Key Vault Admin
                ]
              },
              {
                "principalID": "44444444-3333-2222-1111-000000000000", 
                "principalType": "ServicePrincipal",
                "roleIDs": [
                   "b86a8fe4-44ce-4948-aee5-eccb2c155cd7" // Key Vault Secrets Officer
                ]
              }
              ]
              }
          }
        }
    }

The intent here is to help ease maintainability. It helps to be able to quickly identify all the roles being provisioned to a specific Azure Active Directory (AAD) object.

Main.bicep

This is pretty straightforward as we will be using modules. All that’s required is for us to pass the parameter, which is an object containing the information for all the assignments that will be provisioned and a module that will loop into another module.

targetScope = 'subscription'
param roleAssignmentInfo object

module RBACRoles 'modules/var_roleAssignmentInfo_module.bicep'= [for (roleAssignmentInfo,i) in array(roleAssignmentInfo) : {
  name: 'RBACAssignmentModule${i}'
  scope: subscription()
  params:{
    assignment: roleAssignmentInfo.assignments
  }
}]

So, something to notice here is we are using the index array in the loop. Since this module will expand out, we can’t have the same name for each iteration so the index will help differentiate each module substantiation.

It never occurred to me there can be a module without any Azure resources defined in it.

var_roleAssignmentInfo_module.bicep

This module’s sole purpose is to loop into the assignments array value.

param assignment array

module RBACAssignments 'auth_roleassignment_sub.module.bicep' = [for roleAssignmentInfo in array(assignment) : {
  name: 'RBACAssignmentModule${roleAssignmentInfo.principalID}'
  scope: subscription()
  params:{
    principalID: roleAssignmentInfo.principalID
    principalType: roleAssignmentInfo.principalType
    roleIDs: roleAssignmentInfo.RoleIDs
  }
}]

We were sneaky and switched the parameter to an array which will now loop through each AAD ID that is in our parameter file. The list array of RoleIDs will be passed to the next module for further iteration.

auth_roleassignment_sub.module.bicep

param principalID string
@allowed([
  'Group'
  'ServicePrincipal'
])
param principalType string
param roleIDs array
targetScope = 'subscription'

resource roleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = [for (roleID,i) in roleIDs: {
  name: guid(subscription().id, principalID, roleID)
  properties: {
    roleDefinitionId: 'providers/Microsoft.Authorization/roleDefinitions/${roleID}'
    principalId: principalID
    principalType: principalType
  }
}]

Now for the actual role assignments. At this stage an input may contain a single principalID, a Principal type, and a list of roleIDs. I did through in allowed types of only Groups and ServicePrincipals to keep security happy as assigning individual roles at a subscription level is typically not a good practice.

So, the next step here is to loop through all the roleIDs that are passed in and actual execute the individual role assignment.

Conclusion

There you have it! Nested loops in Azure Bicep with an RBAC example. It feels a little like Inception with a module inside a module and honestly not something I would typically recommend unless you have it. That being said it is kind of cool to recognize that something like this could be supported in the work you are doing.

Sources

There is a great discussion I started over on the Bicep git repo on Assigning RBAC via Role Assignment Module. There is a TON of good information on there as honestly my initial vision for this was to leverage a private registry for Bicep modules.

With intention being pass in principalID, type, role, and scope. Could you imagine this helping with scaling out how roles can be assigned across an organization? Even giving control over what roles and types can be assigned by the module. Alas, this proved a little too complicated for what we were looking for given the timeframe. However, maybe there are alternatives?

For additional content on Bicep check out my posts on Introl: CI/CD Pipelines for Bicep or if you prefer video a presentation on using Bicep done for the 20201 Cloud Lunch and Learn Marathon