Keith Drew.NET

Management Groups with ARM

There is big interest in automation of Azure, but most of what you see if around the automation of Azure resources. The question becomes how much can we automate, and looking at the Microsoft CAF and especially Landing Zones then we can probably automate pretty much all of the resources deployed into an Azure tenancy.

Management Groups, in Azure, are essential for the organisation of subscriptions and the foundation of creating a sustainable security framework through policies. No no matter how you want to organise your hierarchy, it seems to me a good idea to start your automation journey here. So I had several aims when looking at Management Groups and ARM templates.

  Principles

With the above in mind lets explore how we can create and maintain a management group hierarchy using ARM templates and a deployment pipeline. And yes this will be in Azure DevOps as this is currently my favourite tool, but the techniques could easily be anywhere.


So where to start, lets review what Management Groups are, from an ARM perspective. Management Groups are a tenant level resource, even when you create a hierarchy they are still the tenant responsibility for management, so when considering these in ARM ideally you should be creating all the groups through an template level deployment. This is true even when setting a parent ID, sure you can define in a management group template, but then you have to set the scope to the tenant. The other thing to remember is that ALL Azure tenancies have a single root level Management Group, it is generally a bad idea to deploy anything to this group. There have been bugs in the past that meant a resource like a new RBAC role could be created at this level, but it could never be deleted.

So Lets see the schema for a Management Group

{
   "name": "string",
   "type": "Microsoft.Management/managementGroups",
   "apiVersion": "2020-05-01",
   "scope": "string",
   "properties": {
      "displayName": "string",
      "details": {
         "parent": { "id": "string" }
      }
   },
   "resources": []
}

Quite simple really, we can discount the scope field as this is only used to specify a scope different to the one of the main template, remember we are going to be using a tenant level deployment.  So our main settings to worry about are the id, display name and parent.  Right now we are not going to concern ourselves with any sub resources.

If you create a new Management Group through the Azure portal, then the id defaults to a GUID, it seems sensible to keep this convention.  We can create a Guid style string in ARM quite easily and even make it idempotent which is essential if we want to deploy multiple times.  So with this information lets look at a simple template, in fact the one I started with

{
   "$schema": "https://schema.management.azure.com/schemas/2019-08-01/tenantDeploymentTemplate.json#",
   "contentVersion": "1.0.0.0",
   "variables": {
     "SandboxId": "[guid('Sandbox')]"
   },
   "resources": [
     {
       "name": "[variables('SandboxId')]",
       "type": "Microsoft.Management/managementGroups",
       "apiVersion": "2020-05-01",
       "properties": {
         "displayName": "Sandbox"
         }
       }
     },
     {
       "name": "[guid('Architecture')]",
       "type": "Microsoft.Management/managementGroups",
       "apiVersion": "2020-05-01",
       "dependsOn": [
         "[variables('SandboxId')]"
       ],
       "properties": {
         "displayName": "Architecture",
         "details": {
           "parent": {
             "id": "[tenantResourceId('Microsoft.Management/managementGroups', variables('SandboxId'))]"
           }
         }
       }
     }
   ]
}

So this will create a single Management Group, within the Tenant Root Group, called Sandbox, and a second Management Group under the Sandbox called Architecture.  This template has one big issue in that the template is untestable, we cannot run the template against anything but the Tenant Root Group.  There are several reasons for this, the first Management Group will always be created in the root Management Group as there is no parent id specified, plus all the ID’s created will ALWYAS be the same GUID style string.  For this simple template it probably does not matter, but with some simple changes we can make the template much more flexible and allow full testing.

Lets have a look at the end result

{
   "$schema": "https://schema.management.azure.com/schemas/2019-08-01/tenantDeploymentTemplate.json#",
   "contentVersion": "1.0.0.0",
   "parameters": {
     "rootManagementGroup": {
       "type": "string",
       "metadata": {
         "description":  "The root management group for creating the structure."
       }
     }
   },
   "variables": {
     "SandboxId": "[guid(concat(parameters('rootManagementGroup'), '-Sandbox'))]"
     "ArchitectureId": "[guid(concat(parameters('rootManagementGroup'), '-Architecture'))]"
   },
   "resources": [
     {
       "name": "[variables('SandboxId')]",
       "type": "Microsoft.Management/managementGroups",
       "apiVersion": "2020-05-01",
       "properties": {
         "displayName": "Sandbox",
         "details": {
           "parent": {
             "id": "[tenantResourceId('Microsoft.Management/managementGroups', parameters('rootManagementGroup'))]"
           }
         }
       }
     },
     {
       "name": "[variables('Architecture')]",
       "type": "Microsoft.Management/managementGroups",
       "apiVersion": "2020-05-01",
       "dependsOn": [
         "[variables('SandboxId')]"
       ],
       "properties": {
         "displayName": "Architecture",
         "details": {
           "parent": {
             "id": "[tenantResourceId('Microsoft.Management/managementGroups', variables('SandboxId'))]"
           }
         }
       }
     }
   ]
}

So with a simple change the effects are profound, adding in the parameter for the root management Group we can specify the start of a hierarchy that we want to create the groups from.  For production use this would be the Id of the Tenant root, which always happens to be the same as your Azure AD,  but we can pass in any Management Group we require, remember you can only go 6 deep though.  Then changing the Sandbox ID to use the concatenation of the root Management Group and the display name means we can run this template multiple times within the same tenant, with different roots and we will get different ID’s but using the same parameters they are idempotent.  If we did the same with teh Architecture ID then the template coudl be deployed multiple times to test its effect AND be deployed to the tenant root group.

Tie this with the ARM-TTK tests and a –whatif deployment we can develop and change the template and gain certainty on how it will deploy and how it will effect our tenancy before we actually perform a deployment.  And yes before any one comments I know this template as is will not pass the default ARM-TTK tests

Are there downsides, yes the amount of time it takes to develop the template is slightly longer, to be honest for most management groups structures its probably quicker to create once through the portal.  But I really do believe that logging into the Azure portal is not the way forward.  Additionally with this template I can then run a drift report weekly / monthly and ask hey who is doing something and why, if something has changed from what is expected.

Posted By Keith Drew on 04/02/2021
DevOps , DevOps , Azure