The Azure Image Builder (AIB) Service is a managed service empowering users to customize machine images using a standardized process. As part of the prerequisites, the user is required to instantiate a user-assigned managed identity (UMI) with a custom role to ensure least privilege for the service. In this post, I explain how I translated the documented requirements to an ARM template to facilitate deployment of the prerequisite resources.

The AIB documentation explains the process to create the UMI, the associated role using a JSON blob stored in a GitHub repository, and the role assignment. The steps are described using the CLI with the assumption that they are run on a Linux based system (dependencies on using sed for search/replace inside of the JSON). Personally, I strive for efficiency and accessibility as much as I can. In this case, I thought that having an Azure Resource Manager (ARM) Template would satisfy both requirements.

The first step in the AIB documentation was to create the UMI. This involved setting a couple of environment variables for the target subscription, resource group, and the name of the identity. The dependency on Linux/Mac shell commands was heavy:

# create user assigned identity for image builder to access the storage account
# where the script is located
identityName=aibBuiUserId$(date +'%s')
az identity create -g $sigResourceGroup -n $identityName

# get identity id
imgBuilderCliId=$(az identity show -g $sigResourceGroup -n $identityName -o json
| grep "clientId" | cut -c16- | tr -d '",')

# get the user identity URI, needed for the template
imgBuilderId=/subscriptions/$subscriptionID/resourcegroups/$sigResourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/$identityName

In ARM, creating this identity is straight forward using parameters and variables defined in the template. The section in the template to create the identity is as follows:

{
  "type": "Microsoft.ManagedIdentity/userAssignedIdentities",
  "apiVersion": "2018-11-30",
  "name": "[parameters('aibIdentityName')]",
  "location": "[variables('resourceLocation')]"
},

The name for the object is required as a String value, and the location can be obtained by referring to the location of the target resource group. The resourceLocation variable is defined as follows:

"resourceLocation": "[resourceGroup().location]",

With the identity object created, it was time to move to the roleDefinition object.

The next step in the CLI was to create the role that would be assigned to the identity created in the previous step. The portion of the shell script responsible for this step is the following:

# Use *cURL* to download the a sample JSON description
curl https://raw.githubusercontent.com/danielsollondon/azvmimagebuilder/master/solutions/12_Creating_AIB_Security_Roles/aibRoleImageCreation.json -o aibRoleImageCreation.json

# Create a unique role name to avoid clashes in the same Azure Active Directory domain
imageRoleDefName="Azure Image Builder Image Def"$(date +'%s')

# Update the JSON definition using stream editor
sed -i -e "s/<subscriptionID>/$subscriptionID/g" aibRoleImageCreation.json
sed -i -e "s/<rgName>/$imageResourceGroup/g" aibRoleImageCreation.json
sed -i -e "s/Azure Image Builder Service Image Creation Role/$imageRoleDefName/g" aibRoleImageCreation.json

# Create a custom role from the sample aibRoleImageCreation.json description file.
az role definition create --role-definition ./aibRoleImageCreation.json

Note that there are dependencies in this section to curl, and sed (both of which are only available on Linux/Mac systems by default. After reviewing the JSON for the role, and understanding the required permissions per the documentation, I created the following segment in the ARM template:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
  "type": "Microsoft.Authorization/roleDefinitions",
  "apiVersion": "2018-07-01",
  "name": "[variables('roleDefinitionId')]",
  "properties": {
   "roleName": "[parameters('roleName')]",
   "description": "[parameters('roleDescription')]",
   "permissions": [
     {
       "actions": [
         "Microsoft.Compute/galleries/read",
         "Microsoft.Compute/galleries/images/read",
         "Microsoft.Compute/galleries/images/versions/read",
         "Microsoft.Compute/galleries/images/versions/write",
         "Microsoft.Compute/images/write",
         "Microsoft.Compute/images/read",
         "Microsoft.Compute/images/delete"
       ],
       "notActions": []
     }
   ],
    "assignableScopes": [
      "[resourceGroup().id]"
    ]
  }
},

The challenge in creating this definition was due to the vague requirements for the roleDefinition object. The documentation states that the name variable is required and that it is “The ID of the role definition”. In addition, the roleDefinition object also contains a RoleDefinitionProperties object which has a variable labeled roleName listed as “The role name”. Now, based on my understanding of ARM, every object has a Globally Unique Identifier (GUID), but why wouldn’t this value for Name be generated at object instantiation time, instead of it being required? After several attempts of trial and error, I discovered that this Name variable was indeed required in the template, and be represented as a **GUID&&. To satisfy the requirement, I defined a variable (as referenced in the above section) in the following way:

"roleDefinitionId": "[guid(subscription().subscriptionId,parameters('roleName'))]"

The guid() function in ARM created a GUID from the parameters passed in ensuring that it’s unique for the deployment. By passing a combination of the subscriptionId and the value of the roleName parameter defined at the top of the template, the resulting identifier was unique.

The last phase of the template was to assign the custom role to the managed identity created in the first step. In the example shell script, the documentation uses a single CLI command to create the assignment:

# grant role definition to the user assigned identity
az role assignment create \
    --assignee $imgBuilderCliId \
    --role "$imageRoleDefName" \
    --scope /subscriptions/$subscriptionID/resourceGroups/$sigResourceGroup

Based on how the previous two objects were handled, I didn’t expect that I would have trouble creating this one. It was tough! The challenge was in how the documentation for the roleAssignments object was written. The field descriptions for name, roleDefinitionId, and principalId weren’t clear, which is why I struggled quite a bit. Primarily, because the documentation didn’t use examples. The reason I wrote this blog post was due to how long it took me to figure this out!

1
2
3
4
5
6
7
8
9
10
{
  "type": "Microsoft.Authorization/roleAssignments",
  "apiVersion": "2020-04-01-preview",
  "name": "[guid(subscription().subscriptionId,variables('roleDefinitionId'))]",
  "properties": {
    "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('roleDefinitionId'))]",
    "principalId":
    "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities',parameters('aibIdentityName'))).principalId]"
  }
}

For the name field, I ended up using a similar construct as what I used for roleDefinition name field as ARM requires a GUID. The roleDefinitionId turns out to be the resourceId of the roleDefinition object. Lastly, the principalId is a property given to the managed identity when it’s instantiated which is not any of the field passed in, nor the resource Id. By reviewing the ARM template documentation on functions, I discovered the reference function which was the solution I needed to obtain the value for this property. Depending on the passed in arguments, the function is able to return a significant amount of data on an object.

With the template created, I was able to create the identity, the role, and the assignment using this one line CLI.

az deployment group create -g rg-eastus-aib -n role-azureimagebuilder -p @./parameters.json -f ./deploy.json

User-Assigned Managed Identities can only be created as part of a resource group which is why the above string specifies group and not sub to target the resource group instead of the subscription.

Through this process, I learned the nuances of creating user-assigned managed identities programatically, and associating them to custom roles.