There is a new way to reference managed identity in ARM template

I have been using managed identity (aka Managed Service Identity - MSI) in Azure for several years now. It's a best practice and a very convenient way to assign an identity (Service Principal) to an Azure resource. A new way to reference managed identities in ARM templates has been introduced recently and its not very well documented, read through for more details.

What is Managed Identity?

Here is the description from Microsoft's documentation:

The managed identities for Azure resources feature in Azure Active Directory (Azure AD) solves this problem. The feature provides Azure services with an automatically managed identity in Azure AD. You can use the identity to authenticate to any service that supports Azure AD authentication, including Key Vault, without any credentials in your code.

There are two types of managed identities:

  • A system-assigned managed identity is enabled directly on an Azure service instance. When the identity is enabled, Azure creates an identity for the instance in the Azure AD tenant that's trusted by the subscription of the identity instance. After the identity is created, the credentials are provisioned onto the instance. The lifecycle of a system-assigned identity is directly tied to the Azure service instance that it's enabled on. If the instance is deleted, Azure automatically cleans up the credentials and the identity in Azure AD.
  • A user-assigned managed identity is created as a standalone Azure resource. Through a create process, Azure creates an identity in the Azure AD tenant that's trusted by the subscription in use. After the identity is created, the identity can be assigned to one or more Azure service instances. The lifecycle of a user-assigned identity is managed separately from the lifecycle of the Azure service instances to which it's assigned.

Assigning a managed identity to a resource in ARM template

First, you need to tell ARM that you want a managed identity for an Azure resource. It can be a Web site, Azure Function, Virtual Machine, AKS, etc. To do so, you add the identity section on your resource definition in your template.

Here is an example of a system-assigned managed identity on a Azure Function.

{
      "type": "Microsoft.Web/sites",
      "kind": "functionapp",
      "name": "[variables('uniqueResourceNameBase')]",
      "apiVersion": "2016-08-01",
      "location": "[resourceGroup().location]",
      "identity": {
        "type": "SystemAssigned"
      },
      "properties": { ... }
}

This part is fairly easy to achieve. Now imagine you want, in or outside your template, to use the managed identity information for permission and role assignments, how would you do that?

Since the change in fairly recent, not all the documentation is up to date and it will take some time to be updated everywhere.

Let's imagine that we want to grant our managed identity access to a Key Vault via its access policies.

Referencing, the old way

What you'll find around on the internet is to grab the information this way:

{
  "type": "Microsoft.KeyVault/vaults",
  "name": "[variables('uniqueResourceNameBase')]",
  "apiVersion": "2016-10-01",
  "location": "[resourceGroup().location]",
  "properties": {
    "sku": {
      "family": "A",
      "name": "standard"
    },
    "tenantId": "[subscription().tenantid]",
    "accessPolicies": [
      {
        "tenantId": "[subscription().tenantid]",
        "objectId": "[reference(concat(resourceId('Microsoft.Web/sites', variables('uniqueResourceNameBase')), '/providers/Microsoft.ManagedIdentity/Identities/default'), '2015-08-31-PREVIEW').principalId]",
        "permissions": {
          "keys": [],
          "secrets": [
            "get"
          ],
          "certificates": []
        }
      }
    ],
    "enabledForDeployment": false,
    "enabledForDiskEncryption": false,
    "enabledForTemplateDeployment": false
  },
  "dependsOn": [
    "[resourceId('Microsoft.Web/sites', variables('uniqueResourceNameBase'))]"
  ]
}

The template expression that is responsible to fetch the managed identity information and expose the objectId (via the principalId property) is this one:

[reference(concat(resourceId('Microsoft.Web/sites', variables('uniqueResourceNameBase')), '/providers/Microsoft.ManagedIdentity/Identities/default'), '2015-08-31-PREVIEW').principalId]

This work today (at the time of this writing) and will continue for some time but is not the recommended approach anymore.

Referencing, the new way

The identity information is now available directly from a resource that support managed identity when you fetch its full set of data. You can obtain the full definition by using the reference function. For the new approach to work, you need to pass the string value full as the last parameter of the reference template function.

Here is a documentation extract for the reference template function:

Usage
reference(resourceName or resourceIdentifier, [apiVersion], ['Full'])

For the last parameter, which is optional:

Value that specifies whether to return the full resource object. If you don't specify 'Full', only the properties object of the resource is returned. The full object includes values such as the resource ID and location.

