Azure Keyvault add Function MSI via ARM - azure

I think Managed Service Identity is a great concept and I love keyvault. However:
When I use the script using an incremental resource group deployment:
Sample is modified for brevity
{
"type": "Microsoft.KeyVault/vaults",
"name": "[parameters('keyvaultName')]",
"apiVersion": "2015-06-01",
"properties": {
"accessPolicies": [
{
"objectId": "[reference(parameters('functionAppName'), '2016-08-01', 'Full').identity.principalId]",
"permissions": {
"keys": [],
"secrets": [
"Get"
]
}
}
]
},
"dependsOn": [
"[resourceId('Microsoft.Web/sites', parameters('functionAppName'))]"
]
},
{
"apiVersion": "2016-08-01",
"type": "Microsoft.Web/sites",
"name": "[parameters('functionAppName')]",
"kind": "functionapp",
"identity": {
"type": "SystemAssigned"
},
}
It deploys successfully and adds the MSI to keyvault, but --
It blows away the already assigned access policies. Is it possible for arm to preserve accessPolicies and only add/update policies that match?
Without this it's impossible to fully script a deployment with a MSI and also assign the principal to keyvault..
Am I missing something?

As the author of the blog post, I'll post the details per the mods:
When you deploy a resource of type Microsoft.KeyVault/vaults/accessPolicies with the name “add”, it will merge in your changes. This special child resource type was created to allow Managed Service Identity scenarios where you don’t know the identity of a VM until the VM is deployed and you want to give that identity access to the vault during deployment.
An incremental deployment can be used along with this json to achieve the objective:
{
"$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"vaultName": {
"type": "string"
}
},
"resources": [
{
"type": "Microsoft.KeyVault/vaults/accessPolicies",
"name": "[concat(parameters('vaultName'), '/add')]",
"apiVersion": "2016-10-01",
"properties": {
"accessPolicies": [
{
"tenantId": "dfe47ca8-acfc-4539-9519-7d195a9e79e4",
"objectId": "5abe9358-10ae-4195-ba23-d34111430329",
"permissions": {
"keys": ["all"],
"secrets": ["all"],
"certificates": ["all"],
"storage": ["all"]
}
}
]
}
}
],
"outputs": {
}
}

The issue with the highest-voted answer is that it removes the key vault from the ARM template altogether, meaning that the key vault's creation becomes a manual process on new environments.
ARM does not allow a key vault to be redeployed without clearing its existing access policies. The accessPolicies property is required (except when recovering a deleted vault), so omitting it will cause an error. Setting it to [] will clear all existing policies. There has been a Microsoft Feedback request to fix this since 2018, currently with 152 votes.
The best way I've found of working around this is to make the key vault deployed conditionally only if it does not already exist, and define the access policies through a separate add child resource. This causes the specified policies to get added or updated, whilst preserving any other existing policies. I check whether the key vault already exists by passing in the list of existing resource names to the ARM template.
In the Azure pipeline:
- task: AzurePowerShell#5
displayName: 'Get existing resource names'
inputs:
azureSubscription: '$(armServiceConnection)'
azurePowerShellVersion: 'LatestVersion'
ScriptType: 'InlineScript'
Inline: |
$resourceNames = (Get-AzResource -ResourceGroupName $(resourceGroupName)).Name | ConvertTo-Json -Compress
Write-Output "##vso[task.setvariable variable=existingResourceNames]$resourceNames"
azurePowerShellVersion: 'LatestVersion'
- task: AzureResourceManagerTemplateDeployment#3
name: DeployResourcesTemplate
displayName: 'Deploy resources through ARM template
inputs:
deploymentScope: 'Resource Group'
action: 'Create Or Update Resource Group'
# ...
overrideParameters: >-
-existingResourceNames $(existingResourceNames)
# ...
deploymentMode: 'Incremental'
In the ARM template:
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"keyVaultName": {
"type": "string"
},
"existingResourceNames": {
"type": "array",
"defaultValue": []
}
},
"resources": [
{
"type": "Microsoft.KeyVault/vaults",
"apiVersion": "2016-10-01",
"name": "[parameters('keyVaultName')]",
"location": "[resourceGroup().location]",
// Only deploy the key vault if it does not already exist.
// Conditional deployment doesn't cascade to child resources, which can be deployed even when their parent isn't.
"condition": "[not(contains(parameters('existingResourceNames'), parameters('keyVaultName')))]",
"properties": {
"sku": {
"family": "A",
"name": "Standard"
},
"tenantId": "[subscription().tenantId]",
"enabledForDeployment": false,
"enabledForDiskEncryption": false,
"enabledForTemplateDeployment": true,
"enableSoftDelete": true,
"accessPolicies": []
},
"resources": [
{
"type": "accessPolicies",
"apiVersion": "2016-10-01",
"name": "add",
"location": "[resourceGroup().location]",
"dependsOn": [
"[parameters('keyVaultName')]"
],
"properties": {
"accessPolicies": [
// Specify your access policies here.
// List does not need to be exhaustive; other existing access policies are preserved.
]
}
}
]
}
]
}

