When working with ARM Templates, chances are you have set a value that was a storage account connection string. For example, as the value of an appsetting, or as a secret in Key Vault, which I did as an example in a previous article.
However, this does not result in the most maintainable and readable piece of JSON. Even worse if you have multiple connection strings in the same template.
In this article I will show that adding a user-defined function to our template can improve on this.
The example from the earlier article.
{ "type": "Microsoft.KeyVault/vaults/secrets", "name": "LoremVault/ConnectionString", "apiVersion": "2015-06-01", "properties": { "contentType": "text/plain", "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', 'loremipsumstore', ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', 'loremipsumstore'), '2019-06-01').keys[0].value, ';EndpointSuffix=core.windows.net')]" }, "dependsOn": [ "[resourceId('Microsoft.KeyVault/vaults', 'LoremVault')]", "[resourceId('Microsoft.Storage/storageAccounts', 'loremipsumstore')]" ] }
User-Defined Functions
ARM templates support user-defined functions, but I do not see them very often, to be honest, I never encountered one in the wild.
Although there are some limitations, none of them will prevent us from improving our template.
First, we will add a functions
array to the root of the template and add a function outline. We must specify a namespace
, the name
of the function and the output
type
and value
.
"functions": [ { "namespace": "storage", "members": { "connectionString": { "output": { "type": "string", "value": "" } } } } ],
To create a storage account connection string, we need the account name and one of the keys. As user-defined functions cannot use the reference
function or any of the list*
functions, we will need to supply these parameters to the function.
So, we add accountName
and accountKey
as parameters
.
"connectionString": { "parameters": [ { "name": "accountName", "type": "string" }, { "name": "accountKey", "type": "string" } ], "output": { "type": "string", "value": "" } }
Now we concatenate the supplied parameter values with the static parts to form a complete connection string as the output
value
.
"output": { "type": "string", "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', parameters('accountName'), ';AccountKey=', parameters('accountKey'), ';EndpointSuffix=core.windows.net')]" }
Calling the function
With this done, we can replace the original value in our template with the return value of the new method we call.
{ "type": "Microsoft.KeyVault/vaults/secrets", "name": "LoremVault/ConnectionString", "apiVersion": "2015-06-01", "properties": { "contentType": "text/plain", "value": "[storage.connectionString('loremipsumstore', listKeys(resourceId('Microsoft.Storage/storageAccounts', 'loremipsumstore'), '2019-06-01').keys[0].value)]" }, "dependsOn": [ "[resourceId('Microsoft.KeyVault/vaults', 'LoremVault')]", "[resourceId('Microsoft.Storage/storageAccounts', 'loremipsumstore')]" ] }
Although user-defined functions have quite some limitations in ARM templates, this makes for a much cleaner and more readable template.
The full example is available as a GitHub Gist.