Skip to main content

Azure Automation: deploy webhooks with ARM templates

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

Popular posts from this blog

CS8357: The specified version string contains wildcards, which are not compatible with determinism.

Today I was busy with creating a WCF service solution in Visual Studio Enterprise 2017 (15.9.2). In this solution I use a few C# class libraries based on .NET 4.7.2. When I compiled the solution I got this error message: Error CS8357: The specified version string contains wildcards, which are not compatible with determinism. Either remove wildcards from the version string, or disable determinism for this compilation The error message is linking to my AssemblyInfo.cs file of the Class library projects. In all the projects of this solution I use the wildcard notation for generating build and revision numbers. // Version information for an assembly consists of the following four values: // // Major Version // Minor Version // Build Number // Revision // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0....

Fixing HTTP Error 401.2 unauthorized on local IIS

Sometimes the Windows Authentication got broken on IIS servers so you cannot log in locally on the server. In that case you get the dreadfully error message HTTP Error 401.2 - Unauthorized You are not authorized to view this page due to invalid authentication headers. To fix this issue you can repair the Windows Authentication feature with the following PowerShell commands: Remove-WindowsFeature Web-Windows-Auth Add-WindowsFeature Web-Windows-Auth

Assign an existing certificate to your IIS website with WiX

Recently I had to change the bindings of existing IIS hosted websites and APIs from HTTP to HTTPS. They are installed with a MSI file created with the WiX Toolset . Because I have to use an already on the server installed certificate I cannot use the Certificate element from the IIS Extension because this element only supports installing and uninstalling certificates based on PFX files. After doing some research I found the blog article Assign Certificate (Set HTTPS Binding certificate) to IIS website from Wix Installer which described the usage of Custom Actions for this purpose. I adopted this approach and rewrote the code for my scenario. With WiX I still create the website. <iis:WebSite Id="WebSite" ConfigureIfExists="yes" AutoStart="yes" Description="MyWebsite" Directory="IISROOT" StartOnInstall="yes"> <iis:WebAddress Id="WebSite...