I was looking into adding a step that makes an HTTP request from ARM templates after I finish the swapping operation. Is it something doable using ARM templates?
ARM templates have no build-in methods to perform HTTP requests. If you absolutely must call an HTTP endpoint from your template, you can deploy a deployment script resource. This is a temporary container resource that executes a script and can be configured to be automatically removed after it ran sucessfully. In your case that might be a lot of infrastructure overhead for very little function.
An alternative approach would be to use your ARM template to deploy an Event Grid event subscription. The event subscription can be configured to call a webhook or an Azure Function every time a slot swap is completed successfully on your app:
Here is an example:
"resources": [
{
"type": "Microsoft.EventGrid/systemTopics",
"apiVersion": "2020-10-15-preview",
"name": "{{ SYSTEM_TOPIC_NAME }}",
"location": "{{ LOCATION }}",
"properties": {
"source": "{{ RESOURCE_ID_OF_APP }}",
"topicType": "Microsoft.Web.Sites"
}
},
{
"type": "Microsoft.EventGrid/systemTopics/eventSubscriptions",
"apiVersion": "2020-10-15-preview",
"name": "{{ SYSTEM_TOPIC_NAME }}/{{ EVENT_SUBSCRIPTION_NAME }}",
"dependsOn": [
"[resourceId('Microsoft.EventGrid/systemTopics', '{{ SYSTEM_TOPIC_NAME }}')]"
],
"properties": {
"destination": {
"properties": {
"maxEventsPerBatch": 1,
"preferredBatchSizeInKilobytes": 64,
"endpointUrl": "{{ WEBHOOK_URL }}"
},
"endpointType": "WebHook"
},
"filter": {
"includedEventTypes": [
"Microsoft.Web.SlotSwapCompleted"
],
"enableAdvancedFilteringOnArrays": true
},
"labels": [],
"eventDeliverySchema": "EventGridSchema",
"retryPolicy": {
"maxDeliveryAttempts": 30,
"eventTimeToLiveInMinutes": 1440
}
}
}
]
Related
I'm trying to create an Eventgrid subscription on an Azure Storage Account using an ARM template. Manually creating it in the Portal and going to the advanced settings yielded me the template below. I further added the required template items such as schema to it, but it keeps yielding me errors. I've tried looking online for similar templates, but can't seem to find any using the "endpointType": "AzureFunction". Also within the Resource Explorer there's no mention of the deployment to further help me along.
Anybody can help me out what is wrong?
The template as generated during creation from the portal:
{
"name": "test123",
"properties": {
"topic": "/subscriptions/<guid>/resourceGroups/<myGroup>/providers/Microsoft.Storage/storageAccounts/<myStorageAccount>",
"destination": {
"endpointType": "AzureFunction",
"properties": {
"resourceId": "/subscriptions/<guid>/resourceGroups/<myGroup>/providers/Microsoft.Web/sites/<myFunctionsApp>/functions/<myFunction>",
"maxEventsPerBatch": 1,
"preferredBatchSizeInKilobytes": 64
}
},
"filter": {
"includedEventTypes": [
"Microsoft.Storage.BlobCreated"
],
"advancedFilters": [
{
"operatorType": "StringContains",
"key": "Subject",
"values": [
"-original"
]
}
]
},
"labels": [],
"eventDeliverySchema": "EventGridSchema"
}
}
The full template:
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
},
"resources": [
{
"name": "test123",
"type": "Microsoft.EventGrid/eventSubscriptions",
"apiVersion": "2020-01-01-preview",
"location": "westeurope",
"properties": {
"topic": "/subscriptions/<guid>/resourceGroups/<myGroup>/providers/Microsoft.Storage/storageAccounts/<myStorageAccount>",
"destination": {
"endpointType": "AzureFunction",
"properties": {
"resourceId": "/subscriptions/<guid>/resourceGroups/<myGroup>/providers/Microsoft.Web/sites/<myFunctionsApp>/functions/<myFunction>",
"maxEventsPerBatch": 1,
"preferredBatchSizeInKilobytes": 64
}
},
"filter": {
"includedEventTypes": [
"Microsoft.Storage.BlobCreated"
],
"advancedFilters": [
{
"operatorType": "StringContains",
"key": "Subject",
"values": [
"-original"
]
}
]
},
"labels": [
],
"eventDeliverySchema": "EventGridSchema"
}
}
]
}
The error:
The specified topic property does not match the expected topic from the event subscription scope
I've been trying to do the exact same thing by any option in the Azure tool chain (ARM Template/CLI/REST). I looked at the Portal's calls and found it is using the 2020-01-01-preview EventGrid API that you show.
After some testing I can confirm the new API allows deploying a subscription with an EndpointType of AzureFunction like so:
{
"name": "[concat(variables('eventDomainName'), '/Microsoft.EventGrid/', variables('subscriptionName'))]",
"type": "Microsoft.EventGrid/domains/providers/eventSubscriptions",
"location": "[variables('location')]",
"apiVersion": "2020-01-01-preview",
"properties": {
"destination": {
"endpointType": "AzureFunction",
"properties": {
"resourceId": "[resourceId('Microsoft.Web/sites/functions/', parameters('functionAppName'), parameters('functionName'))]"
}
},
"filter": "[parameters('subscriptionProperties').filter]"
}
}
It seems that your problem is unrelated to trying to target the AzureFunction and you're using the right API version so it doesnt seem to be that.
I think the problem is your "Type" value. I think it should be in this format: //providers/eventSubscriptions
So it would be Microsoft.Storage/storageAccounts/providers/eventSubscriptions.
I don't believe there is a separate endpointType of AzureFunction as documented. It is simply a special case of a webhook handler.
This GitHub Repo contains a sample ARM Template that you can refer to. Here is the exact snippet that you would need
...
"destination": {
"endpointType": "WebHook",
"properties": {
"endpointUrl": "[concat(variables('functionUrl'), listKeys(resourceId('Microsoft.Web/sites/host/', variables('functionAppName'), 'default'),'2016-08-01').systemkeys.eventgrid_extension)]"
}
}
...
I'm going around in circles since few days. I would like to link, via ARM Template, my resource "microsoft.insights/actionGroups" to a resource "Microsoft.Automation/automationAccounts/webhooks"
The webhook and the action group are both created via ARM Template. The problem is that when creating the webhook nothing can keep the uri produced by ARM. Then on my resource group the "automationRunbookReceivers" property requests the "serviceUri" parameter which is mandatory. If I refer to my webhook via the resource the uri I get is empty ...
resourceId ('Microsoft.Automation / automationAccounts / webhooks', parameters ('AzureAutomationName'), 'RunBookName')
how could I automate this process?
This is the templates I've used to generate my resources :
{
"name": "[concat(variables('automationAccountName'), '/WebHookName')]",
"type": "Microsoft.Automation/automationAccounts/webhooks",
"apiVersion": "2015-10-31",
"dependsOn": [
"[concat('Microsoft.Automation/automationAccounts/', variables('automationAccountName'), '/runbooks/', 'RunBookName')]"
],
"properties": {
"isEnabled": "true",
"expiryTime": "2026-11-20",
"runbook": {
"name": "RunBookName"
}
}
},
{
"name": "[variables('ActionGroupName')]",
"type": "microsoft.insights/actionGroups",
"apiVersion": "2019-06-01",
"location": "Global",
"tags": {
"displayName": "ActionGroupName"
},
"properties": {
"groupShortName": "[variables('ActionGroupShortName')]",
"enabled": true,
"automationRunbookReceivers": [
{
"name": "MyRunBookReceiver",
"automationAccountId": "[resourceId('microsoft.insights/components', parameters('AzureTelemetryName'))]",
"runbookName": "RunBookName",
"webhookResourceId": "[resourceId('Microsoft.Automation/automationAccounts/webhooks', parameters('AzureAutomationName'), 'WebHookName')]",
"isGlobalRunbook": false,
"serviceUri": "listCallbackURL? resourceId? reference? other? ?????????"
}
]
}
}
I desperately need help!
Thank you!
[reference(resourceId('Microsoft.Automation/automationAccounts/webhooks', parameters('AzureAutomationName'), 'WebHookName'), '2015-10-31')].uri
return empty string
As far as I know, we only can see the url of webhook when we create it. You can use Powershell to create it and you can see url of outputs
#bit is correct - the webhook URI is only retrievable at the time of webhook creation and the property is nulled thereafter. Since you're creating both the actionGroup and the webhook in the same template, though, the deployment happens synchronously and you can refer to the webhook's URI using its .uri property.
The official Microsoft documentation has an example: https://learn.microsoft.com/en-us/azure/automation/automation-webhooks#create-runbook-and-webhook-with-arm-template
Your ARM template could be modified as follows:
{
"name": "[concat(variables('automationAccountName'), '/WebHookName')]",
"type": "Microsoft.Automation/automationAccounts/webhooks",
"apiVersion": "2015-10-31",
"dependsOn": [
"[concat('Microsoft.Automation/automationAccounts/', variables('automationAccountName'), '/runbooks/', 'RunBookName')]"
],
"properties": {
"isEnabled": "true",
"expiryTime": "2026-11-20",
"runbook": {
"name": "RunBookName"
}
}
},
{
"name": "[variables('ActionGroupName')]",
"type": "microsoft.insights/actionGroups",
"apiVersion": "2019-06-01",
"location": "Global",
"tags": {
"displayName": "ActionGroupName"
},
"properties": {
"groupShortName": "[variables('ActionGroupShortName')]",
"enabled": true,
"automationRunbookReceivers": [
{
"name": "MyRunBookReceiver",
"automationAccountId": "[resourceId('microsoft.insights/components', parameters('AzureTelemetryName'))]",
"runbookName": "RunBookName",
"webhookResourceId": "[resourceId('Microsoft.Automation/automationAccounts/webhooks', parameters('AzureAutomationName'), 'WebHookName')]",
"isGlobalRunbook": false,
"serviceUri": "[reference(concat(variables('automationAccountName'), '/WebHookName')).uri]"
}
]
}
}
As an aside, that Microsoft doc uses an "outputs": { } object to emit the webhook URI. That's a really bad idea because the plaintext value of the URI will be recorded in the resource group deployment metadata. If you need to create the webhook and its clients asynchronously, one solution is to store the webhook URI in a Key Vault secret in the template that creates the webhook, and then consume the Key Vault secret value when deploying the webhook client.
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')]"
}
}
}
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.
]
}
}
]
}
]
}
I'm using Azure ARM templates in order to make sure that I can repeatedly deploy uniform infrastructure and services. My ARM template consists of an App Service, Web App, Service Bus Queue, and Azure SQL database. On top of this, I'm setting up continuous deployment through VSTS.
Everything is working well EXCEPT I am not sure how to set a Service Bus SAS token at the Namespace level. I don't see a way in the Service Bus ARM template to specify one, so I cannot pre-generate a token and place it in my web.config file. I also don't see a way to have one generated on my behalf, then pull the values back to my web.config file. Any suggestions would be greatly appreciated.
I believe you have two options:
1) Get generated key from the output:
"outputs": {
"eh:Endpoint": {
"value": "[listKeys(resourceId('Microsoft.EventHub/namespaces/authorizationRules', variables('eventHubNamespaceName'), 'SendOnlyKey'),'2015-08-01').primaryKey]",
"type": "string"
},
}
And incorporate it in your build/release process.
2) Try to push a key with a template:
{
"apiVersion": "[parameters('eventHubVersion')]",
"name": "[variables('eventHubNamespaceName')]",
"type": "Microsoft.EventHub/namespaces",
"location": "[resourceGroup().location]",
"resources": [
{
"apiVersion": "2014-09-01",
"name": "[variables('eventHubName')]",
"type": "eventHubs",
"dependsOn": [
"[concat('Microsoft.EventHub/namespaces/', variables('eventHubNamespaceName'))]"
],
"properties": {
"path": "[variables('eventHubName')]",
"MessageRetentionInDays": "[parameters('messageRetentionInDays')]",
"PartitionCount": "[parameters('partitionCount')]"
},
"resources": [
{
"apiVersion": "[parameters('eventHubVersion')]",
"name": "StorageRetention",
"type": "consumergroups",
"dependsOn": [
"[variables('eventHubName')]",
"[concat('Microsoft.EventHub/namespaces/', variables('eventHubNamespaceName'))]"
],
"tags": {
"displayName": "eh"
}
}
]
},
{
"apiVersion": "[parameters('eventHubVersion')]",
"name": "[concat(variables('eventHubNamespaceName'),'/SendOnlyKey')]",
"type": "Microsoft.EventHub/namespaces/authorizationRules",
"dependsOn": [
"[concat('Microsoft.EventHub/namespaces/', variables('eventHubNamespaceName'))]"
],
"location": "[resourceGroup().location]",
"properties": {
"KeyName": "SendOnlyKey",
"ClaimType": "SendSharedAccessKey",
"ClaimValue": "None",
"PrimaryKey": "[parameters('eventHubSendPrimaryKey')]",
"SecondaryKey": "your_key",
"Rights": [ "Send" ],
"Revision": -1
}
},
{
"apiVersion": "[parameters('eventHubVersion')]",
"name": "[concat(variables('eventHubNamespaceName'),'/ListenOnlyKey')]",
"type": "Microsoft.EventHub/namespaces/authorizationRules",
"dependsOn": [
"[concat('Microsoft.EventHub/namespaces/', variables('eventHubNamespaceName'))]"
],
"location": "[resourceGroup().location]",
"properties": {
"KeyName": "ListenOnlyKey",
"ClaimType": "ReceiveSharedAccessKey",
"ClaimValue": "None",
"PrimaryKey": "your_key",
"SecondaryKey": "your_key",
"Rights": [ "Listen" ],
"Revision": -1
}
}
]
}
However note that the second solutions works only for an older version of API and sooner or later will be deprecated. Additionally I tested it only for pushing keys for a hub, not a namespace.
This might help others arriving at this answer
In EastUS using API_VERSION = 2017-04-01
The following will work to obtain references to primarykey and related fields
- connectionString: "[concat('',listKeys(resourceId('Microsoft.EventHub/namespaces/eventhubs/authorizationRules','{{ eh_namespace }}', '{{ eventhub_name }}','fw'), '2017-04-01').primaryConnectionString,'')]"