Эта заметка - продолжение публикации Запуск и остановка Azure серверов по расписанию v2. В ней рассказывается о скрипте, который проверяет состояние серверов и отправляет письма об ошибках запуска виртуальных машин администраторам системы, что им необходимо подключиться к порталу Azure и проконтролировать процесс.
Предпосылкой к созданию скрипта проверки стала нестабильность облака Azure, а конкретно то, что команды на включение виртуальных машин могут не исполняться ввиду физической ограниченности ресурсов в датацентрах Azure ("The hardware cluster where the VM is currently deployed did not have enough capacity..."), о чем я писал в предыдущей заметке.
В этом скрипте используется новый модуль взаимодействия PowerShell с облаком Azure, поэтому я расскажу более подробно, как создать новую учетную запись автоматизации. Уже имеющуюся учетную запись автоматизации из скриптов запуска-остановки виртуальных машин использовать без обновления модулей нельзя, а обновлять модули нельзя, так как в скриптах используются старые команды.
- Запуск сервиса через 30 минут от команды на запуск серверов - каждый час.
- Вход в подписку от имени сервисной учетной записи.
- Сканирование всех виртуальных машин в указанном регионею.
- Поиск тегов "Schedule Start" у которых значение равно текущему часу (те же самые метки, которые используются для скрипта запуска серверов).
- Проверка у найденных объектов тега "Schedule Days", если в нем есть текущий день недели, или тег "Schedule Days" отсутствует вообще (разрешены все дни недели).
- Проверка состояния найденных виртуальных машин (запущены ли).
- Запуск отфильтрованных виртуальных машин.
- Запись выполненного действия в тег "Comment" для каждого обработанного объекта.
- Отправка письма администраторам сервиса о найденных незапущенных серверах, что необходимо проконтролировать их.
Для запуска задания от сервисной учетной записи необходимо создать объект типа Automation Account. В моем примере он будет называться aac-Infrastructure.
Итак, необходимо создать новый объект Automation.

Ввести название, например aac-Infrastructure, подписку, ресурсную группу, датацентр (локацию) и выбрать создание учетной записи Azure Run As account.

В свойствах созданного объекта Automation account необходимо перейти в раздел подключаемых модулей Modules и установить новые модули из галереи Browse gallery.

Модули используются для правильной обработки команд PowerShell, которые обращаются к объектам Azure. Список модулей, которые необходимо установить:
- Az.Accounts
- Az.Compute
- Az.KeyVault
- Az.Network
- Az.Profile
- Az.Resources



В конечном итоге раздел Modules должен выглядеть вот так:

Служебная учетная запись Automation создана. Теперь можно переходить к созданию учетной записи SendGrid.
SendGrid - это специальный сервис Azure, который занимается рассылкой сообщений. Чтобы воспользоваться этим сервисом, необходимо создать учетную запись SendGrid.
Необходимо создать новый объект SendGrid.

Ввести название, например sga-Infrastructure, подписку, ресурсную группу, датацентр (локацию), пароль к новой учетной записи, тип подписки и заполнить контактные данные. При первом входе в портал SendGrid сервис потребует подтвердить e-mail, который указан в контактных данных.

В созданном объекте нажать на кнопку управления Manage, чтобы перейти в портал SendGrid.

В портале SendGrid перейти в раздел настроек Settings на вкладку API keys и создать новый ключ доступа для API запросов Create API key.

Дать названию новому ключу, например Send e-mail from Azure Automation Account, назначить полные права FullAccess на доступ у функционалу SendGrid и создать ключ.

Скопировать и сохранить полученный пароль к ключу, так как в дальшейшем он показываться не будет. В моем примере это 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.

Скопировать и сохранить идентификатор ключа. В моем примере это 2wbBs9ItQVKIaabCZqPVLQ.

Учетная запись SendGrid создана и готова к использованию.
Key Vault - это специальное хранилище паролей, токенов, сертификатов, ключей API и других секретных сведений со строгим контролем доступа к ним.
Чтобы воспользоваться сервисом, необходимо создать хранилище ключей Key Vault.

Ввести название, например key-Infrastructure, подписку, ресурсную группу, датацентр (локацию) и прочие параметры.

В созданном хранилище ключей перейти в раздел секретов Secrets и создать новую запись.

Ввести данные API ключа, полученного в сервисе SendGrid. В моем случае это идентификатор ключа 2wbBs9ItQVKIaabCZqPVLQ и пароль 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. Другие параметры выставлять не нужно.

Далее необходимо настроить доступ к хранилищу ключей - выдать учетной записи автоматизации доступ на чтение ключей. Это делается в разделе Access policies.

В новой политике доступа необходимо выбрать доступ на чтение List и получение Get к секретам Secret permissions для учетной записи aac-Infrastructure.

Проверить список доступов и сохранить его.

Хранилище ключей готово.
Последним объектом, необходимым для задачи, является книга запуска Runbook, которая хранит в себе скрипт PowerShell и его настройки запуска.
Необходимо открыть созданную ранее учетную запись автоматизации acc-Infrastructure, перейти в раздел книг запуска Runbooks и создать новую книгу.

Ввести имя, например run-NotStartedInstances, и тип скрипта Powershell.

Вставить текст скрипта (приведен ниже), настроить переменные в начале текста скрипта, сохранить и опубликовать книгу запуска.

Связать созданную книгу с расписанием.

Создать новое расписание с именем Run script every hour at 30 min, выставив первое время запуска в 5:30 и повторение каждый час.

