Azure, Bicep, DevOps, IaC

Create Management Groups and Subscriptions with Bicep

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

  1. Select Properties and then select the ID
  2. Then go to Billing Profiles and select the profile then select Properties and then the ID
  3. 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):

Note: This diagram was created using Diagrams, see my previous post for details

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.