Menu Close

Create role assignments for different scopes with Bicep

When you create Azure resources with Bicep, you are able to set up role assignments as well. This can be very useful in different scenario’s as it enables you to add an extra layer to your infra as code, which you don’t have to perform manually or through other automation. A role assignment is a resource when you use it in Bicep and can be defined as such, but handling the different scope levels can be a challenge. That is why in this post, I will explain how to create role assignments for different scopes with Bicep.

Before you read on, I recommend you scan through this page in the Microsoft Docs, which gives a great explanation of the different properties of a role assignment. In this post, we will focus on how to handle the scope. To do so, I will walk through all scopes with examples of how a template could be deployed.

Management Group Scope

Let’s start with the biggest scope we have available in Azure resource manager: Management Group. This is how you define the role assignment in Bicep:

To deploy it and set the parameters, you can use different methods. For this post I have used PowerShell. As you can see, you need to define the Id for both the roledefinition and the principalID. To make this a bit easier and more readable, I have made use of Get-AzADUser and Get-AzRoleDefinition

Deployment would go like this:

$Parameters = @{
    TemplateFile      = '.\rbac-mg.bicep'
    ManagementGroupId = 'ExampleGroup'
    Location          = 'WestEurope'
    PrincipalType     = 'User'
    PrincipalId       = (Get-AzADUser -UserPrincipalName example@domain.com).id
    RoleDefinitionId  = (Get-AzRoleDefinition -Name 'Reader').id
}

New-AzManagementGroupDeployment @parameters -Verbose

This deployment would refer to the file rbac-mg.bicep, which it will use to give the user example@domain.com the reader role over the management group exampleGroup. Of course you change the values to your own use case.

Subscription scope

So how do we do the same for a subscription? Fortunately, we don’t have to make that many changes. We change the following values:

  • Change targetScope = 'managementGroup' to targetScope = 'subscription'
  • managementGroup()  is changed to subscription()
  • managementGroup().id is changed to subscription().subscriptionId

This is all we have to do for the Bicep template. You can find the new template here.

Note: if you don’t define the targetscope, it will by default use the subscription if you use New-AzDeployment. But for readability, you could consider specifying it anyway.

As for the PowerShell deployment, that would now look like this:

$Parameters = @{
    TemplateFile     = 'rbac-sub.bicep'
    Location         = 'WestEurope'
    PrincipalType    = 'User'
    PrincipalId      = (Get-AzADUser -UserPrincipalName example@domain.com).id
    RoleDefinitionId = (Get-AzRoleDefinition -Name 'Reader').id
}

New-AzDeployment @parameters -Verbose

The role assignment will be deployed to the current scope of the PowerShell session, which you can find by using Get-AzContext.

Resource Group Scope

The resource scope is very similar to the subscription scope. Again, you change the targetscope and the references from subscription() to  resourcegroup().  And for resource groups as well you are able to leave out the targetscope, but you could consider adding it for readability.

To see what the Bicep file would look like, click here.

And deployment would go like this:

$Parameters = @{
    TemplateFile      = 'rbac-rg.bicep'
    ResourceGroupName = 'example'
    PrincipalType     = 'User'
    PrincipalId       = (Get-AzADUser -UserPrincipalName example@domain.com).id
    RoleDefinitionId  = (Get-AzRoleDefinition -Name 'Reader').id
}

New-AzResourceGroupDeployment @parameters -Verbose

Resource scope

A resource resource role assignment is a little different than the previous deployments. You need to refer to the resource within the role assignment, which you do with the scope parameter. To show how it works, I have used a storage account as an example.

A role assignment to a resource has a different use case than to one of the other scopes. If you are doing an assignment for users, a Resource scope is possible, but can make your environment very complicated. It is recommended to keep the assignments at a higher level.
That is different for managed identities or service principles. They need the least amount of principal and could be set on a resource level. An example is a key vault, where you want the role assignment to be very specific.

This is what the bicep file could look like:

I have removed all flexibility in the storage account to keep the focus. In production, you would want some flexibility in the properties of the storage account.

Deployment is the same method as the resource group scope:

#Deploy to a resource
$Parameters = @{
    TemplateFile      = 'rbac-res.bicep'
    ResourceGroupName = 'example'
    PrincipalType     = 'User'
    PrincipalId       = (Get-AzADUser -UserPrincipalName example@domain.com).id
    RoleDefinitionId  = (Get-AzRoleDefinition -Name 'Reader').id
}

New-AzResourceGroupDeployment @parameters -Verbose

Deployment in Modules

This is all cool, but one of the great things about Bicep is the use of modules! So how would you do that? For Management groups, subscriptions, or resource groups, that is pretty straight forward. All of the above files can be used as a module and deployed from a different file with the following syntax:

param rbacprincipalId string
param rbacRoleDefinitionId string

module rbac 'rbac-rg.bicep' = {
  name: 'rbac-Deployment'
  params: {
    principalId: rbacprincipalId
    principalType: 'User'
    RoleDefinitionId: rbacRoleDefinitionId
  }
}

Now with Resources, it is a little more difficult. As we have seen before, you have to define the scope within the role assignment. You are not able to use a parameter for this scope, as it needs the resource itself as a scope. This makes it more difficult to provide flexibility. Even if you would consider making use of the existing option, you wouldn’t get flexibility as you need to define the resource type there. So how doe we make sure we don’t have to create a separate rbac-module for every resource type?

The answer can be found in the files in the brand new public registry: you make the role assignment a part of the resource module. So in this case, for every storage account you provide the option to add a role assignment in the module file.

For-loops

But you still need flexibility. Flexibility to not create a role assignment or to create multiple. Again, we can find a great method at the public registry: we use a for loop that is empty by default. If the role assignment is not needed, just don’t add it as a parameter. If you need more than one, you add them as an array. Let’s see how that would look.

The template

Although the registry uses a submodule, I prefer to add it straight to the resource. This is how the module for a storage account would look:

If we want to create the resource without role assignment, we just omit the roleAssignments parameter. If we do want to create one (or multiple), our main.bicep file would refer to it like this:

By using this method for every resource you have in a module, you will have some duplicate code, but you won’t have a separate rbac-module for every resource type. This will keep your module files clean but still provide flexibility.

Conclusion

So this is how you create role assignments for different scopes with Bicep. You can find all the complete Bicep files that are referred to here.

If you want to learn more about Bicep, follow along with the LearnLive series that are running right now: https://aka.ms/learnlive-iac-and-bicep

1 Comment

  1. GearyS

    I’m also having a problem with the scope of a role assignment. Your post was helpful, but I do have one question. For the example for management group scope, there’s a parameter called “ManagementGroupId”, but it doesn’t seem to be used. I see the role definition is created a the management group level. Is the role assignment also defined at the management group level by default? I assume that’s the case since the targetScope = “managementGroup”. Thanks.

Leave a Reply

Your email address will not be published.