Книга запуска готова.
# ------------------------------------------------------------------------------------------------
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "----- The script started -----")
# ------------------------------------------------------------------------------------------------
# Defining variables
$Subscription = "Subscription_Name"
$EmailToAddress = "AzureAdmins@domain.com"
$EmailSubject = "Azure not started instances"
$TagsTimeZone = "Central Europe Standard Time"
$TagsTimeZoneAbbreviation = "CET"
$Location = "westeurope"
# ------------------------------------------------------------------------------------------------
Function SendEmail($To, $Subject, $Message) {
$FromEmailAddress = "noreply@domain.com"
$FromName = "Azure no reply"
$VaultName = "key-Infrastructure"
$SecretKeyName = "2wbBs9ItQVKIaabCZqPVLQ"
$SENDGRID_API_KEY = (Get-AzKeyVaultSecret -VaultName $VaultName -Name $SecretKeyName).SecretValueText
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("Authorization", "Bearer " + $SENDGRID_API_KEY)
$headers.Add("Content-Type", "application/json")
$EmailBody = @{
"personalizations" = @(
@{
"to" = @(
@{
"email" = $To
}
)
"subject" = $Subject
}
)
"content" = @(
@{
"type" = "text/html"
"value" = $Message
}
)
"from" = @{
"email" = $FromEmailAddress
"name" = $FromName
}
}
$BodyJson = $EmailBody | ConvertTo-Json -Depth 4
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "Sending e-mail to $To")
Invoke-RestMethod -Uri https://api.sendgrid.com/v3/mail/send -Method Post -Headers $headers -Body $bodyJson | Out-Null
}
# ------------------------------------------------------------------------------------------------
# Logging on to Azure portal
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "Logging in to Azure")
$AutoConnection = Get-AutomationConnection -Name AzureRunAsConnection
Connect-AzAccount -ServicePrincipal -Tenant $AutoConnection.TenantID `
-ApplicationId $AutoConnection.ApplicationID `
-CertificateThumbprint $AutoConnection.CertificateThumbprint | Out-Null
# ------------------------------------------------------------------------------------------------
# Executing commands in Azure portal
# Making time filter
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "System time zone = " + ([TimeZoneInfo]::Local).Id)
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "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 ((Get-Date -Format "HH:mm:ss") + ": " + "Tags time zone = $($TagsTimeZone)")
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "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 ((Get-Date -Format "HH:mm:ss") + ": " + "Adapting time to search for ""$($START_TIME)"" in tags")
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "Looking for instances where ""Schedule Start"" tag = ""$($START_TIME)"" ...")
[array]$VMs = Get-AzVm -Location $Location | Where-Object {$PSItem.Tags["Schedule Start"] -eq $START_TIME}
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "$($VMs.Count) instances found")
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "Processing the instances...")
$FoundFlag = $false
ForEach ($VM in $VMs) {
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "$($VM.Name) instance in $($VM.ResourceGroupName) resource group:")
$VMTags = $VM.Tags
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + " Checking the ""Schedule Days"" tag ...")
If ( -not($VMTags.Keys -contains "Schedule Days") -or $VMTags["Schedule Days"].Split(',').Trim() -contains $START_DAY ) {
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + " The instance is allowed to be processed today" )
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + " Checking the instance status ...")
$VMStatus = (Get-AzVM -ResourceGroupName $vm.ResourceGroupName -Name $vm.Name -Status).Statuses[1].DisplayStatus
If ($VMStatus -ne "VM running") {
$FoundFlag = $true
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + " The instance is in ""$($VMStatus)"" state")
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + " The instance name is added into the report")
$FoundServers += "- " + [string]$vm.Name + " (" + $VM.Tags["Description"] + ")</br>"
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + " Starting the instance")
Start-AzVM -Name $VM.Name -ResourceGroupName $VM.ResourceGroupName -AsJob | Out-Null
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + " Updating COMMENT tag for the instance")
$VMTagCommentText = ("Started by Status Checker 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-AzResource -ResourceGroupName $VM.ResourceGroupName -Name $VM.Name -ResourceType "Microsoft.Compute/VirtualMachines" `
-Tag $VMTags -Force -AsJob | Out-Null
} Else {
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + " The instance is in ""$($VMStatus)"" state")
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + " No action needed")
}
} Else {
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + " The instance is not allowed to be processed today" )
}
}
If ($FoundFlag -eq $true) {
$Report = ""
$Report += "<body style=""font-family: Consolas, 'Courier New', monospace; font-size: 10pt;"">"
$Report += "<h2>Report for $Subscription subscription</h2>"
$Report += "<h3>Not started instances:</h3>"
$Report += "<div>" + $FoundServers + "</div>"
$Report += "</br>"
$Report += "<div>The mentioned instances were not started after sending the <i>Start-Stop Scheduler</i> start command.</br>"
$Report += "The <i>Status Checker</i> tried to start the instances again.</div>"
$Report += "</br>"
$Report += "<div style=""color: Red;""><b>Please login to the Azure portal and check the instances states.</b></div>"
$Report += "</body>"
SendEmail -To $EmailToAddress -Subject $EmailSubject -Message $Report
}
Write-Output ((Get-Date -Format "HH:mm:ss") + ": " + "----- The script stopped -----")
Write-Output (Get-Job)
Если скрипт находит незапущенный сервер, то он пытается запустить его и отправляет письмо администраторам, что необходимо проконтролировать процесс.

