Search code examples
azure-devopsazure-resource-managerazure-rm-template

listKeys problems in ARM template using inside variables or parameters


I'm setting up an ARM template for my Azure Functions. I have functions_app.json which will function as the "main" template and will be used for other release pipelines as well.

Then there will be project specific parameter templates, like:

  • counter_function_arm.parameters.test.json
  • counter_function_arm.parameters.acceptance.json
  • weather_function_arm.parameters.test.json

etc.

But I'm stuck at this part where it creates the actual Function resource inside the functions_app.json template:

"resources": [
{
    "apiVersion": "2018-11-01",
    "name": "[variables('resourceName')]",
    "type": "Microsoft.Web/sites",
    "properties": {
        "name": "[variables('resourceName')]",
        "siteConfig": {
            "appSettings": [
                {
                    "name": "FUNCTIONS_EXTENSION_VERSION",
                    "value": "~4"
                },
                {
                    "name": "FUNCTIONS_WORKER_RUNTIME",
                    "value": "dotnet"
                },
                {
                    "name": "AzureWebJobsStorage",
                    "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listKeys(resourceId('xxxx-xxxx','MyResourceGroup','Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value,';EndpointSuffix=','core.windows.net')]"
                },
                {
                    "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
                    "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listKeys(resourceId('xxxx-xxxx','MyResourceGroup','Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value,';EndpointSuffix=','core.windows.net')]"
                },
                {
                    "name": "WEBSITE_CONTENTSHARE",
                    "value": "[toLower(variables('resourceName'))]"
                }
            ]
        }
    }
}

I left some things out for brevity. The import part is the appSettings section.

My initial idea was to put that array in variables. Those will be the "default" settings. Then from my counter_function_arm.parameters.test.json file I would add another array specific to that application and then union those two together so all AppSettings are configured.

Something like this:

counter_function_arm.parameters.test.json

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "customAppSettings":{
            "value": [
                {
                    "name": "CustomProperty",
                    "value": "some value"
                }
            ]
        }
    }
}

functions_app.json

"variables": {
    "defaultAppSettings": [
                {
                    "name": "FUNCTIONS_EXTENSION_VERSION",
                    "value": "~4"
                },
                {
                    "name": "FUNCTIONS_WORKER_RUNTIME",
                    "value": "dotnet"
                },
                {
                    "name": "AzureWebJobsStorage",
                    "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listKeys(resourceId('xxxx-xxxx','MyResourceGroup','Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value,';EndpointSuffix=','core.windows.net')]"
                },
                {
                    "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
                    "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listKeys(resourceId('xxxx-xxxx','MyResourceGroup','Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value,';EndpointSuffix=','core.windows.net')]"
                },
                {
                    "name": "WEBSITE_CONTENTSHARE",
                    "value": "[toLower(variables('resourceName'))]"
                }
            ]
},
"resources": [
{
    "apiVersion": "2018-11-01",
    "name": "[variables('resourceName')]",
    "type": "Microsoft.Web/sites",
    "properties": {
        "name": "[variables('resourceName')]",
        "siteConfig": {
            "appSettings": "[union(variables('defaultAppSettings'), parameters('customAppSettings'))]"
        }
    }
}

So in this case I union the variable variables('defaultAppSettings') and parameters('customAppSettings') together.

But, the problem is that I can't use listKeys in the variables section. I also can't move this to the parameters section and set it as defaultValue because listKeys is also not allowed there.

Adding a hard coded array to union also doesn't work:

"appSettings": "[union([1,2,3], parameters('customAppSettings'))]"

So I am out of ideas.

Is there a way to do this?


Solution

  • I've found a way around this, it is not pretty but it works... Basically I have an array of purely static app settings (in your case FUNCTIONS_EXTENSION_VERSION, FUNCTIONS_WORKER_RUNTIME, WEBSITE_CONTENTSHARE) that you can create in variables, an array of customAppSettings from parameters, and another array that I have to write as raw JSON. Here's an example :

    "resources": [
        {
            "apiVersion": "2018-11-01",
            "name": "[variables('resourceName')]",
            "type": "Microsoft.Web/sites",
            "properties": {
                "name": "[variables('resourceName')]",
                "siteConfig": {
                    "appSettings": "[union(variables('staticAppSettings'), parameters('customAppSettings'), json(concat('[{\"name\": \"AzureWebJobsStorage\",\"value\": \"', concat('DefaultEndpointsProtocol=https;AccountName=', parameters('azureFunctionParameters').functionStorageName,';AccountKey=', listKeys(resourceId(parameters('azureFunctionParameters').mainStorageResourceGroup, 'Microsoft.Storage/storageAccounts', parameters('azureFunctionParameters').functionStorageName), '2021-08-01').keys[1].value, ';EndpointSuffix=core.windows.net'),'\"}]')))]"
                }
            }
        }
    ]
    

    Sadly this is the best I could come up with, it is very prone to syntax error but if you're using multi-line strings you could format it in a way to avoid errors (but be careful if you're using Azure Devops it is not compatible).

    EDIT : There is another way to accomplish this that may be more suitable for you, which is to use a linkedTemplate :

    Main template

    "resources": [
        {
            "type": "Microsoft.Resources/deployments",
            "apiVersion": "2021-04-01",
            "name": "yourResourceDeployment",
            "properties": {
                "mode": "Incremental",
                "templateLink": {
                    "uri": "[concat(parameters('blobBaseUrl'), '/', parameters('resourceGroupName'), '/azurefunctions/baseazurefunction.json')]",
                    "contentVersion": "1.0.0.0"
                },
                "parameters": {
                    "azureFunctionParameters": {
                        "value": "[parameters('baseAzureFunctionParameters')]"
                    },
                    "appSettings": {
                        "value" : "[union(variables('staticAppSettings'), parameters('customAppSettings'))]"
                    },
                    "someMoreAppSettings": {
                        "value" : [
                            {
                                "name": "AzureWebJobsStorage",
                                "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listKeys(resourceId('xxxx-xxxx','MyResourceGroup','Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value,';EndpointSuffix=','core.windows.net')]"
                            },
                            {
                                "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
                                "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listKeys(resourceId('xxxx-xxxx','MyResourceGroup','Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value,';EndpointSuffix=','core.windows.net')]"
                            }
                        ]
                    }
                }
            }
        }
    ]
    

    In the baseazurefunction.json linkedTemplate you can just use

    {
        "apiVersion": "2018-11-01",
        "name": "[variables('resourceName')]",
        "type": "Microsoft.Web/sites",
        "properties": {
            "name": "[variables('resourceName')]",
            "siteConfig": {
                "appSettings": "[union(parameters('appSettings'), parameters('moreAppSettings'))]"
            }
        }
    }
    

    If you have a security concern over sending connection strings as parameters to other templates, you could wrap your appsettings in a secureobject with a property called array which would contain all your settings and then use it like this instead

        "appSettings": {
            "type": "secureobject",
            "metadata": {
                "description": "Secure object with an array property which contains the appsettings"
            }
        },
        "moreAppSettings": {
            "type": "secureobject",
            "metadata": {
                "description": "Secure object with an array property which contains more appsettings"
            }
        }
    
        .
        .
        .
    
        "appSettings": "[union(parameters('appSettings').array, parameters('moreAppSettings').array)]"