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.

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.

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

param( $tableName='VMSchedule', $storageAccount='StorageAccountName', $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) { $RG=$row.PartitionKey $eVm=$row.ExcludeServers $ExcludeVM=$ExcludeVM+','+$eVM #Check if there is an exception If ($row.override-eq'false') { $Days=$row.Days-split',' $start=$row.UTCstart $stop=$row.UTCstop } else { $Days=$row.ExceptDays-split',' $start=$row.Exceptstart $stop=$row.Exceptstop } # 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) { $StartRG+=$RG+',' } else { $StopRG+=$RG+',' } } } $stopRG=$stopRG-replace".$" $startRG=$startRG-replace".$" $ExcludeVM=$ExcludeVM-replace".$" $webhook='https://s2events.azure-automation.net/webhooks?token=...' $Body=@(StartRG=$StartRG;StopRG=$StopRG;ExcludeVM=$ExcludeVM) $params=@{ Body=$Body|convertto-json Method='Post' URI=$webhook } Invoke-RestMethod@params-Verbose 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.

[CmdletBinding()] param ( [object]$WebhookData, [Parameter(Mandatory=$true)] [String]$StartRG , [String]$StopRG , [String]$excludeVM ) $VerbosePreference = 'continue' if ($WebHookData){ # Collect properties of WebhookData $WebhookBody=$WebHookData.RequestBody # Collect individual headers. Input converted from JSON. $webData= (ConvertFrom-Json-InputObject $WebhookBody) $StartRG=$webData.StartRG $StopRG=$webData.StopRG $excludeVM=$webData.ExcludeVM } else { Write-Error-Message 'Runbook was not started from Webhook'-ErrorAction stop } $runbook = 'ScheduledStartStop_Parent' #Authentrication $connectionName = "AzureRunAsConnection" try { # Get the connection "AzureRunAsConnection " $servicePrincipalConnection=Get-AutomationConnection-Name $connectionName "Logging in to Azure..." Add-AzureRmAccount` -ServicePrincipal ` -TenantId $servicePrincipalConnection.TenantId` -ApplicationId $servicePrincipalConnection.ApplicationId` -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint } catch { if (!$servicePrincipalConnection) { $ErrorMessage="Connection $connectionName not found." throw$ErrorMessage } else{ Write-Error-Message $_.Exception throw$_.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.