Related

How to pass Managed Identity Object ID to KeyVault template in Azure Blueprints

So, I am trying to spin up a Managed Identity and a KeyVault using Blueprints. I have an individually working template for each. My endgoal is to spin up both resources along with a few more together in the same blueprint. The issue I'm having is that I need to pass my Managed Identity's object ID to the KeyVault template during deployment. I have the blueprint setup to deploy the managed identity resource group, then the managed identity, followed by the keyvault group and keyvault.
The deployment is working till the KV resource group and then failing during deployment of keyvault. Does anyone have any insight on how to do this?
Below are 2 pics that show the layout of the blueprint and also the section of the keyvault template where the object Id is needed.
Azure Blueprint Layout:
KeyVault Template where access policy to managed Identity is located:
Error Pic:
Pic of the error on the next page after I click view deployment details:
From your description and screenshot, you want to add the UserAssigned Managed Identity to your keyvault along with the creation of it, and the keyvault and Managed Identity are in different resource groups.
If so, the accessPolicies should be below, it works fine on my side.
"accessPolicies": [
{
"tenantId": "[subscription().tenantId]",
"objectId": "[reference(ResourceId(parameters('managedIdentityRG'), 'Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managedIdentityName')),'2018-11-30','Full').properties.principalId]",
"permissions": {
"keys": [],
"secrets": [
"Get"
],
"certificates": []
}
}
]
My complete template:
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"vaults_joykeyvault12_name": {
"type": "String"
},
"managedIdentityName": {
"type": "String"
},
"managedIdentityRG":{
"type": "String"
}
},
"variables": {},
"resources": [
{
"type": "Microsoft.KeyVault/vaults",
"apiVersion": "2016-10-01",
"name": "[parameters('vaults_joykeyvault12_name')]",
"location": "eastus",
"tags": {},
"properties": {
"sku": {
"family": "A",
"name": "Standard"
},
"tenantId": "[subscription().tenantId]",
"accessPolicies": [
{
"tenantId": "[subscription().tenantId]",
"objectId": "[reference(ResourceId(parameters('managedIdentityRG'), 'Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managedIdentityName')),'2018-11-30','Full').properties.principalId]",
"permissions": {
"keys": [],
"secrets": [
"Get"
],
"certificates": []
}
}
],
"enabledForDeployment": false,
"enabledForDiskEncryption": false,
"enabledForTemplateDeployment": false,
"enableSoftDelete": true
}
}
]
}

How to pass DevOps release secrets to ARM for Key Vault Deployment

