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.