使用 PowerShell 脚本监控并启动被停用的 Azure VM
2021年1月25日

最近部署在 Azure 上的 VM 经常因为某些原因被停用(Deallocated),遂想着写一个 PowerShell 脚本配置为 Windows 的计划任务来监控 Azure VM 的状态,一旦发现被停用了,自动启动 Azure VM。该脚本将用到 Azure PowerShell 提供的命令集,通过预先创建的 Azure service principal 来自动登录并获取 VM 状态,如果状态是 deallocated 就调用 Start-AzVm 命令来启动 VM,每次运行结果都会记录到本地日志。

安装 Azure PowerShell 命令集

Azure PowerShell 命令集可以运行在最新的 PowerShell 7.x 上或者 Windows 内置的 PowerShell 5.1 上。

PowerShell core 当前用户安装命令:

Install-Module -Name Az -AllowClobber -Scope CurrentUser

PowerShell core 所有用户安装命令:

Install-Module -Name Az -AllowClobber -Scope AllUsers

Windows 内置 PowerShell 5.1 安装命令:

Install-Module -Name PowerShellGet -Force

安装完成后可以执行 Connect-AzAccount,如果弹出了 Azure 登录窗口那么安装就成功了。

注意:如果想同时在 PowerShell 5.1 和 PowerShell 7.x 上使用 Azure PowerShell,那就得在这两个终端上都安装一次。

创建 Azure service principal

获取 VM 运行状态前需要先登录 Azure 账号。运行 Connect-AzAccount 可以交互式地登录 Azure,但是我们要创建的是自动化脚本,不可能采用这种交互式登录的方式,所以我们需要创建一个 Azure service principal 来代替我们人工登录。

  1. 首先运行 Connect-AzAccount 登录到我们的 Azure 账号。

  2. 登录后会自动选择到账号默认的 Tenant,通过 Get-AzVm -Name 命令看看能否获取到我们想管理的 VM,如果成功获取到了那就直接下一步。如果没有获取到的话,则需要通过 Set-AzContext -SubscriptionId 来切换到 VM 所在 Subscription 所对应的 Tenant。

  3. 然后使用 New-AzADServicePrincipal 创建一个 Azure service principal,指定一个 displayName 即可,会自动生成一个随机密码,通过以下方式可以把明文密码显示出来。

    $sp = New-AzADServicePrincipal -DisplayName "vm-powershell-script-svc-principal"
    $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($sp.Secret)
    $UnsecureSecret = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
    $UnsecureSecret
    

    • ApplicationId 可以作为用户名登录 Azure
    • UnsecureSecret 可以作为密码登录 Azure
    • Id 可以作为 ObjectId 传入 Remove-AzADServicePrincipal -ObjectId 来删除这个 Azure service principal
  4. 我们可以在 Disconnect-AzAccount 后通过下面的命令验证是否可以成功使用刚才创建的 Azure service principal 登录并获取 VM 状态:

    $password = ConvertTo-SecureString -String "cf0d6408-6e25-4685-b4b9-84548821e7d1" -AsPlainText -Force
    $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList "970b5ce1-4dee-47f6-b317-9fb4c925e13c", $password
    Connect-AzAccount -ServicePrincipal -Credential $credential -Tenant "6e30ec6d-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    Get-AzVM -Name "joji-ea"
    

将用户名、密码配置为环境变量

把 Azure service princial 的用户名、密码直接写在脚本里不是很安全,我们选择把用户名、密码配置成为系统环境变量

脚本内容

其实只要登录这块跑通了,脚本本身逻辑还是很简单明了的,判断一下 VM 状态是否是 deallocated,是的话调用 Start-AzVm 来启动 VM,然后加上一个日志功能就算完工了。

function log($message) {
    $global:logContent += "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") $message`r`n"
}

$logFile = "c:\temp\vm.log"
$global:logContent = ""
if (!(Test-Path $logFile)) {
    New-Item $logFile -Force
}
else {
    $global:logContent = Get-Content $logFile -Raw
}
$password = ConvertTo-SecureString -String $Env:VM_SCRIPT_PWD -AsPlainText -Force
$credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Env:VM_SCRIPT_APPID, $password
Connect-AzAccount -ServicePrincipal -Credential $credential -Tenant "6e30ec6d-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -WarningAction SilentlyContinue

$vm = Get-AzVm -Name "joji-ea" -Status
log("VM PowerState: $($vm.PowerState)");

if ($vm.PowerState -match "deallocated") {
    $startvm = Start-AzVm -Name "joji-ea" -ResourceGroupName "linux-rg"
    log("Starting VM...`r`nOperationId: $($startvm.OperationId)`r`nStatus: $($startvm.Status)`r`nStartTime: $($startvm.StartTime.ToString())`r`nEndTime: $($startvm.EndTime.ToString())")
    $newVMstatus = Get-AzVm -Name "joji-ea" -Status
    log("Refreshing... VM PowerState: $($newVMstatus.PowerState)");
}
Set-Content $logFile $global:logContent.TrimEnd()

配置计划任务

最后我们将配置一个计划任务来循环运行这个 PowerShell 脚本以达到监控效果。假设脚本保存在: "C:\temp\vm-monitor.ps1",先打开 PowerShell 运行 C:\temp\vm-monitor.ps1 确保脚本执行没有问题。Windows 上默认的 PowerShell 脚本 ExecutionPolicyRestricted,如果之前没有设置过 ExecutionPolicy,脚本是运行不了的,需要以管理员权限打开 PowerShell 并运行命令 Set-ExecutionPolicy -ExecutionPolicy RemoteSigned 来允许脚本运行。

触发条件配置为:系统启动后就执行,之后每分钟执行一次。

任务执行命令为: pwsh -File c:\temp\vm-monitor.ps1,如果没有安装 PowerShell 7.x 的话,可以改为 powershell -File c:\temp\vm-monitor.ps1 使用 Windows 内置的 PowerShell。

最后是一些其它的配置选项,比如:前一个任务还没运行完,那么起一个新的实例执行任务、如果任务执行超过 1 小时强制停止。

参考资料