I don’t take any credit for this post.
It’s mostly stuff I learnt from Google searches, and a script I put together for my own use from other places. I should have credited the sources, but all this was put together over time and today I had to do it again and that’s when I went through my past snippets etc. and created something in a formal way.
Anyways. Here’s the issue. I have a bunch of Azure Automation Runbooks that run on a schedule. Thing is, sometimes a Runbook takes longer than expected and so while it is running another one launches. This leads to both of them slowing down or conflicting with each others tasks and soon I have a whole bunch of Runbooks launching and making a mess of things. I need a way of ensuring only one Runbook is active at a time.
There’s a variable within Runbooks called PSPrivateMetadata
. It doesn’t have anything except the JobId. You can extract this via $PSPrivateMetadata.JobId.Guid
. Then you must search all running jobs to find the Automation Account name, Runbook name, and Resource Group name of this Runbook; and using that find what other jobs are running. And then decide what to do – wait for quit. Simple really. Wish the variable had the various names so I can save some trouble finding them… but oh well.
Here’s what I came up with:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# This is a Function I created (from various Google results) to throttle a Runbook. # It will either wait or quit the runbook. function Throttle-AzRunbook { param( [switch]$quitRatherThanWait, [int]$numberOfInstances = 1 ) # Connect to Azure. With a Managed Identity in this case as that's what I use. # It's like I am already connectes but I can't assume that within this function. # Must connect to Azure before running Get-AzAutomationJob or Get-AzResource try { # From https://docs.microsoft.com/en-us/azure/automation/enable-managed-identity-for-automation#authenticate-access-with-system-assigned-managed-identity # Ensures you do not inherit an AzContext in your runbook Disable-AzContextAutosave -Scope Process | Out-Null # Connect to Azure with system-assigned managed identity Connect-AzAccount -Identity | Out-Null } catch { Write-Error "Runbook could not connect to Azure: $($_.Exception.Message)" exit } # Get the Job ID from PSPrivateMetadata. That's the only thing it contains! $automationJobId = $PSPrivateMetadata.JobId.Guid # Get all Runbooks in the current subscription $allAutomationAccounts = Get-AzResource -ResourceType Microsoft.Automation/automationAccounts $automationAccountName = $null $resourceGroupName = $null $runbookName = $null foreach ($automationAccount in $allAutomationAccounts) { $runbookJob = Get-AzAutomationJob -AutomationAccountName $automationAccount.Name ` -ResourceGroupName $automationAccount.ResourceGroupName ` -Id $automationJobId ` -ErrorAction SilentlyContinue if (!([string]::IsNullOrEmpty($runbookJob))) { $automationAccountName = $runbookJob.AutomationAccountName $resourceGroupName = $runbookJob.ResourceGroupName $runbookName = $runbookJob.RunbookName } } # At this point I'll have the Automation Account Name, Runbook Name, Job ID and Resource Group Name, # Find all other active jobs of this Runbook. $allActiveJobs = Get-AzAutomationJob -AutomationAccountName $automationAccountName ` -ResourceGroupName $resourceGroupName ` -RunbookName $runbookName | Where-Object { ($_.Status -eq "Running") -or ($_.Status -eq "Starting") -or ($_.Status -eq "Queued")} if ($quitRatherThanWait.IsPresent -and $allActiveJobs.Count -gt $numberOfInstances) { Write-Output "Exiting as another job is already running" exit } else { $oldestJob = $AllActiveJobs | Sort-Object -Property CreationTime | Select-Object -First 1 # If this job is not the oldest created job we will wait until the existing jobs complete or the number of jobs is less than numberOfInstances while (($AutomationJobID -ne $oldestJob.JobId) -and ($allActiveJobs.Count -ge $numberOfInstances)) { Write-Output "Waiting as there are currently running $($allActiveJobs.Count) active jobs for this runbook already. Sleeping 30 seconds..." Write-Output "Oldest Job is $($oldestJob.JobId)" Start-Sleep -Seconds 30 $allActiveJobs = Get-AzAutomationJob -AutomationAccountName $automationAccountName ` -ResourceGroupName $resourceGroupName ` -RunbookName $runbookName | Where-Object { ($_.Status -eq "Running") -or ($_.Status -eq "Starting") -or ($_.Status -eq "Queued")} $oldestJob = $allActiveJobs | Sort-Object -Property CreationTime | Select-Object -First 1 } Write-Output "Job can continue..." } } |
Will update this in case of any bugs later.
I put a line like this before my main script in the Runbook:
1 2 3 |
# === [ Main Script ] === # Quit if another instance is active Throttle-AzRunbook -quitRatherThanWait |
And that’s it.
Update (17th July 2023): Looks like PowerShell 7.1 and 7.2 Runbooks dont have the $PSPrivateMetadata
variable. Here’s the output of Get-Variable
on a PowerShell 7.2 HRW:
But PowerShell 5.1 has it.
So the above function won’t work on 7.x for now.
Update (2nd Sept 2023): Noticed that $PSPrivateMetadata
is now available in PowerShell 7.2 (must have been for a while, I just noticed today).
Update (26th Sept 2023): It is however missing in Hybrid Runbook Workers!
Update (29th Sept 2023): See also this post.