Запуск и остановка Azure серверов по расписанию

18.04.2019

Для того, чтобы уменьшить расходы на инфраструктуру, можно перевести тестовые серверы в облако Azure и запускать машины только когда они нужны. Чтобы автоматизировать процесс активности серверов, Azure предлагает инструмент Automation Account (учетная запись автоматизации), который доступен по этой ссылке. Проблема представленного метода управления состоянием виртуальных машин в том, что он управляет целой ресурсной группой, а не конкретными машинами, а так же для всех машин будет одно и то же расписание управления.

В этой заметке я предлагаю решение, которое управляет машинами индивидуально, сканируя тэги у всех машин в подписке. Подобный метод используется в этой публикации.

Логика работы
  1. Запуск сервиса каждый час.
  2. Вход в подписку от имени сервисной учетной записи.
  3. Сканирование всех виртуальных машин в подписке и поиск тегов "Schedule Start" и "Schedule Stop" у которых значение равно текущему часу.
  4. Проверка у найденных объектов тега "Schedule Days", если в нем есть текущий день недели, или тег "Schedule Days" отсутствует вообще (разрешены все дни недели).
  5. Проверка состояния найденных объектов (запущены или остановлены).
  6. Запуск или остановка отфильтрованных виртуальных машин.
  7. Запись выполненного действия в тег "Comment" для каждого обработанного объекта.
Примечание.

Возможные значения (только одно из перечисленных значений) тегов "Schedule Start" и "Schedule Stop": 0:00, 1:00, 2:00, 3:00, 4:00, 5:00, 6:00, 7:00, 8:00, 9:00, 10:00, 11:00, 12:00, 13:00, 14:00, 15:00, 16:00, 17:00, 18:00, 19:00, 20:00, 21:00, 22:00, 23:00 - то есть каждый час без учета минут. Часовой пояс времени, используемых в тегах задается в переменных скрипта-обработчика.

Возможные значения (список значений через запятую) тега "Schedule Days": Mon, Tue, Wed, Thu, Fri, Sat, Sun - то есть список дней недели через запятую, когда можно запускать обработчик включения-выключения виртуальной машины. Если тег у виртуальной машины не установлен (не существует), то разрешены все дни недели. Если тег установлен, но пустой, то запрещены все дни недели - задание выполняться не будет.

Создание учетной записи автоматизации

Для запуска задания от сервисной учетной записи необходимо создать объект типа Automation Account. В моем примере он будет называться aac-StartStopSchedule.

1. В консоли Azure в поиске объектов необходимо ввести Automation и выбрать из результатов поиска сервис Automation Account.

Servers - Start-stop Azure instances by a schedule

2. Далее необходимо добавить новый объект.

Servers - Start-stop Azure instances by a schedule

3. Дать объекту имя, например aac-StartStopSchedule. Указать подписку, ресурсную группу, расположение и тип учетной записи. Надать кнопку Создать.

Servers - Start-stop Azure instances by a schedule

4. В результе создания служебной учетной записи может появиться ошибка, что Классическая учетная запись не создалась - это можно проигнорировать если у вас в системе нет объектов старого типа Classic.

Servers - Start-stop Azure instances by a schedule

5. В свойствах созданной учетной записи в разделе Run as accounts можно проверить, что учетная запись для портала создана, а для классического старого портала не создана.

Servers - Start-stop Azure instances by a schedule

6. В разделе Connections можно увидеть типы соединения созданные к порталу.

Servers - Start-stop Azure instances by a schedule

7. В разделе Modules располагаются модули, используемые служебной учетной записью. Рекомендуется обновить все модули до последней версии. Для этого нужно нажать на кнопку Update Azure modules над списком модулей.

Servers - Start-stop Azure instances by a schedule

8. В результате успешного обновления над списком модулей появится соотвествующая запись, а числа версий модулей изменятся на другие.

Servers - Start-stop Azure instances by a schedule

Служебная учетная запись создана. Теперь можно писать скрипты и назначать этому аккаунту запуск скриптов.

Создание скриптов обработки состояния виртуальных машин

Когда служебная учетная запись создана и настроена, то можно писать скрипты и назначать их запуск с помощью этой учетной записи.

1. В свойствах служебной учетной записи перейти в пункт Runbooks и создать новый объект скрипта.

Servers - Start-stop Azure instances by a schedule

2. В окне создания скрипта указать имя, например run-StartSchedule, тип Powershell и нажать кнопку Создать.

Start-stop Azure instances by a schedule

3. Когда объект скрипта создан, в его свойствах нужно нажать на кнопку Редактирования.

Start-stop Azure instances by a schedule

