When you are migrating VMs from on-premise to Azure, you always have to evaluate the needed availability of several VMs. Your decisions in terms of VM size, storage tiers, and pricing options do rely on this evaluation. In my current migration of on-prem Remote Desktop Services to Azure Virtual Desktop, we have a Remote App that is used quite irregularly. Sometimes once per week, sometimes one to two days, and sometimes not a single time in a week. So we will go with Pay as you Go for these needed VM’s. We can deal with this behavior easily in Azure Virtual Desktop (planned shutdown and start on connect), but that’s only the frontend VM. In my scenario, I have some additional backend VMs which hold some services needed for the running application (licenseservice and some webservices for the DMS integration). We don’t need to run the backend VMs if nobody uses the frontend application, so I want to link the running state of these VMs with each other.
The Frontend VM will be triggered by AVD’s “Starts on Connect” feature, and the needed backend server will be automatically started and deallocated depending on the Frontend VM.
I know there are solutions using EventGrid + Logic App + Azure Automation. But as you may already know, serverless Azure Functions are simply more efficient in terms of scaling and pricing.
In my setup how-to, I decided to simplify the setup with two single VMs. It shouldn’t be hard for someone to adjust, because in the end, you only have to tag the dependent VMs with the same value.
So let’s start …
Create some VMs for testing

We created two resource groups for testing. In my example, I created one named “Lab_init” and one “Lab_triggered” ? This way, we can define which VM can trigger the start process by putting them into this resource group.


I’m going with Ubuntu this time, but it doesn’t really matter. We only want to start and stop, so go with whatever you prefer.

Setup Azure Function App
Now we get to the funny part.





Note: You shouldn’t add the full Az module, as it’s quite large. Only add the submodules you really need.


param($eventGridEvent, $TriggerMetadata)
# Make sure to pass hashtables to Out-String so they're logged correctly
# $eventGridEvent | Out-String | Write-Host
$tAction = ($eventGridEvent.data.authorization.action -split "/")[-2]
$tVmName = ($eventGridEvent.data.authorization.scope -split "/")[-1]
$tSubscriptionId = $eventGridEvent.data.subscriptionId
# preflight check
Write-Host "Check trigger action"
if (($tAction -ne "start") -and ($tAction -ne "deallocate")) {
Write-Warning "Unsupported action: [$tAction], we stop here"
break
}
Write-Host "##################### Triggerinformation #####################"
Write-Host "Vm: $tVmName"
Write-Host "Action: $tAction"
Write-Host "Subscription: $tSubscriptionId"
Write-Host "Get information about trigger vm"
$context = Set-AzContext -SubscriptionId $tSubscriptionId
if ($context.Subscription.Id -ne $tSubscriptionId) {
# break if no access
throw "Azure Function have no access to subscription with id [$tSubscriptionId], check permissions of managed identity"
}
$tVm = Get-AzVM -Name $tVmName
$bindingGroup = $tVm.Tags.bootbinding
if (!$bindingGroup) {
Write-Warning "No tag with bootbinding found for [$tVmName], check your tagging"
break
}
# main
Write-Host "Query all subscriptions"
$subscriptions = Get-AzSubscription
foreach ($sub in $subscriptions) {
Write-Host "Set context to subscription [$($sub.Name)] with id [$($sub.id)]"
$context = Set-AzContext -SubscriptionId $sub.id
if (!$context) {
# break if no access
Write-Warning "Azure Function have no access to subscription with id [$tSubscriptionId], check permissions of managed identity"
return
}
# get vms with bootbinding tag
$azVMs = Get-AzVM -Status -ErrorAction SilentlyContinue | Where-Object { ($_.Tags.bootbinding -eq $bindingGroup) -and ($_.Name -ne $tVmName) }
if ($azVMs) {
$azVMs | ForEach-Object {
Write-Host "VM [$($_.Name)] is in same binding-group, perform needed action "
$vmSplatt = @{
Name = $_.Name
ResourceGroupName = $_.ResourceGroupName
NoWait = $true
}
switch ($tAction) {
start {
Write-Host "Start VM"
$_.PowerState -ne 'VM running' ? (Start-AzVM @vmSplatt | Out-Null) : (Write-Warning "$($_.Name) is already running")
}
deallocate {
Write-Host "Stop VM"
$_.PowerState -ne 'VM deallocated' ? (Stop-AzVM @vmSplatt -Force | Out-Null) : (Write-Warning "$($_.Name) is already running")
}
Default {}
}
}
}
}
Setup event grid






Validate Setup
Now let’s test our configuration




Log into our VMs


As you can see, there is most likely a time difference of 3 minutes between the boottimes, so keep that in mind. In my AVD scenario, it doesn’t really matter, because we have some buffer until the user logs in and starts the application. We never had problems with that.
Hope it can be usefull for somebody, feel free to a adjust