The implementation of Azure Automation in ARM templates has some quirks. The last time I blogged about the lack of idempotency for the jobSchedule resource. In this article I will write about the solution I had to write because the ARM template for webhooks has a little flaw. If you read the template reference documentation you can see that the template format is as follows.
{ "name": "string", "type": "Microsoft.Automation/automationAccounts/webhooks", "apiVersion": "2015-10-31", "properties": { "isEnabled": "boolean", "uri": "string", "expiryTime": "string", "parameters": {}, "runbook": { "name": "string" }, "runOn": "string" } }
Quite straight forward you will think but there is one catch: there are some properties which are defined as not required so you will think that you can leave them blank. Think again: you will have to specify a value for the isEnabled, uri, expiryTime and runbook properties. OK, most of the parameters are quite easy to give a value but what do you have to specify for the uri? It's a specific uri which has some kind of relation to the automation account.
After doing some investigation I found out that the PowerShell implementation for webhooks will generate the uri but that the ARM template version does not support this behavior at the moment. But I do not want to use PowerShell commandlets in my CI/CD pipeline for Azure Automation. My team standardized on ARM templates so my goal is to use as much ARM templates as possible. After doing some research I found the API call Webhook - Generate Uri in the Microsoft documentation. After experimenting with the Try It feature on the page I learned that a trigger URL will be generated when you supply the API the parameter values automation account name and resource group name with the following syntax.
https://[guid].webhook.we.azure-automation.net/webhooks?token=[token]
The next quest is how to incorporate this Rest API in the ARM template. The solution to solve this quest is also this time: Deployment Scripts in combination with a PowerShell file and this time also an Azure Automation variable to store the webhook uri for future usage.
The PowerShell script needs the name of the Automation variable to retrieve/store the webhook uri and the names of the automation account and its resource group. The script will check if the variable exists. If so it will retrieve the value and if its absent it will call the Rest API to generate the uri and store its value in the Automation variable. The last step is to expose the value to the output section of the ARM template section.
[CmdletBinding()] param ( [Parameter(Mandatory = $true)] [String] $AutomationVariableName, [Parameter(Mandatory = $true)] [String] $AutomationAccountName, [Parameter(Mandatory = $true)] [String] $ResourceGroupName ) if (-not (Get-Module -Name Az.Accounts -ListAvailable -ErrorAction SilentlyContinue | Where-Object { $_.Version -eq "2.2.4" })) { Install-Module -Name Az.Accounts -RequiredVersion 2.2.4 -Scope CurrentUser -Force } if (-not (Get-Module -Name Az.Automation -ListAvailable -ErrorAction SilentlyContinue | Where-Object { $_.Version -eq "1.4.2" })) { Install-Module -Name Az.Automation -RequiredVersion 1.4.2 -Scope CurrentUser -Force } if (-not (Get-Module -Name Az.Accounts)) { Import-Module -Name Az.Accounts -RequiredVersion 2.2.4 } if (-not (Get-Module -Name Az.Automation)) { Import-Module -Name Az.Automation -RequiredVersion 1.4.2 } function Get-AccessToken { [CmdletBinding()] param () $azProfile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile if (-not $azProfile.Accounts.Count) { Write-Error 'Could not find a valid profile, please run Connect-AzAccount' return } $profileClient = New-Object Microsoft.Azure.Commands.ResourceManager.Common.RMProfileClient($azProfile) $token = $profileClient.AcquireAccessToken((Get-AzContext).Tenant.TenantId) $token.AccessToken } $contextName = (Get-AzContext).Name Write-Output "Context: $contextName" Write-Output "Retrieve value from Azure Automation account variable $AutomationVariableName."
try { $webhookUri = (Get-AzAutomationVariable -Name $AutomationVariableName -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -ErrorAction Stop).Value Write-Output "Webhook Uri already exist in Azure Automation account variable $AutomationVariableName." } catch { Write-Output "Azure Automation account variable $AutomationVariableName does not exist." } finally { # Reset error count. $Error.Clear() } if ($null -eq $webhookUri) { $armToken = Get-AccessToken $subscriptionId = (Get-AzContext).Subscription.Id $generateWebhookUri = "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.Automation/automationAccounts/$AutomationAccountName/webhooks/generateUri?api-version=2018-06-30" $webhookUri = Invoke-RestMethod -Method Post -Uri $generateWebhookUri -Headers @{Authorization="Bearer $armToken"} -ErrorAction SilentlyContinue Write-Output "Generated a new webhook URI." New-AzAutomationVariable -Name $AutomationVariableName -Encrypted $false -Description "Webhook Uri for running a runbook" -Value $webhookUri -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName | Out-Null Write-Output "Webhook Uri stored in Azure Automation account variable $AutomationVariableName."
} $DeploymentScriptOutputs["webhookUri"] = $webhookUri
The PowerShell script will be triggered within the deployment script. You will need an user assigned identity with contributor rights on the resource group of the Azure Automation account. The script will write the retrieved webhook uri to the outputs section of this deployment step.
{ "name": "dsWebhook", "type": "Microsoft.Resources/deploymentScripts", "apiVersion": "2020-10-01", "identity": { "type": "userAssigned", "userAssignedIdentities": { "/subscriptions/01234567-89AB-CDEF-0123-456789ABCDEF/resourceGroups/myResourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myID": {} } }, "location": "[resourceGroup().location]", "kind": "AzurePowerShell", "properties": { "azPowerShellVersion": "5.0", "cleanupPreference": "Always", "primaryScriptUri": "[concat(parameters('StorageAccount'), '/', 'Get-WebhookUri.ps1', parameters('StorageAccountSASToken'))]", "arguments": "[concat('-AutomationVariableName ', parameters('variableName'), ' -AutomationAccountName ', parameters('automationAccountName'), ' -ResourceGroupName ', resourceGroup().name)]", "retentionInterval": "PT1H", "timeout": "PT15M" } }
In the webhooks section we will reference the retrieved value from the output section of our deploymentScript. The expiryTime parameter has to be specified and has to be in the future. Regarding date/time objects the Azure Automation resources are not idempotent also. These parameters has always to be 5 minutes in the future I found out. The trick is to use a parameter with the default value utcNow('u'). I called this parameter now in the example and based on its value I calculate a date in the future. I choose to adjust the date with 5 years in this case.
{ "name": "[concat(parameters('automationAccountName'), '/', parameters('webhookName'))]", "type": "Microsoft.Automation/automationAccounts/webhooks", "apiVersion": "2018-06-30", "dependsOn": [ "dsWebhook" ], "properties": { "isEnabled": true, "uri": "[reference('dsWebhook').outputs.webhookUri]", "expiryTime": "[dateTimeAdd(parameters('now'), 'P5Y')]", "runbook": "runbook": { "name": "MyRunbook" } } }
With this approach I can deploy webhooks with ARM templates. I hope that Microsoft will solve this issue in the future. In the Azure Feedback I tracked an issue from Fabien Lavocat about this behavior and I found out that the status of that issue is changed today to the Planned status. Let's see when Microsoft will release a fix for this.
A last remark. The documentation on the Microsoft website is outdated regarding the apiVersion parameter. Most of the Azure Automation resources have a newer apiVersion than the published 2015-10-21 version on the website which is marked as the latest. In found that out by changing the apiVersion to an invalid value and look to the error message when I tried to deploy the template.
Comments
Post a Comment