Merge Azure App Service App Settings in ARM Template

Well you can easy provide Azure App Service App setting in ARM template. Right? But what if you would like to merge settings included in script with those provided outside. This is another story.

Look at the script below. The sample above shows a ARM template to deploy Azure Function App.

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "functionStorageName": {
      "type": "string",
      "metadata": {
        "description": "Name of the storage."
      }
    },
    "functionName": {
      "type": "string",
      "metadata": {
      "description": "Name of the function."
      }
    },
    "functionAppServicePlanName":{
      "type": "string",
      "metadata": {
      "description": "Name of the app service plan."
      }
    },
    "functionPackageUri": {
      "type": "string",
      "metadata": {
          "description": "Uri to function package."
      }
    }
  },
  "variables":
  {
    "functionStorageAccountId": "[concat(resourceGroup().id,'/providers/','Microsoft.Storage/storageAccounts/', parameters('functionStorageName'))]"
  },
  "resources": [
    {
      "type": "Microsoft.Storage/storageAccounts",
      "name": "[parameters('functionStorageName')]",
      "apiVersion": "2019-04-01",
      "location": "[resourceGroup().location]",
      "kind": "Storage",
      "sku": {
        "name": "Standard_LRS"
      },
      "properties": {
        "supportsHttpsTrafficOnly": true
      }
    },
    {
      "type": "Microsoft.Web/serverfarms",
      "apiVersion": "2019-08-01",
      "name": "[parameters('functionAppServicePlanName')]",
      "location": "[resourceGroup().location]",
      "sku": {
        "name": "Y1",
        "tier": "Dynamic"
      },
      "properties": {
        "name": "[parameters('functionAppServicePlanName')]",
        "computeMode": "Dynamic"
      }
    },
    {
      "apiVersion": "2019-08-01",
      "type": "Microsoft.Web/sites",
      "name": "[parameters('functionName')]",
      "location": "[resourceGroup().location]",
      "identity": {
        "type": "SystemAssigned"
      },
      "kind": "functionapp",
      "dependsOn": [
        "[resourceId('Microsoft.Web/serverfarms', parameters('functionAppServicePlanName'))]",
        "[resourceId('Microsoft.Storage/storageAccounts', parameters('functionStorageName'))]"
      ],
      "properties": {
        "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('functionAppServicePlanName'))]",
        "siteConfig": {
          "appSettings": [
            {
              "name": "AzureWebJobsDashboard",
              "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', parameters('functionStorageName'), ';AccountKey=', listKeys(variables('functionStorageAccountId'),'2015-05-01-preview').key1)]"
            },
            {
              "name": "AzureWebJobsStorage",
              "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', parameters('functionStorageName'), ';AccountKey=', listKeys(variables('functionStorageAccountId'),'2015-05-01-preview').key1)]"
            },
            {
              "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
              "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', parameters('functionStorageName'), ';AccountKey=', listKeys(variables('functionStorageAccountId'),'2015-05-01-preview').key1)]"
            },
            {
              "name": "WEBSITE_CONTENTSHARE",
              "value": "[toLower(parameters('functionName'))]"
            },
            {
              "name": "FUNCTIONS_EXTENSION_VERSION",
              "value": "~2"
            },
            {
              "name": "WEBSITE_NODE_DEFAULT_VERSION",
              "value": "8.11.1"
            },

            {
              "name": "FUNCTIONS_WORKER_RUNTIME",
              "value": "dotnet"
            }
          ]
        }
      },
      "resources": [
        {
          "name": "MSDeploy",
          "type": "extensions",
          "location": "[resourceGroup().location]",
          "apiVersion": "2015-08-01",
          "dependsOn": [
            "[resourceId('Microsoft.Web/sites', parameters('functionName'))]"
          ],
          "properties": {
            "packageUri": "[concat(parameters('functionPackageUri'))]"
          }
        }
      ]
    }
  ]
}