I am trying to deploy a key vault with secretName and secretValue and I have created a variable group in azure devops with all the secrets and I am using the below parameters in parameter file, but when this gets deployed the secret value gets stored as $(secret) and not the password actually stored in the task group in Azure DevOps.
"secretsObject": {
"value": {
"secrets": [
{
"secretName": "App012",
"secretValue": "$(mysecret)"
},
and this is what I got in the key vault template:
{
"type": "Microsoft.KeyVault/vaults/secrets",
"name": "[concat(parameters('keyVaultName'), '/', parameters('secretsObject').secrets[copyIndex()].secretName)]",
"apiVersion": "2018-02-14",
"dependsOn": [
"[concat('Microsoft.KeyVault/vaults/', parameters('keyVaultName'))]"
],
"copy": {
"name": "secretsCopy",
"count": "[length(parameters('secretsObject').secrets)]"
},
"properties": {
"value": "[parameters('secretsObject').secrets[copyIndex()].secretValue]"
}
}
]
}
Any idea how to pass the "secretvalue" as a variable?
I believe your asking how to leverage your secrets that are stored as a variable group to be deployed securely with your ARM template via Azure DevOps. If that is the case look at using Override Template Parameters in your release task.
This would be in the format of -NameOfARMParameter $(NameofDevOpsVariable)
In your case it would be -mysecret $(NameOfDevOpsVariable)
The deployment .json should look like this for parameter declaration:
"secretValue": {
"type": "string",
"metadata": {
"description": "This is for receiving a value from DevOps releases of the secret to be stored in the key vault"
}
},
"secretName": {
"type": "string",
"metadata": {
"description": "Name of the Secret"
}
},
For the actual deployment
{
"type": "Microsoft.KeyVault/vaults/secrets",
"name": "[concat(variables('keyVaultName'),'/',parameters('secretName'))]",
"apiVersion": "2018-02-14",
"properties": {
"contentType": "text/plain",
"value": "[parameters('secretValue')]"
},
"dependsOn": [
"[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]"
]
},
And the parameters file doesn't need to have anything in it if these values will be fed from Dev Ops
You need to create a parameter file with the secret / link to Key Vault.
Here's a sample of it:
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"adminLogin": {
"value": "exampleadmin"
},
"adminPassword": {
"reference": {
"keyVault": {
"id": "/subscriptions/<subscription-id>/resourceGroups/<rg-name>/providers/Microsoft.KeyVault/vaults/<vault-name>"
},
"secretName": "ExamplePassword"
}
},
"sqlServerName": {
"value": "<your-server-name>"
}
}
}
More info:
https://learn.microsoft.com/en-us/azure/azure-resource-manager/templates/parameter-files
and https://learn.microsoft.com/en-us/azure/azure-resource-manager/templates/add-template-to-azure-pipelines

ARM Templates for Azure Functions with many appSettings for different environments and slots