4. Скопировать в окно редактирования скрипт из листинга ниже, нажать Сохранить и Опубликовать.

Примечание. Перед публикацией скрипта его можно протестировать в тестовой панели, нажав кнопку Test pane.
Start-stop Azure instances by a schedule

5. Далее нужно назначить расписание на запуск скрипта на вкладке Schedules в свойствах объекта скрипта. Для этого надо нажать на кнопку Добавить расписание.

Start-stop Azure instances by a schedule

6. В свойствах расписания выбрать уже существующее расписание или создать новое, например Run script every hour. В свойствах скипта указать Время начала работы расписания, Часовой пояс, Периодичность - каждый час, Устаревание - нет.

Start-stop Azure instances by a schedule

Теперь мы имеем всё, что необходимо для управления состояния серверов по расписанию: сервисный аккаунт, скрипт, расписание.

 

Установка тегов расписания включения-выключения на виртуальную машину

Чтобы поставить виртуальную машину в расписание на запуск и остановку, нужно создать в её свойствах теги Schedule Start и Schedule Stop, указав в них значения часов запуска и остановки в формате [Час без ведущих нулей]:00, например 7:00 и 18:00 соответсвенно.

Опционально можно установить тег Schedule Days, в котором указать дни недели, когда применять расписание, например Mon, Wed, Fri. Если этого тега не существует, то расписание выполняется ежедневно, если тег существует, но пустой, то расписание не выполняется вообще.

Start-stop Azure instances by a schedule

 

run-StartSchedule - скрипт запуска серверов
$connectionName = "AzureRunAsConnection"
$TagsTimeZone = "Central Europe Standard Time"
$TagsTimeZoneAbbreviation = "CET"

Write-Output ("----- The script started -----")
Try {
   # Get the connection "AzureRunAsConnection "
   $servicePrincipalConnection=Get-AutomationConnection -Name $connectionName         
   Write-Output ("Logging in to Azure...")
   Login-AzureRmAccount `
       -ServicePrincipal `
       -TenantId $servicePrincipalConnection.TenantId `
       -ApplicationId $servicePrincipalConnection.ApplicationId `
       -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint | Out-Null
}
catch {
   if (!$servicePrincipalConnection) {
       $ErrorMessage = "Connection $connectionName not found"
       throw $ErrorMessage
   } else {
       Write-Error -Message $_.Exception
       throw $_.Exception
   }
}

# Making time filter
Write-Output ("System time zone = " + ([TimeZoneInfo]::Local).Id)
Write-Output ("Current system time = " + (Get-Date -Format "dd.MM.yyyy HH:mm, ddd") )
$TagsTime = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), $TagsTimeZone)).ToString("dd.MM.yyyy HH:mm, ddd")
Write-Output ("Tags time zone = $($TagsTimeZone)")
Write-Output ("Tags time = $($TagsTime)")

$START_TIME = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), $TagsTimeZone)).ToString('H:00')
$START_DAY = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), $TagsTimeZone)).ToString('ddd')
Write-Output ("Adapting time to search for ""$($START_TIME)"" in tags")

Write-Output ("Looking for instances where ""Schedule Start"" tag = ""$($START_TIME)"" ...")

[array]$VMs = Get-AzureRMVm | Where-Object {$PSItem.Tags["Schedule Start"] -eq $START_TIME}

Write-Output ("$($VMs.Count) instances found")

