Start/Stop VMs during off-hours solution

Microsoft recently released, in Azure, a VM Start/Stop Solution. And I have been fiddling with it the last few days, as we were working already to overhaul our old runbooks.

Azure portal

While it is straightforward there are a few things that need to be improved like retry on failure and multiple schedules for start. So I started working on those issues by adding another automation account, an Azure storage table, two new runbooks into the mix and a scheduled job which will call the one of the runbooks via a webhook. The end result will be looking close to that.

fintz vm startstop

The Schedule wrapper will be the first script, the main goal of this one is to read the data from the Schedule table, parse them and call the schedule agent. To use the Get-AzureStoragetableRowAll I have imported on the PS modules of the Scheduler Automation account the  AzureRmStorageTable. The Azure table itself is pretty basic with the PartionKey being the Resource group and the Rowkey the Subscription,Excluded VMs, an Active and an Exception flag, Start/Stop in UTC time and Days




$Sub="Sub ID"


$tenantId = "Tenant ID"

$ACred = 'VM mamangement AAD Account'

$aadcreds = Get-AutomationPSCredential -Name $ACred -ErrorAction SilentlyContinue;

Login-AzureRmAccount -ServicePrincipal -Tenant $tenantId -Credential $aadcreds -subscriptionId $Sub | Out-Null;

$sAccount = Get-AzureRmStorageAccount | ?{ $_.StorageAccountName -eq $storageAccount }

if ( !$sAccount )


throw"Storage account [$storageAccount] is not present in current context."


$ctx = $sAccount.Context

$table = Get-AzureStorageTable -Context $ctx -Name $tableName -ErrorAction SilentlyContinue

if ( !$table )


throw"Storage table [$tableName] does not exist in account [$storageAccount]."


$rows=Get-AzureStorageTableRowAll-table $table

# Parse configuration

ForEach ($rowin$rows)


If ($row.Active-eq'true'-and$row.RowKey-eq$Sub)





#Check if there is an exception

If ($row.override-eq'false')












# Check the schedule and create variables

$offHours= ($now.DayOfWeek-notin$Days)

$offHours=$offHours -or ( $row.ExcludeFrom -and $now.TimeOfDay -le $Start)

$offHours=$offHours -or ( $row.ExcludeTo -and $now.TimeOfDay -ge $Stop )

If (!$offHours)




















The MS solution for scheduled Start/Stop VMs is using three parameters External_ExcludeVMNames, External_Start_ResourceGroupNames and External_Stop_ResourceGroupName. The above script is reading our RG schedule and exceptions creating three variable strings for Start,Stop and Exclude and passing them via a Webhook to the Schedule Agent.
So as it can be seen below we do edit the three variables and call the ScheduleStartStop_Parent twice with the Start and Stop parameters.

param (





[String]$StartRG ,

[String]$StopRG ,



$VerbosePreference = 'continue'

if ($WebHookData){

# Collect properties of WebhookData


# Collect individual headers. Input converted from JSON.

$webData= (ConvertFrom-Json-InputObject $WebhookBody)







Write-Error-Message 'Runbook was not started from Webhook'-ErrorAction stop


$runbook = 'ScheduledStartStop_Parent'


$connectionName = "AzureRunAsConnection"



# Get the connection "AzureRunAsConnection "

$servicePrincipalConnection=Get-AutomationConnection-Name $connectionName

"Logging in to Azure..."


-ServicePrincipal `

-TenantId $servicePrincipalConnection.TenantId`

-ApplicationId $servicePrincipalConnection.ApplicationId`

-CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint






if (!$servicePrincipalConnection)


$ErrorMessage="Connection $connectionName not found."


} else{

Write-Error-Message $_.Exception




$StartParam = @{"Action"="Start"}

$StopParam = @{"Action"="Stop"}

$automationAccountName = Get-AutomationVariable -Name 'Internal_AutomationAccountName'

# Initialize Automation Account Global Variables

Set-AutomationVariable –Name 'External_Start_ResourceGroupNames' –Value ''

Set-AutomationVariable –Name 'External_Stop_ResourceGroupNames' –Value ''

Set-AutomationVariable –Name 'External_ExcludeVMNames' –Value ''

# Set Automation Account Global Variables

Set-AutomationVariable –Name 'External_ExcludeVMNames' –Value $excludeVM

Set-AutomationVariable –Name 'External_Start_ResourceGroupNames' –Value $StartRG

Set-AutomationVariable –Name 'External_Stop_ResourceGroupNames' –Value $StopRG

# Start StartStopVm_parent runbbok for Stop/Start actions
$startcheck = Get-AutomationVariable –Name 'External_Start_ResourceGroupNames'

$stopcheck = Get-AutomationVariable –Name 'External_Stop_ResourceGroupNames'

If ($startcheck -ne 'none') {Start-AzureRmAutomationRunbook -automationAccountName $automationAccountName -name $runbook -ResourceGroupName $automationRG -Parameters $StartParam }

If ($stopcheck -ne 'none') {Start-AzureRmAutomationRunbook -automationAccountName $automationAccountName -name $runbook -ResourceGroupName $automationRG -Parameters $StopParam}

I hope it was interesting and helpful, currently I am working to expand the features with retry on failure based on the Azure analytic events and multiple schedules for the same Resource group.