2

I am trying to take an array of parameters - specifically a list of email recipients - and write these to the app settings of my web app.

The following template works but - obviously - only writes the first and second item from the array.

I've studied the copy function but this only appears to handle the creation of an object but I need to add to an existing list of key value pairs.

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  ... 
  "parameters": {
    "Email:Recipients": {
    "type": "array"
  },
  ...
  "resources": [
    {
      "apiVersion": "2015-08-01",
      "type": "Microsoft.Web/sites",
      ...
      "resources": [
        {
          "apiVersion": "2015-08-01",
          "name": "appsettings",
          "type": "config",
          "properties": {
            ...
            "Email:Recipients:0": "[parameters('Email:Recipients')[0]]",
            "Email:Recipients:1": "[parameters('Email:Recipients')[1]]",
            ...
          }
        }
  ]
}
Frodo
  • 43
  • 5

2 Answers2

3

I had a very similar use case to you: I wanted the user to specify the number of storage accounts to use for sharding, and for the template to create all the storage accounts and then for each one add a connection string as an app setting for a web app alongside all my other app settings, in the form:

<other app settings>
...
"STORAGE_CONNECTION_STRING_00": "<connectionString00>",
"STORAGE_CONNECTION_STRING_01": "<connectionString01>",
...

I realise I'm probably too late to help you, but hopefully this will help someone else.

The key to the solution is that instead of specifying your app settings as a child resource of the Microsoft.Web/sites resource, to specify them inline within the properties section. Crucially, this allows you to specify them as an array of objects, each with name and value properties, instead of as one large object as in your question:

{
  "apiVersion": "2015-08-01",
  "type": "Microsoft.Web/sites",
  ...
  "properties": {
    ...
    "siteConfig": {
      "appSettings": [
        { 
          name: "STORAGE_CONNECTION_STRING_00",
          value: "<connectionString00>"
        },
        ...
      ]
  },
  ...
}

For my first attempt I tried using copy to add all my connection strings to this list:

{
  "apiVersion": "2015-08-01",
  "type": "Microsoft.Web/sites",
  ...
  "properties": {
    "siteConfig": {
      "copy": [
        {
          "name": "appSettings",
          "count": "[parameters('resultsShardCount')]",
          "input": {
            "name": "[concat('STORAGE_CONNECTION_STRING_', padLeft(copyIndex('appSettings'), 2, '0'))]",
            "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageDataAccountNames')[copyIndex('appSettings')],';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageDataAccountNames')[copyIndex('appSettings')]), providers('Microsoft.Storage', 'storageAccounts').apiVersions[0]).keys[0].value)]"
          }
        }
      ]
    }
  },
  ...
}

This worked, but it didn't allow me to add any of my other app settings alongside the storage connection strings.

I tried adding adding the other app settings using a separate appsettings child resource, as in the original question, in the hope they would be merged, but this simply overwrote the connection strings.

Next I tried defining two variables, one an array of my connection string objects using copy, and the other a static array of all the other app settings objects. I figured I could then use the union function to combine them:

  "apiVersion": "2015-08-01",
  "type": "Microsoft.Web/sites",
  ...
  "properties": {
    "siteConfig": {
      "appSettings": "[union(variables('additionalAppSettings), variables('storageAppSettings'))]"
    }
  },
  ...
}

Unfortunately template variables are eagerly evaluated, meaning you can't reference any resource properties. This was a problem both for evaluating the connection strings and for some of my other app settings which contained references to other resources I was deploying in the template.

Researching that problem lead to looking at nested templates, and in particular this Stack Overflow answer for building up a list of dynamically evaluated objects in an array using copy on the nested templates.

This method was looking quite promising, until I hit upon another limitation of ARM templates:

For nested templates, you cannot use parameters or variables that are defined within the nested template.

The solution would be to use linked templates instead, but that is a massive overkill for what should be a trivial problem.

After some head scratching I eventually figured out a way to adapt it to use only output parameters, allowing it to work using nested templates:

...
{
  "name": "reference0",
  "type": "Microsoft.Resources/deployments",
  "apiVersion": "2015-01-01",
  "dependsOn": [
    "storageDataAccountCopy"
  ],
  "properties": {
    "mode": "Incremental",
    "template": {
      "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
      "contentVersion": "1.0.0.0",
      "resources": [],
      "outputs": {
        "storageAppSettings": {
          "type": "array",
          "value": []
        },
        "storageAppSetting": {
          "type": "object",
          "condition": "[greater(parameters('resultsShardCount'), 0)]",
          "value": {
            "name": "[concat('STORAGE_CONNECTION_STRING_', padLeft(0, 2, '0'))]",
            "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageDataAccountNames')[0],';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageDataAccountNames')[0]), providers('Microsoft.Storage', 'storageAccounts').apiVersions[0]).keys[0].value)]"
          }
        },
        "additionalAppSettings": {
          "type": "array",
          "value": [
            {
              "name": "APPLICATION_INSIGHTS_KEY",
              "value": "[reference(concat('Microsoft.Insights/components/', variables('applicationInsightsName'))).InstrumentationKey]"
            }
            ...
          ]
        } 
      }
    }
  }
},
{
  "name": "[concat('reference', copyIndex(1))]",
  "type": "Microsoft.Resources/deployments",
  "apiVersion": "2015-01-01",
  "copy": {
    "name": "storageAppSettings",
    "count": "[parameters('resultsShardCount')]"
  },
  "properties": {
    "mode": "Incremental",
    "template": {
      "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
      "contentVersion": "1.0.0.0",
      "resources": [],
      "outputs": {
        "storageAppSettings": {
          "type": "array",
          "value": "[concat(reference(concat('reference', copyIndex())).outputs.storageAppSettings.value, array(reference(concat('reference', copyIndex())).outputs.storageAppSetting.value))]"
        },
        "storageAppSetting": {
          "type": "object",
          "condition": "[less(copyIndex(1), parameters('resultsShardCount'))]",
          "value": {
            "name": "[concat('STORAGE_CONNECTION_STRING_', padLeft(copyIndex(1), 2, '0'))]",
            "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageDataAccountNames')[min(variables('maximumShardIndex'), copyIndex(1))],';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageDataAccountNames')[min(variables('maximumShardIndex'), copyIndex(1))]), providers('Microsoft.Storage', 'storageAccounts').apiVersions[0]).keys[0].value)]"
          }
        }
      }
    }
  }
},
...

To explain how this works:

The reference0 deployment is used to:

  • Output the initial storageAppSettings array as an empty array.
  • Output the first connection string app settings object.
  • Output the array of all the other app settings objects required by the web app.

The referenceN deployments are then looped over using copy, one for each shard I want to deploy (or in your case one for each email recipient). Each one does the following:

  • Outputs the new storageAppSettings array as a concatenation of the storageAppSettings array and the storageAppSetting object generated in the previous iteration referenceN-1.
  • Outputs the Nth storageAppSetting object.

Note that on the final referenceN we don't need to output storageAppSetting because the first is created in reference0, so for neatness we have a condition to stop that. Unfortunately, even with the condition present the value is still evaluated, causing an index out of bounds error unless you protect against it using something like min(variables('maximumShardIndex'), copyIndex(1)), where the variable maximumShardIndex is defined as [sub(parameters('resultsShardCount'), 1)]. Another workaround, but with that in place it works fine.

The storageAppSettings array output from the final reference is therefore our complete array of connection string app setting objects, and the additionalAppSettings array output from reference0 is all the other app settings objects we want alongside the connection strings.

So finally you can create your appSettings array in your web app as a union of those two arrays:

{
  "apiVersion": "2015-08-01",
  "type": "Microsoft.Web/sites",
  ...
  "properties": {
    "siteConfig": {
      "appSettings": "[union(reference('reference0').outputs.additionalAppSettings.value, reference(concat('reference', parameters('resultsShardCount'))).outputs.storageAppSettings.value)]"
    }
  },
  ...
}

I've tested this and it has successfully deployed a web app which shards data against N storage accounts, as specified by the resultsShardCount template parameter.

I think the solution for you will look broadly the same, except instead of building up an array of connection string name/value objects you'll build up a similar array from the recipients list passed in to your template.

James Thurley
  • 2,650
  • 26
  • 38
0

You can use the copy function within the properties object - see: https://learn.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-multiple#property-iteration

Think of this as a copy/paste type of operation before the template is deployed.

bmoore-msft
  • 8,376
  • 20
  • 22
  • I cannot see how the Copy function could be used. It takes three arguments. The first is a name. The name becomes the name of the "pasted" property. But I don't want a new property. I want to add to the existing "properties" property. The third argument is a class. But I don't want a class. I want a key value pair. – Frodo Dec 22 '17 at 15:43
  • you're right I don't think that will work here - you're trying to convert from an array to a misc blob of JSON. I can't think of an easy way to do that (not sure it's even possible with nested deployments because the property name is not deterministic) in the template language. One option would be to pass in the entire properties object as a parameter and do the "math" outside of the template. – bmoore-msft Jan 04 '18 at 19:42