Write-Output ("Processing the instances...")
ForEach ($VM in $VMs) {
   Write-Output "$($VM.Name) instance in $($VM.ResourceGroupName) resource group:"

   $VMTags = $VM.Tags

   Write-Output "  Checking the ""Schedule Days"" tag ..."

   IF ( -not($VMTags.Keys -contains "Schedule Days") -or $VMTags["Schedule Days"].Split(',').Trim() -contains $START_DAY ) {
       Write-Output "  The instance is allowed to be processed today"
       Write-Output "  Checking the instance status ..."
       $VMStatus = (Get-AzureRMVM -ResourceGroupName $vm.ResourceGroupName -Name $vm.Name -Status).Statuses[1].DisplayStatus
       If ($VMStatus -eq "VM deallocated") {
           Write-Output "  The instance is in ""$($VMStatus)"" state"
           Write-Output "  Starting the instance"
           Start-AzureRMVM -Name $VM.Name -ResourceGroupName $VM.ResourceGroupName | Out-Null

           Write-Output "  Updating COMMENT tag for the instance"
           $VMTagCommentText = ("Started by Start-Stop Scheduler at " + `
             ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), $TagsTimeZone)).ToString("dd.MM.yyyy HH:mm") + `
             " $($TagsTimeZoneAbbreviation)")

           If (-not($VMTags.ContainsKey("Comment"))) {
               $VMTags.Add("Comment", $VMTagCommentText)
           } Else {
               $VMTags["Comment"] = $VMTagCommentText
           }
           Set-AzureRMResource -ResourceGroupName $VM.ResourceGroupName -Name $VM.Name -ResourceType "Microsoft.Compute/VirtualMachines" `
           -Tag $VMTags -Force | Out-Null
       } Else {
           Write-Output "  The instance is in ""$($VMStatus)"" state"
           Write-Output "  No action needed"
       }

   } Else {
       Write-Output "  The instance is not allowed to be processed today"
   }


}

Write-Output ("----- The script stopped -----")
run-StopSchedule - скрипт остановки серверов
$connectionName = "AzureRunAsConnection"
$TagsTimeZone = "Central Europe Standard Time"
$TagsTimeZoneAbbreviation = "CET"

Write-Output ("----- The script started -----")
Try {
   # Get the connection "AzureRunAsConnection "
   $servicePrincipalConnection=Get-AutomationConnection -Name $connectionName         
   Write-Output ("Logging in to Azure...")
   Login-AzureRmAccount `
       -ServicePrincipal `
       -TenantId $servicePrincipalConnection.TenantId `
       -ApplicationId $servicePrincipalConnection.ApplicationId `
       -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint | Out-Null
}
catch {
   if (!$servicePrincipalConnection) {
       $ErrorMessage = "Connection $connectionName not found"
       throw $ErrorMessage
   } else {
       Write-Error -Message $_.Exception
       throw $_.Exception
   }
}

# Making time filter
Write-Output ("System time zone = " + ([TimeZoneInfo]::Local).Id)
Write-Output ("Current system time = " + (Get-Date -Format "dd.MM.yyyy HH:mm, ddd") )
$TagsTime = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), $TagsTimeZone)).ToString("dd.MM.yyyy HH:mm, ddd")
Write-Output ("Tags time zone = $($TagsTimeZone)")
Write-Output ("Tags time = $($TagsTime)")

$STOP_TIME = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), $TagsTimeZone)).ToString('H:00')
$STOP_DAY = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), $TagsTimeZone)).ToString('ddd')
Write-Output ("Adapting time to search for ""$($STOP_TIME)"" in tags")

Write-Output ("Looking for instances where ""Schedule Stop"" tag = ""$($STOP_TIME)"" ...")

[array]$VMs = Get-AzureRMVm | Where-Object {$PSItem.Tags["Schedule Stop"] -eq $STOP_TIME}

Write-Output ("$($VMs.Count) instances found")

Write-Output ("Processing the instances...")
ForEach ($VM in $VMs) {
   Write-Output "$($VM.Name) instance in $($VM.ResourceGroupName) resource group:"

   $VMTags = $VM.Tags

   Write-Output "  Checking the ""Schedule Days"" tag ..."

   IF ( -not($VMTags.Keys -contains "Schedule Days") -or $VMTags["Schedule Days"].Split(',').Trim() -contains $STOP_DAY ) {
       Write-Output "  The instance is allowed to be processed today"
       Write-Output "  Checking the instance status ..."
       $VMStatus = (Get-AzureRMVM -ResourceGroupName $vm.ResourceGroupName -Name $vm.Name -Status).Statuses[1].DisplayStatus
       If ($VMStatus -eq "VM running") {
           Write-Output "  The instance is in ""$($VMStatus)"" state"
           Write-Output "  Stopping the instance"
           Stop-AzureRMVM -Name $VM.Name -ResourceGroupName $VM.ResourceGroupName -Force | Out-Null

           Write-Output "  Updating COMMENT tag for the instance"
           $VMTagCommentText = ("Stopped by Start-Stop Scheduler at " + `
             ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), $TagsTimeZone)).ToString("dd.MM.yyyy HH:mm") + `
             " $($TagsTimeZoneAbbreviation)")
           
           If (-not($VMTags.ContainsKey("Comment"))) {
               $VMTags.Add("Comment", $VMTagCommentText)
           } Else {
               $VMTags["Comment"] = $VMTagCommentText
           }
           Set-AzureRMResource -ResourceGroupName $VM.ResourceGroupName -Name $VM.Name -ResourceType "Microsoft.Compute/VirtualMachines" `
           -Tag $VMTags -Force | Out-Null
       } Else {
           Write-Output "  The instance is in ""$($VMStatus)"" state"
           Write-Output "  No action needed"
       }

   } Else {
       Write-Output "  The instance is not allowed to be processed today"
   }
}

Write-Output ("----- The script stopped -----")