Recently I had the opportunity to do something that I had never done before and setup Management Groups and Subscriptions for a new Tenant in Azure. As there was going to be quite a few subscriptions and groups I wondered about using Bicep to do this.
Getting Started
In order to programmatically create Subscriptions, according to the docs, you must have an owner, contributor, or Azure subscription creator role on an invoice section or owner or contributor role on a billing profile or a billing account to create subscriptions.
So I added Contributor rights on the Billing Account for my user but I still had some permission issues and found I needed elevate permissions using this command.
az role assignment create --scope '/' --role 'Owner' --assignee-object-id $(az ad signed-in-user show --query objectId)
Note: My user has the Global Admin role
In order to find the billing scope you can use the Azure Portal by navigating to Cost Management + Billing
- Select Properties and then select the ID
- Then go to Billing Profiles and select the profile then select Properties and then the ID
- Then go to Invoice Sections select the invoice section then Properties then the ID
You can then use that information to build up a valid Billing Scope id “/providers/Microsoft.Billing/billingAccounts/[1. Billing Account ID]/billingProfiles/[2. Profile ID]/invoiceSections/[3. Invoice ID]”
I like the idea of getting the ids using the Azure CLI and after some experimentation I was able to build a single Azure CLI command to get the appropriate full id needed to create subscriptions by supplying the name of the billing account and the name of the invoice section.
az billing profile list --account-name (az billing account list --query "[?displayName == '<Name of Billing account>'].name" -o tsv) --expand "InvoiceSections" --query "[*].invoiceSections[].value[?displayName == '<Name of Invoice Section>'].id" -o tsv
So, let’s assume I want to create this structure (I’ve substituted the real names with generic ones e.g. Main, Project1):

Bicep
I wanted to be able to specify the main management group and also sub management groups with subscription names. Then I saw this as building the Subscriptions, Management Groups and then moving the Subscription to the specified Management Group. The end result is the below bicep file and modules.
Main/Deploy File
targetScope = 'tenant'
@description('Provide the full resource ID of billing scope to use for subscription creation.')
param billingScope string
@description('The name of the main group')
param mainManagementGroupName string = 'mg-main'
@description('The display name for the main group')
param mainMangementGroupDisplayName string = 'Main Management Group'
param managementGroups array = [
{
name: 'mg-project1-non-prod'
displayName: 'Project1 Non-Prod Management Group'
subscriptions: [
{
name: 'project1-dev'
workload: 'Production'
}
{
name: 'project1-test'
workload: 'Production'
}
]
}
{
name: 'mg-project1-prod'
displayName: 'Project1 Prod Management Group'
subscriptions: [
{
name: 'project1-prod'
workload: 'Production'
}
]
}
{
name: 'mg-project2-non-prod'
displayName: 'Project2 Non-Prod Management Group'
subscriptions: [
{
name: 'project2-dev'
workload: 'Production'
}
{
name: 'project2-test'
workload: 'Production'
}
]
}
{
name: 'mg-project2-prod'
displayName: 'Project2 Prod Management Group'
subscriptions: [
{
name: 'project2-prod'
workload: 'Production'
}
]
}
{
name: 'mg-infrastructure'
displayName: 'Infrastructure Management Group'
subscriptions: [
{
name: 'infrastructure'
workload: 'Production'
}
]
}
]
resource mainManagementGroup 'Microsoft.Management/managementGroups@2020-02-01' = {
name: mainManagementGroupName
scope: tenant()
properties: {
displayName: mainMangementGroupDisplayName
}
}
module subsModule './modules/subs.bicep' = [for group in managementGroups: {
name: 'subscriptionDeploy-${group.name}'
params: {
subscriptions: group.subscriptions
billingScope: billingScope
}
}]
module mgSubModule './modules/mg.bicep' = [for (group, i) in managementGroups: {
name: 'managementGroupDeploy-${group.name}'
scope: managementGroup(mainManagementGroupName)
params: {
groupName: group.name
groupDisplayName: group.displayName
parentId: mainManagementGroup.id
subscriptionIds: subsModule[i].outputs.subscriptionIds
}
dependsOn: [
subsModule
]
}]
Management Group Module
targetScope = 'managementGroup'
param groupName string
param groupDisplayName string
param subscriptionIds array
param parentId string
resource mainManagementGroup 'Microsoft.Management/managementGroups@2020-02-01' = {
name: groupName
scope: tenant()
properties: {
displayName: groupDisplayName
details: {
parent: {
id: parentId
}
}
}
}
resource subscriptionResources 'Microsoft.Management/managementGroups/subscriptions@2020-05-01' = [for sub in subscriptionIds: {
parent: mainManagementGroup
name: sub.id
dependsOn: [
mainManagementGroup
]
}]
output groupId string = mainManagementGroup.id
Subscription Module
targetScope = 'tenant'
@description('Provide the full resource ID of billing scope to use for subscription creation.')
param billingScope string
param subscriptions array = []
resource subscriptionAlias 'Microsoft.Subscription/aliases@2020-09-01' = [for sub in subscriptions: {
scope: tenant()
name: sub.name
properties: {
workload: sub.workload
displayName: sub.name
billingScope: billingScope
}
}]
output subscriptionIds array = [for (subs, i) in subscriptions: {
id: subscriptionAlias[i].properties.subscriptionId
}]
Running
Running the bicep, using the script from earlier we can get the scope and update the parameter to run it with a what-if to see what will be built.
$scope = az billing profile list --account-name (az billing account list --query "[?displayName == '<Name of Billing account>'].name" -o tsv) --expand "InvoiceSections" --query "[*].invoiceSections[].value[?displayName == '<Name of Invoice Section>'].id" -o tsv
az deployment tenant what-if --name "subscriptions-deploy" --location uksouth --template-file main.bicep --parameters billingScope=$scope
We can then run using create to actually run in the script.
az deployment tenant create --name "subscriptions-deploy" --location uksouth --template-file main.bicep --parameters billingScope=$scope
Once all this ran in to took a bit of time for the Subscriptions to move under the Management Groups in the Azure Portal.
It’s great that something like this can be done using Bicep and so much easier than using ARM templates. Bicep is really becoming my go to for Infrastructure as Code.