I've got two Azure Function apps that use deployment slots, stage and production. These two Azure Function apps have about 50~ key:value pairs in Application Settings to define various API keys, application behavior, connection strings, etc.
I want to deploy these two Azure Function apps to five different environments (CI, DEV, QA, STG, PROD). I believe that deploying these resources to Azure using ARM templates is the better choice over Azure CLI. I will create tasks in my Azure DevOps release pipeline to achieve this.
In order to break down the ARM template into something easily maintainable, I wanted to create an ARM template parameter file for each environment. When defining the deployment file for the Azure Function, one of the properties to define is the siteConfig object, where you define the appSettings object with a NameValuePair object. For each environment, the stage and production slot will have different API keys, connection strings, and application behavior. My deployment file creates the Azure Function app with both the production slot and stage slot. In the deployment file, I have to provide the appSettings NameValuePair object twice. Then, I have to create 5 different parameter files for each environment. Multiply that by 2 because I have two slots.
Is it also true that all parameters defined in the parameter file have to be defined in the deployment template file in the parameters object?
Can I just pass in an array of objects with NameValuePairs from the parameter file so I don't have to have the entire list of parameters defined in the deployment file at the top and also under siteConfig.appSettings for the function app?
The documentation here shows that you can only provide an array of strings or a single object with many key:values. But appSettings is an array of objects where each object has 3 key:value pairs.
This is what the resource looks like in the deployment file. I would like to simply reference an entire array of objects from the parameter file, but it looks like the documentation states that I define all 50~ parameters at the top of the deployment file, which then the parameter file overrides when executed by Azure CLI or Azure DevOps task.
{
"type": "Microsoft.Web/sites",
"apiVersion": "2018-11-01",
"name": "[parameters('function-app-name')]",
"location": "[parameters('location')]",
"tags": {
"project": "[variables('projectName')]",
"env": "[parameters('deploymentEnvironment')]"
},
"kind": "functionapp",
"properties": {
"enabled": true,
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]",
"siteConfig": {
"appSettings": [] # I need to provide an array of objects here
}
}
}
In addition to my complaint...I can't believe I'm going to have to create 20 parameter files for all five environments and their two Azure Functions that have two slots. Is there a better way to deploy to all my environments and their deployment slots using ARM templates and parameter files with their unique application settings?
UPDATE:
I was able to piece together various methods for creating environment-specific ARM templates and came up with the following result, with some inconvenient problems. First, I'll explain where I am now and then bring up the problems associated with the design.
In my deployment template, I've defined two parameters. Here they are:
"deploymentEnvironment": {
"type": "string",
"allowedValues": [
"CI",
"DEV",
"QA",
"TRN",
"STG",
"PROD"
],
"metadata": {
"description": "Type of environment being deployed to. AKA \"Stage\" in release definition."
}
},
"applicationSettings": {
"type": "object",
"metadata": {
"description": "Application settings from function.parameters.json"
}
}
My function.parameters.json has a structure like this:
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"applicationSettings": {
"value": {
"CI": {
"appsetting1": "",
"appsetting2": ""
},
"DEV": {
"appsetting1": "",
"appsetting2": "" },
"QA": {
"appsetting1": "",
"appsetting2": ""
}
}
}
}
}
For each environment, I had placed all of my connection strings, apikeys, and application settings.
For the production slot for the function app, you can add a "resources" property which applies configuration to it. Here is the entire function app deployment:
{
"name": "[parameters('function-app-name')]",
"type": "Microsoft.Web/sites",
"apiVersion": "2018-11-01",
"kind": "functionapp",
"location": "[parameters('location')]",
"tags": {
"project": "[variables('projectName')]",
"env": "[parameters('deploymentEnvironment')]"
},
"properties": {
"enabled": true,
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms/', variables('appServicePlanName'))]"
},
"dependsOn": [
"[resourceId('Microsoft.Insights/components/', variables('applicationInsightsName'))]",
"[resourceId('Microsoft.Web/serverfarms/', variables('appServicePlanName'))]",
"[resourceId('Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]"
],
"resources": [
{
"name": "appsettings",
"type": "config",
"apiVersion": "2018-11-01",
"properties": "[parameters('applicationSettings')[parameters('deploymentEnvironment')]]",
"dependsOn": [
"[resourceId('Microsoft.Web/sites/', parameters('function-app-name'))]"
]
}
]
}
Next up was defining the stage slot deployment resource. Here it is:
{
"type": "Microsoft.Web/sites/slots",
"apiVersion": "2018-11-01",
"name": "[concat(parameters('function-app-name'), '/stage')]",
"location": "[parameters('location')]",
"dependsOn": [
"[resourceId('Microsoft.Web/sites/', parameters('function-app-name'))]"
],
"tags": {
"project": "[variables('projectName')]",
"env": "[parameters('deploymentEnvironment')]"
},
"kind": "functionapp",
"properties": {
"enabled": true,
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms/', variables('appServicePlanName'))]"
},
"resources": [
{
"name": "appsettings",
"type": "config",
"apiVersion": "2018-11-01",
"properties": "[parameters('applicationSettings')[parameters('deploymentEnvironment')]]",
"dependsOn": [
"[resourceId('Microsoft.Web/sites/', parameters('function-app-name'))]",
"[resourceId('Microsoft.Web/sites/slots/', parameters('function-app-name'), 'stage')]"
]
}
]
}
With this solution, I don't have to have a bunch of parameters.json files for each environment.
The problems...
Defining all of the application settings in parameters.json means I can't use template functions to get connection strings or Azure Key Vault values.
This is when I started to move some of the application settings to the deployment template to use template functions. So instead of having the APPINSIGHTS_INSTRUMENTATIONKEY and other AzureWebJobs* application settings in the parameters.json file, I provided the siteConfig object in the "properties" object for the Microsoft.Web/Sites resource and the Microsoft.Web/Sites/Slots resource.
This is the real bummer - When the deployment ran, it applied the siteConfig.appsettings values with the function app, then when it applied the parameters.json file, it deleted the application settings and applied only the ones from the json, instead of merging them together. That was a HUGE disappointment. In my initial testing with the AzureCLI, I used this command az functionapp config appsettings set --name $functionAppName --resource-group $resourceGroupName --settings $settingsFile --slot $slot to test what would happen with application settings that were not in a json file and was happy that it never deleted application settings. The powershell command gets and sets the values, merging it nicely and never deleting. But the ARM API deletes all those name value pairs and applies only what is defined. This means I cannot use template functions to create dynamic application settings and a json file to apply static application settings.
As of now, I feel like the only way to do a decent ARM template deployment is just deploy the resources without the siteConfig object or the config resource to apply application settings and then follow up with the Azure CLI to deploy the application settings. I suppose I could learn how to retrieve Key Vault secrets using Azure CLI or Azure DevOps pipeline tasks, but it would be even better to just have it all in a single ARM template.
For reference, here is my entire deployment template when I attempted to use dynamically generated appSettings and the config resource to define more appsettings.
{
"$schema": "https://schema.management.azure.com/schemas/2019-08-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"function-app-name": {
"defaultValue": "functionappname",
"type": "String",
"metadata": {
"description": "The name of the function app that you wish to create."
}
},
"sku": {
"type": "string",
"allowedValues": [
"S1",
"S2",
"S3"
],
"defaultValue": "S3",
"metadata": {
"description": "The pricing tier for the hosting plan."
}
},
"storageAccountType": {
"type": "string",
"defaultValue": "Standard_LRS",
"metadata": {
"description": "Storage Account type"
}
},
"location": {
"type": "string",
"defaultValue": "southcentralus",
"metadata": {
"description": "Location for all resources."
}
},
"deploymentEnvironment": {
"type": "string",
"allowedValues": [
"CI",
"DEV",
"QA",
"TRN",
"STG",
"PROD"
],
"metadata": {
"description": "Type of environment being deployed to."
}
},
"applicationSettings": {
"type": "object",
"metadata": {
"description": "Application settings from function.parameters.json"
}
}
},
"variables": {
"storageAccountName": "[concat('store', uniquestring(resourceGroup().id))]",
"appServicePlanName": "[concat('ASP-', uniquestring(resourceGroup().id))]",
"applicationInsightsName": "[concat('appInsights-', uniquestring(resourceGroup().id))]",
"projectName": "DV"
},
"resources": [
{
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2019-04-01",
"name": "[variables('storageAccountName')]",
"kind": "Storage",
"location": "[parameters('location')]",
"sku": {
"name": "[parameters('storageAccountType')]"
},
"tags": {
"project": "[variables('projectName')]",
"env": "[parameters('deploymentEnvironment')]"
}
},
{
"name": "[variables('appServicePlanName')]",
"type": "Microsoft.Web/serverfarms",
"apiVersion": "2019-08-01",
"location": "[parameters('location')]",
"properties": {
},
"tags": {
"project": "[variables('projectName')]",
"env": "[parameters('deploymentEnvironment')]"
},
"sku": {
"Name": "[parameters('sku')]",
"capacity": 2
},
"dependsOn": [
"[resourceId('Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]"
]
},
{
"name": "[variables('applicationInsightsName')]",
"apiVersion": "2015-05-01",
"type": "Microsoft.Insights/components",
"kind": "web",
"location": "[parameters('location')]",
"tags": {
"project": "[variables('projectName')]",
"env": "[parameters('deploymentEnvironment')]"
},
"properties": {
"Application_Type": "web"
}
},
{
"name": "[parameters('function-app-name')]",
"type": "Microsoft.Web/sites",
"apiVersion": "2018-11-01",
"kind": "functionapp",
"location": "[parameters('location')]",
"tags": {
"project": "[variables('projectName')]",
"env": "[parameters('deploymentEnvironment')]"
},
"properties": {
"enabled": true,
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms/', variables('appServicePlanName'))]",
"siteConfig": {
"appSettings": [
{
"name": "APPINSIGHTS_INSTRUMENTATIONKEY",
"value": "[reference(concat('microsoft.insights/components/', variables('applicationInsightsName'))).InstrumentationKey]"
},
{
"name": "AzureWebJobsDashboard",
"value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value)]"
},
{
"name": "AzureWebJobsStorage",
"value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value)]"
},
{
"name": "FUNCTIONS_EXTENSION_VERSION",
"value": "~1"
}
]
}
},
"dependsOn": [
"[resourceId('Microsoft.Insights/components/', variables('applicationInsightsName'))]",
"[resourceId('Microsoft.Web/serverfarms/', variables('appServicePlanName'))]",
"[resourceId('Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]"
],
"resources": [
{
"name": "appsettings",
"type": "config",
"apiVersion": "2018-11-01",
"properties": "[parameters('applicationSettings')[parameters('deploymentEnvironment')]]",
"dependsOn": [
"[resourceId('Microsoft.Web/sites/', parameters('function-app-name'))]"
]
}
]
},
{
"type": "Microsoft.Web/sites/slots",
"apiVersion": "2018-11-01",
"name": "[concat(parameters('function-app-name'), '/stage')]",
"location": "[parameters('location')]",
"dependsOn": [
"[resourceId('Microsoft.Web/sites/', parameters('function-app-name'))]"
],
"tags": {
"project": "[variables('projectName')]",
"env": "[parameters('deploymentEnvironment')]"
},
"kind": "functionapp",
"properties": {
"enabled": true,
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms/', variables('appServicePlanName'))]",
"siteConfig": {
"appSettings": [
{
"name": "APPINSIGHTS_INSTRUMENTATIONKEY",
"value": "[reference(concat('microsoft.insights/components/', variables('applicationInsightsName'))).InstrumentationKey]"
},
{
"name": "AzureWebJobsDashboard",
"value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value)]"
},
{
"name": "AzureWebJobsStorage",
"value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').keys[0].value)]"
},
{
"name": "FUNCTIONS_EXTENSION_VERSION",
"value": "~1"
}
]
},
"resources": [
{
"name": "appsettings",
"type": "config",
"apiVersion": "2018-11-01",
"properties": "[parameters('applicationSettings')[parameters('deploymentEnvironment')]]",
"dependsOn": [
"[resourceId('Microsoft.Web/sites/', parameters('function-app-name'))]"
]
}
]
}
}
]
}
Update 2:
I raised a github issue to have them fix the problem with ARM templates replacing all of the application settings on each deployment.
FWIW - I also voted on some Azure feedback post.
Sorry I don't have a huge amount of time to answer, and you have a bunch of questions which relate mainly to "what's the best way to...", and the answer is always "it depends".
One thing I find easier to manage is instead of using siteConfig to set all the app settings, you can create a top level resource of type Microsoft.Web/sites/config (which I find useful sometimes as you can create them after the site is created, so if you have dependencies elsewhere that aren't setup yet, it can be handy to separate out the config and the site).
"parameters": {
"appSettings": {
"type": "object",
"defaultValue": {
"property1": "value1",
"property2": "value2"
}
}
}
"resources": [
{
"type": "Microsoft.Web/sites",
"apiVersion": "2018-11-01",
"name": "[parameters('function-app-name')]",
"location": "[parameters('location')]",
"kind": "functionapp",
"properties": {
"enabled": true,
"serverFarmId": "..."
}
},
{
"type": "Microsoft.Web/sites/config",
"name": "[concat(parameters('function-app-name'), '/appsettings')]",
"apiVersion": "2018-11-01",
"properties": "[parameters('appSettings')]"
"dependsOn": [ "[resourceId('Microsoft.Web/sites/sites', parameters('function-app-name'))]",
}
]
One of the drawbacks of the above, is that you can't use certain functions in the params section, so you can't use listKeys() to get a key to a resource, so it's only useful sometimes, or like this example, if you wanted to add a reference to app insights which is also created in the same template, this isn't possible if you're passing in the settings as a param.
{
"type": "Microsoft.Web/sites/config",
"name": "[concat(parameters('function-app-name'), '/appsettings')]",
"apiVersion": "2018-11-01",
"properties": {
"property1": "value1",
"property2": "value2",
"APPINSIGHTS_INSTRUMENTATIONKEY": "[reference(resourceId('microsoft.insights/components/', variables('appInsightsName')), '2015-05-01').InstrumentationKey]"
}
"dependsOn": [
"[resourceId('Microsoft.Web/sites/sites', parameters('function-app-name'))]",
"[resourceId('microsoft.insights/components', variables('appInsightsName'))]"
}
You should really be resolving everything you can at deploy time, so a storage account (for example) connection string can be added into the template securely, and resolved at deploy time only.
Another handy tip, is to use key vault to store any secure credentials, api keys, connection strings etc that cannot be resolved in the template. You mention needing them, but then you're committing them to source control in the templates... Well, they wont stay secret very long (another tip, ensure they all use securestring instead of string types, otherwise the portal will expose them in the deployment logs for the resource group). You can access key vaults from app settings like this:
"secretConnectionString": "[concat('#Microsoft.KeyVault(SecretUri=https://', variables('vaultName'), '.vault.azure.net/secrets/my-connection-string/)')]",
But for the above to work, you will need to give your application read access to the vault "vaultName", which should be fine if you use managed service identities.
It is possible to combine static configuration with deployment-time references.
You use the union template function to combine your static configuration (object or array) with some deployment-time value that you wrap using the json template function.
In the example below I combine:
a static base config object
a static service specific config object
a deployment-time Application Insights config value
[union(
variables('appServiceBaseConfig'),
variables('appService1'),
json(
concat(
'{\"APPINSIGHTS_INSTRUMENTATIONKEY\":\"',
reference(concat('microsoft.insights/components/', variables('applicationInsightsName')), '2015-05-01').InstrumentationKey,
'\"}')
)
)
]
For anyone who comes to this thread, I wanted to offer an alternative to the suggestion above of using the union. I first went with that option but started to find it hard to use the string concatenation forming the json so wanted to offer this alternative:
[union(
variables('appServiceBaseConfig'),
variables('appService1'),
createObject(
'APPINSIGHTS_INSTRUMENTATIONKEY', reference(concat('microsoft.insights/components/', variables('applicationInsightsName')), '2015-05-01').InstrumentationKey,
'MY_VALUE', 'xyz'
)
)
]
The reason for this additional suggested answer is that the createObject function makes it much easier to construct the object if you have multiple keys values pairings.
Note: Multi-line functions like this are ONLY currently supported in ARM if you are using the Powershell cli as per this note in the docs. Took me a while to figure this out trying to deploy in Azure DevOps :facepalm:
To answer this piece:
Is it also true that all parameters defined in the parameter file have
to be defined in the deployment template file in the parameters
object?
Yes everything in the parameters file needs to be defined in the deployment file. The opposite is not true. Everything defined in your deployment file does not need to be defined in your parameters file. The definition in the deployment file can have a default value:
"location": {
"type": "string",
"defaultValue": "Central US",
"metadata": {
"description": "Specifies the Azure location where the key vault should be created."
}
},
Alternatively a parameter can be passed in as an override parameter in a release task.
just adding input to this thread to add my solution for this issue. I'm not sure if the overwriting App Settings issue has been fixed in normal ARM Templates, but the release pipeline step allows you to set additional app settings that get deployed when the app gets deployed. So, I have the dynamic app settings/the app settings that all my azure functions need defined on creation set in the ARM Template, and then I have additional app settings set in the release pipeline (using a variable group with secret variables to hide their values). Like so:

Azure Deployment - Overide parameter with ARM functions

I'm using VSTS to deploy azure resources.
I use task "Azure Resource Group Deployment" to deploy ARM templates.
How can I, for a specific parameter, override the value with a ARM function (concat, listkeys, etc)?
Example: My ARM template has a parameter that is a storage account key and instead of providing the key directly, I want to provide it by passing [listkeys(...)]
You cannot do that, several functions (like listKeys()) are evaluated at runtime only. I don't know what you are trying to achieve, so there are probably ways of doing what you try to achieve.
If you want to hide the keys you can store them in the Key Vault and retrieve at deployment time:
"password": {
"reference": {
"keyVault": {
"id": "[resourceId('kvGroup', 'Microsoft.KeyVault/vaults', 'kvName')]"
},
"secretName": "secret"
}
},
If the storage account isn't created within the same ARM template, I'd use the parameter to supply the name of the storage account and then listkeys() within the ARM template to get at the storage account connection string.
If you're creating the storage account in a previous ARM template deployment in your pipeline you could use output parameters to make the connection string available in the pipeline. Here is an example where xxx represents your company naming prefix:
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"environment": {
"type": "string",
"defaultValue": "d",
"metadata": {
"description": "The deployment environment, given by develop (d), testing (t), production (p) or quality assurance (q)"
}
}
},
"variables": {
"busUnit": "vendor_name_here",
//storage account names must be lowercase and are limited to 24 alpha numeric characters
"storage_account_name": "[concat('xxx', parameters('environment'), variables('busUnit'), 'stor')]"
},
"resources": [
{
"type": "Microsoft.Storage/storageAccounts",
"sku": {
"name": "Standard_LRS", //this is a hard coded SKU
"tier": "Standard" //general purpose versus blob-only
},
"kind": "Storage",
"name": "[variables('storage_account_name')]",
"apiVersion": "2017-06-01",
"location": "[resourceGroup().location]", //add it to the same region/location as the resource group
"properties": {
"encryption": {
"keySource": "Microsoft.Storage",
"services": {
"blob": {
"enabled": true
}
}
},
"networkAcls": {
"bypass": "AzureServices",
"defaultAction": "Allow",
"ipRules": [],
"virtualNetworkRules": []
}
},
"dependsOn": []
}
],
"outputs": {
"storageAccountKey": {
//"description": "This works if the storage account is in the same resource group. It returns the access key for the account",
"type": "securestring",
"value": "[listKeys(variables('storage_account_name'),'2015-05-01-preview').key1]"
},
"storageAccountName": {
//"description": "This is the computed name of the storage account, based on naming conventions in the variables",
"type": "string",
"value": "[variables('storage_account_name')]"
}
}
}

Can't create and reference a keyvault secret in the same ARM template deployment

as part of a deployment I'm doing I want to deploy a Key vault into a resource group, create some secrets and then deploy a SQL server into another resource group using one of these secrets as the admin password. The following is a snippet of the resources I am using to do this:
{
"type": "Microsoft.KeyVault/vaults",
"name": "[variables('KeyVaultName')]",
"apiVersion": "2015-06-01",
"location": "[resourceGroup().location]",
"properties": {
"enabledForTemplateDeployment": "true",
"accessPolicies": "[variables('KeyVaultAccessPolicies')]",
"tenantId": "[parameters('TenantId')]",
"sku": {
"name": "Standard",
"family": "A"
}
}
},
{
"type": "Microsoft.KeyVault/vaults/secrets",
"name": "[concat(variables('KeyVaultName'), '/secretname')]",
"apiVersion": "2015-06-01",
"properties": {
"value": "<a randomised value>"
},
"dependsOn": [
"[concat('Microsoft.KeyVault/vaults/', variables('KeyVaultName'))]"
]
},
{
"apiVersion": "2017-05-10",
"name": "deploy",
"type": "Microsoft.Resources/deployments",
"resourceGroup": "<another resource group>",
"properties": {
"mode": "Incremental",
"templateLink": {
"uri": "<my linked template>",
"contentVersion": "1.0.0.0"
},
"parameters": {
"SQLServerAdminPasswordSecretName": {
"value": "secretname"
}
}
},
"dependsOn": [
"[resourceId('Microsoft.KeyVault/vaults/secrets', variables('KeyVaultName'), 'secretname')]"
]
}
My linked template is also has a deployment which looks a bit like the following:
{
"apiVersion": "2017-05-10",
"name": "sql-server-deployment",
"type": "Microsoft.Resources/deployments",
"properties": {
"mode": "Incremental",
"templateLink": {
"uri": "<another linked tempalate>",
"contentVersion": "1.0.0.0"
},
"parameters": {
"SQLServerName": {
"value": "[variables('SQLServerName')]"
},
"SQLServerAdminPassword": {
"reference": {
"keyVault": {
"id": "[resourceId(parameters('ParentResourceGroupName'), 'Microsoft.KeyVault/vaults', parameters('ParentKeyVaultName'))]"
},
"secretName": "[parameters('SQLServerAdminPasswordSecretName')]"
}
}
}
}
}
This reference is currently killing my entire deployment saying:
"The specified KeyVault '/subscriptions/[subscription
id]/resourceGroups/[parent resource
group]/providers/Microsoft.KeyVault/vaults/[my keyvault]' could not be
found."
If I remove the Keyvault reference in my linked template the error goes away.
What is confusing to me is that the linked template deployment depends on the key vault secret, which in turn depends on the key vault, yet nothing is being deployed because the key vault reference is trying to be resolved before anything else.
Am I missing something obvious or is this a flaw in ARM templates?
Thanks in advance
Parameter references are evaluated at "compile" time (i.e. validation), so the vault does need to exist when the template is validated. You might be able to pull this off by nesting a few levels deep and using output references from the nested deployment. You'd need to do something like this:
Nest the vault setup and output the vaultId
nested the SQL Server deployment into a "level 1" nesting
in the parameters for the above bullet, reference the output in step 1
nest another deployment with the parameter reference constructed from the vaultId in the "level 1" nesting
That said, I'm interested in why you'd want to do this... normally the vault reference is used to keep a secret out of plain text/json. Since you're passing the secret value into the deployment at the top level, you already have the value. There's no reason to reference it from the vault since you already have it...

Resources