If you look in the code you’ll see there are some static configuration values like FUNCTIONS_WORKER_RUNTIME and other. There are also some dynamic configuration values which are only available during the deployment like the account storage key provided by listkeys function inside of AzureWebJobsDashboard variable. You can always add your own configuration values to those from outside of the script over custom parameters. No magic here. See customConfigurationValue parameter below which is linked CustomConfigurationKey in appSettings array.

"customConfigurationValue":
{
	"type": "string",
    "metadata": {
    "description": "Custom configuration value for function app settings."
}
...
"appSettings": 
[
  {
     "name": "CustomConfigurationKey",
     "value": "[parameters('customConfigurationValue')]"
  }
]

Well, things are going to be more tricky when you need provide some multiple parameters outside of the script. Sure you can always add new parameter value, but this isn’t very nice. You will always need to make a change in script, if so.

What if we could do something like this?

"functionConfiguration": 
{
    "value": [
               {
                 "name": "CustomConfigurationKey1",
                 "value": "customConfigurationValue1"
               },
               {
                  "name": "CustomConfigurationKey2",
                  "value": "customConfigurationValue2"
               }
             ]
}

You see, there isn’t configuration parameter for each value, instead I use array of values. And now things are going to be complicated and ugly. Not because of array, you can use array inside of appSettings naturally. Because now we need to merge dynamic values I described before with those provided over parameter functionConfiguration in the code above.

        "siteConfig": {
          "appSettings": "[union(parameters('functionConfiguration'),
          createArray(
            json(concat('{\"name\": \"AzureWebJobsDashboard\", \"value\": \"',concat('DefaultEndpointsProtocol=https;AccountName=', parameters('functionStorageName'), ';AccountKey=', listKeys(variables('functionStorageAccountId'),'2019-06-01').keys[0].value),'\"}')),
            json(concat('{\"name\": \"AzureWebJobsDashboard\", \"value\": \"',concat('DefaultEndpointsProtocol=https;AccountName=', parameters('functionStorageName'), ';AccountKey=', listKeys(variables('functionStorageAccountId'),'2019-06-01').keys[0].value),'\"}')),
            json(concat('{\"name\": \"AzureWebJobsStorage\", \"value\": \"',concat('DefaultEndpointsProtocol=https;AccountName=', parameters('functionStorageName'), ';AccountKey=', listKeys(variables('functionStorageAccountId'),'2019-06-01').keys[0].value),'\"}')),
            json(concat('{\"name\": \"WEBSITE_CONTENTAZUREFILECONNECTIONSTRING\", \"value\": \"',concat('DefaultEndpointsProtocol=https;AccountName=', parameters('functionStorageName'), ';AccountKey=', listKeys(variables('functionStorageAccountId'),'2019-06-01').keys[0].value),'\"}')),
            json(concat('{\"name\": \"WEBSITE_CONTENTSHARE\", \"value\": \"',parameters('functionStorageName'),'\"}'))))]"
        }

What I use ist the union function to merge arrays together. The ugly part: well you can see it, I need to create for each value json and literally stick it together with concat function. Maybe there ist more beautiful way I didn’t find anything else. It wouldn’t be that ugly if ARM templates would support expressions inside of strings which isn’t that case now.