Here is the new construct targeting the same identity that we saw a little bit earlier

[reference(resourceId('Microsoft.Web/sites', variables('uniqueResourceNameBase')), '2019-08-01', 'full').identity.principalId]

First, we need to build a resource identifier using the resourceId template function. We then provide the apiVersion to use for that resource type (here: Microsoft.Web/sites). Finally we provide the full parameter to indicate we want the full object, not just the base properties.

Personally I like it better this way, it is more concise. It is the way to get that sort of information from now on.

Complete example

Here is a complete and functional ARM template that use the new construct to populate the access policy of a Key Vault with the system managed identity information of an Azure Function.

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
      "location": {
          "type": "string",
          "defaultValue": "[resourceGroup().location]"
      }
  },
  "variables": {
      "uniqueResourceNameBase": "[uniqueString(resourceGroup().id, parameters('location'), deployment().name)]"
  },
  "resources": [
    {
      "type": "Microsoft.KeyVault/vaults",
      "name": "[variables('uniqueResourceNameBase')]",
      "apiVersion": "2016-10-01",
      "location": "[parameters('location')]",
      "properties": {
        "sku": {
          "family": "A",
          "name": "standard"
        },
        "tenantId": "[subscription().tenantid]",
        "accessPolicies": [
        {
            "tenantId": "[subscription().tenantid]",
            "objectId": "[reference(resourceId('Microsoft.Web/sites', variables('uniqueResourceNameBase')),'2019-08-01', 'full').identity.principalId]",
            "permissions": {
              "keys": [],
              "secrets": [
                "get"
              ],
              "certificates": []
            }
          }
        ],
        "enabledForDeployment": false,
        "enabledForDiskEncryption": false,
        "enabledForTemplateDeployment": false
      },
      "dependsOn": [
        "[resourceId('Microsoft.Web/sites', variables('uniqueResourceNameBase'))]"
      ]
    },
    {
      "type": "Microsoft.Storage/storageAccounts",
      "sku": {
        "name": "Standard_LRS",
        "tier": "Standard"
      },
      "kind": "Storage",
      "name": "[variables('uniqueResourceNameBase')]",
      "apiVersion": "2019-06-01",
      "location": "[parameters('location')]",
      "scale": null,
      "properties": {
        "supportsHttpsTrafficOnly": false,
        "encryption": {
          "services": {
            "file": {
              "enabled": true
            },
            "blob": {
              "enabled": true
            }
          },
          "keySource": "Microsoft.Storage"
        }
      },
      "dependsOn": []
    },
    {
      "type": "Microsoft.Web/serverfarms",
      "apiVersion": "2018-02-01",
      "name": "[variables('uniqueResourceNameBase')]",
      "location": "[parameters('location')]",
      "sku": {
        "name": "Y1",
        "tier": "Dynamic"
      },
      "properties": {
        "name": "[variables('uniqueResourceNameBase')]",
        "computeMode": "Dynamic"
      }
    },
    {
      "type": "Microsoft.Web/sites",
      "kind": "functionapp",
      "name": "[variables('uniqueResourceNameBase')]",
      "apiVersion": "2016-08-01",
      "location": "[parameters('location')]",
      "identity": {
        "type": "SystemAssigned"
      },
      "properties": {
        "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('uniqueResourceNameBase'))]",
        "siteConfig": {
          "appSettings": [
            {
              "name": "AzureWebJobsStorage",
              "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('uniqueResourceNameBase'), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('uniqueResourceNameBase')),'2015-05-01-preview').key1)]"
            },
            {
              "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
              "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('uniqueResourceNameBase'), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('uniqueResourceNameBase')),'2015-05-01-preview').key1)]"
            },
            {
              "name": "WEBSITE_CONTENTSHARE",
              "value": "[toLower(variables('uniqueResourceNameBase'))]"
            },
            {
              "name": "FUNCTIONS_EXTENSION_VERSION",
              "value": "~2"
            },
            {
              "name": "WEBSITE_NODE_DEFAULT_VERSION",
              "value": "~10"
            },
            {
              "name": "FUNCTIONS_WORKER_RUNTIME",
              "value": "dotnet"
            }
          ]
        }
      },
      "dependsOn": [
        "[resourceId('Microsoft.Web/serverfarms', variables('uniqueResourceNameBase'))]"
      ]
    }
  ]
}

Hope you like this update

Happy ARM template!

References