ARM Templates: list*() & reference() functions in variables workaround

Use ARM templates enough and eventually you'll wish to use one of the list*() functions or reference() in your variables.

For example, you have multiple app services which require near identical appsettings. You'd like to define this object/array once, then reuse multiple times elsewhere in the template. Who likes repeating themselves? Unfortunately one of those settings includes e.g. a storage account access key or a reference() to grab an application insights key...

Unfortunately, it's well documented that this is not supported:

The template function 'listKeys' is not expected at this location

What isn't so well documented, are possible workarounds, other than "don't do that". So here's mine:

Nested Templates

A possibly naive way to use these is through treating their output as your variables. See e.g. the "Get values from linked template" example. Note that you can create output arrays or objects, not just strings. e.g.

example-1-output.json:
    
{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "resourceprefix": {
            "type": "string",
            "defaultValue": "joetesting"
        }
    },
    "variables": {},
    "resources": [
        {
            "type": "Microsoft.Resources/deployments",
            "apiVersion": "2020-10-01",
            "name": "template_appsettings",
            "properties": {
                "mode": "Incremental",
                "template": {
                    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
                    "contentVersion": "1.0.0.0",
                    "resources": [],
                    "outputs": {
                        "appsettings": {
                            "type": "object",
                            "value": {
                                "storagekey": "[listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('resourceprefix')), '2019-04-01').keys[0].value]",
                                "name2": "value",
                                "name3": "value"
                            }
                        }
                    }
                }
            }
        },
        {
            "name": "[concat(parameters('resourceprefix'), '-web0/appsettings')]",
            "type": "Microsoft.Web/sites/config",
            "apiVersion": "2018-11-01",
            "properties": "[reference('template_appsettings').outputs.appsettings.value]",
            "dependsOn": ["template_appsettings"]
        },
        {
            "name": "[concat(parameters('resourceprefix'), '-web1/appsettings')]",
            "type": "Microsoft.Web/sites/config",
            "apiVersion": "2018-11-01",
            "properties": "[union(reference('template_appsettings').outputs.appsettings.value, createObject('extra', 'value'))]",
            "dependsOn": ["template_appsettings"]
        }
    ]
}
    

1

This has a few benefits - notably expressionEvaluationOptions defaults to outer and so no parameters or variables in the template need redefining/passing through to the inner template.

There's one big downside however: If you take a look at the deployments in the Azure Portal you'll realise that deployment outputs are very much not secret. A problem if you're popping keys in there:

Attempting to use "type": "securestring" or "secureobject" at this point will lead you to the realisation that even though these are valid types for outputs, it simply causes them not to be output. They're not even referencable by a nested template during the same deployment:

'The language expression property 'value' doesn't exist, available properties are 'type'.'"

A safer way of achieving this is instead to pop the resources into the nested template, e.g.

example-2-nested.json:
    
{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "resourceprefix": {
            "type": "string",
            "defaultValue": "joetesting"
        }
    },
    "variables": {},
    "resources": [
        {
            "type": "Microsoft.Resources/deployments",
            "apiVersion": "2020-10-01",
            "name": "template_appsettings",
            "properties": {
                "mode": "Incremental",
                "expressionEvaluationOptions": {
                    "scope": "inner"
                },
                "parameters": {
                    "defaultAppSettings": {
                        "value": {
                            "storagekey": "[listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('resourceprefix')), '2019-04-01').keys[0].value]",
                            "name2": "value",
                            "name3": "value"
                        }
                    },
                    "resourceprefix": {
                        "value": "[parameters('resourceprefix')]"
                    }
                },
                "template": {
                    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
                    "contentVersion": "1.0.0.0",
                    "parameters": {
                        "defaultAppSettings": {
                            "type": "secureObject"
                        },
                        "resourceprefix": {
                            "type": "string"
                        }
                    },
                    "resources": [
                        {
                            "name": "[concat(parameters('resourceprefix'), '-web0/appsettings')]",
                            "type": "Microsoft.Web/sites/config",
                            "apiVersion": "2018-11-01",
                            "properties": "[parameters('defaultAppSettings')]"
                        },
                        {
                            "name": "[concat(parameters('resourceprefix'), '-web1/appsettings')]",
                            "type": "Microsoft.Web/sites/config",
                            "apiVersion": "2018-11-01",
                            "properties": "[union(parameters('defaultAppSettings'), createObject('extra', 'value'))]"
                        }
                    ]
                }
            }
        }
    ]
}
    

1

In short, the nested template defines it's own parameters that it expects i.e. at properties.parameters.template.parameters and the same rules apply here as to the outer template.

However, if you set "expressionEvaluationOptions": { "scope": "inner" } you must now define the values of the parameters for the nested template. (Just like how you'd need to provide values or a parameter file on the command line) This is at properties.parameters and here you can use any of the ARM functions you'd like because these are evaluated in the context of creating the Microsoft.Resources/deployments resource.


Footnotes:

1

Check out base-infra.json for the supporting template if you'd like to run these yourself. It contains the storage account, webapps & app service plan that aren't interesting in the templates above.