Hope it helps some of you. See completed script below.

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "functionStorageName": {
      "type": "string",
      "metadata": {
        "description": "Name of the storage."
      }
    },
    "functionName": {
      "type": "string",
      "metadata": {
      "description": "Name of the function."
      }
    },
    "functionAppServicePlanName":{
      "type": "string",
      "metadata": {
      "description": "Name of the app service plan."
      }
    },
    "functionPackageUri": {
      "type": "string",
      "metadata": {
          "description": "Uri to function package."
      }
    },
    "functionConfiguration":{
      "type": "array",
      "metadata": {
      "description": "Configuration entries for function."
      }
    }    
  },
  "variables":
  {
    "functionStorageAccountId": "[concat(resourceGroup().id,'/providers/','Microsoft.Storage/storageAccounts/', parameters('functionStorageName'))]"
  },
  "resources": [
    {
      "type": "Microsoft.Storage/storageAccounts",
      "name": "[parameters('functionStorageName')]",
      "apiVersion": "2019-04-01",
      "location": "[resourceGroup().location]",
      "kind": "Storage",
      "sku": {
        "name": "Standard_LRS"
      },
      "properties": {
        "supportsHttpsTrafficOnly": true
      }
    },
    {
      "type": "Microsoft.Web/serverfarms",
      "apiVersion": "2019-08-01",
      "name": "[parameters('functionAppServicePlanName')]",
      "location": "[resourceGroup().location]",
      "sku": {
        "name": "Y1",
        "tier": "Dynamic"
      },
      "properties": {
        "name": "[parameters('functionAppServicePlanName')]",
        "computeMode": "Dynamic"
      }
    },
    {
      "apiVersion": "2019-08-01",
      "type": "Microsoft.Web/sites",
      "name": "[parameters('functionName')]",
      "location": "[resourceGroup().location]",
      "identity": {
        "type": "SystemAssigned"
      },
      "kind": "functionapp",
      "dependsOn": [
        "[resourceId('Microsoft.Web/serverfarms', parameters('functionAppServicePlanName'))]",
        "[resourceId('Microsoft.Storage/storageAccounts', parameters('functionStorageName'))]"
      ],
      "properties": {
        "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('functionAppServicePlanName'))]",
        "siteConfig": {
          "appSettings": "[union(parameters('functionConfiguration'),
          createArray(
            json(concat('{\"name\": \"AzureWebJobsDashboard\", \"value\": \"',concat('DefaultEndpointsProtocol=https;AccountName=', parameters('functionStorageName'), ';AccountKey=', listKeys(variables('functionStorageAccountId'),'2019-06-01').keys[0].value),'\"}')),
            json(concat('{\"name\": \"AzureWebJobsDashboard\", \"value\": \"',concat('DefaultEndpointsProtocol=https;AccountName=', parameters('functionStorageName'), ';AccountKey=', listKeys(variables('functionStorageAccountId'),'2019-06-01').keys[0].value),'\"}')),
            json(concat('{\"name\": \"AzureWebJobsStorage\", \"value\": \"',concat('DefaultEndpointsProtocol=https;AccountName=', parameters('functionStorageName'), ';AccountKey=', listKeys(variables('functionStorageAccountId'),'2019-06-01').keys[0].value),'\"}')),
            json(concat('{\"name\": \"WEBSITE_CONTENTAZUREFILECONNECTIONSTRING\", \"value\": \"',concat('DefaultEndpointsProtocol=https;AccountName=', parameters('functionStorageName'), ';AccountKey=', listKeys(variables('functionStorageAccountId'),'2019-06-01').keys[0].value),'\"}')),
            json(concat('{\"name\": \"WEBSITE_CONTENTSHARE\", \"value\": \"',parameters('functionStorageName'),'\"}'))))]"
        }
      },
      "resources": [
        {
          "name": "MSDeploy",
          "type": "extensions",
          "location": "[resourceGroup().location]",
          "apiVersion": "2015-08-01",
          "dependsOn": [
            "[resourceId('Microsoft.Web/sites', parameters('functionName'))]"
          ],
          "properties": {
            "packageUri": "[concat(parameters('functionPackageUri'))]"
          }
        }
      ]
    }
  ]
}

Oh one thing. You are not allowed to use new line inside of arm template, so the whole union thing need to be at one line.

About the Author Anton Kalcik

Most of the time, I assist people in the creation of valuable software. I’m a software engineer and entrepreneur specializing in .NET and Microsoft Azure. I offer Code Katas, Coding Dojos, workshops and talks about .NET, Microsoft Azure, DevOps, Agile Methodologies and Clean Code. I'm founder of CoderDojo Wien and president of digital.austria association.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.