Azure FunctionsでVMの起動停止を制御する

# はじめに
コスト削減の対応としてVirtual Machineの稼働時間をコントロールする場合があります。
VMの起動停止機能はMicrosoft社から提供されている[Start/stop VMs v2](https://learn.microsoft.com/ja-jp/azure/azure-functions/start-stop-vms/overview)がありますが、
複数のVMのスケジュール等細かい調整をするには中々難しいところがあると感じています。
そのため、簡単にスケジュール変更出来るようなものをAzure Funtionsで作ってみます。

# Azure Functions
今回はスタックを`PowerShell Core`で作成します。

## 構成
いくつかアプリケーション設定をしておきます。

* SUBSCRIPTIONID
    * 対象サブスクリプションID
* MINUTERANGE
    * 関数が動き出した時から何分前までをタグの設定時間に含めるかの設定（30分毎に動作するなら30）
* START_TAG
    * 起動対象とするVMに設定するタグ名
* STOP_TAG
    * 停止対象とするVMに設定するタグ名

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604991/25070a82-0109-2528-54c9-4801670c93a4.png)


## 関数
トリガーは`Timer trigger`で定期的に動作するようにします。
スケジュールの指定はNCRONTAB式となります。
例えば30分毎に実行する場合は以下のようにします。
`0 */30 * * * *`


```powershell:起動
# Input bindings are passed in via param block.
param($Timer)

# # Get the current universal time in the default string format
# $currentUTCtime = (Get-Date).ToUniversalTime()

# # The 'IsPastDue' porperty is 'true' when the current function invocation is later than scheduled.
# if ($Timer.IsPastDue) {
#     Write-Host "PowerShell timer is running late!"
# }


function runStartVM {
    param ($VirtualMachines,
        [ref]$resultRun,
        $SkipStatus )

    $nextVirtualMachines = @()

    foreach ($VirtualMachine in $VirtualMachines) {

        $Tagval = $VirtualMachine.Tags.$switchTagName
        
        Write-Host "---------------------------------------------------"
        Write-Information "対象VM：$(${VirtualMachine}.Name)"
   
        $TargetDayofWeek = $null
        $TargetPreVM = $null
    
        $TargetDayofWeek = $Tagval.split(",")[1]
        $TargetPreVM = $Tagval.split(",")[2]
    
        # 曜日の指定がない場合、毎日実行
        if (!$TargetDayofWeek) {
            $TargetDayofWeek = "0123456"
        }
        
        # 実行曜日外の場合スキップ
        if ($TargetDayofWeek -notmatch "$([int]$CurrentTime.DayOfWeek)") {
            Write-Information "実行曜日の範囲外のためスキップします。"
            continue
        }
    
        # 先行条件のVMを指定、かつ先行のVMが停止している場合は、次回に持ち越し
        if ($TargetPreVM) {
            Write-Host "先行VM「${TargetPreVM}」"
            $preVM = Get-AzVM -Name $TargetPreVM
            if (!$preVM) {
                Write-Host "先行VMの情報取得に失敗したため、スキップします。"
                $resultRun = $returnCode.Error
                continue
            }
            $VMStatus = Get-AzVM -ResourceId $preVM.Id -Status | Select-Object @{n = "Status"; e = { $_.Statuses[1].Code } }
            Write-Host "先行VMステータス「$(${VMStatus}.Status)」"
            if ($VMStatus.Status -ne $SkipStatus) {
                $nextVirtualMachines += $VirtualMachine
                Write-Host "先行VM「${TargetPreVM}」が起動していないため、スキップします。"
                continue
            }
        }

    
        # VMが起動していたらスキップ
        $VMStatus = $null
        $VMStatus = Get-AzVM -ResourceId $VirtualMachine.Id -Status | Select-Object @{n = "Status"; e = { $_.Statuses[1].Code } }
        if ($VMStatus.Status -eq $SkipStatus) {
            Write-Information "仮想マシン「$($VirtualMachine.Name)」のステータスが「$($VMStatus.Status)」のためスキップします。"
            continue
        }
    
        Write-Information "仮想マシン「$(${VirtualMachine}.Name)」を${StartStop}します。"
        $resultVM = Start-AzVM -Id $VirtualMachine.Id
    
        if ($resultVM -eq "Failed") {
            Write-Error "仮想マシン「$(${VirtualMachine}.Name)」の${StartStop}に失敗しました。"
            $resultRun = $returnCode.Error
            continue
        }
        Write-Information "仮想マシン「$(${VirtualMachine}.Name)」の${StartStop}に成功しました。"
    }

    return $nextVirtualMachines

}


Set-Variable -Name returnCode -Value @{Success = 0; Error = 1; Exception = 99 } -Option ReadOnly

##############################################################
# 起動停止の設定 Stop or Start
$StartStop = "Start"
$SkipStatus = "PowerState/running"
##############################################################

$switchTagName = $env:START_TAG

$MinuteRange = $env:MINUTERANGE
$CurrentTime = (Get-Date)
$RangeStart = $CurrentTime.AddMinutes(-$MinuteRange)
$resultRun = $returnCode.Success

Write-Information "仮想マシンの${StartStop}処理を開始します。"

# Azureにログイン
Connect-AzAccount -Identity -SubscriptionId $env:SUBSCRIPTIONID

# 対象VMの取得
$VirtualMachines = Get-AzVM | where { $_.Tags.Keys -ieq $switchTagName } | where { [datetime]$_.tags.vmstart.split(",")[0] -ge $RangeStart -and [datetime]$_.tags.vmstart.split(",")[0] -le $CurrentTime }

Write-Information "対象時間：${RangeStart}～${CurrentTime}"

$targetCount = $VirtualMachines.count
$nextCount = 0

$nextVirtualMachines = $VirtualMachines
while ($targetCount -ne $nextCount) {
    
    $targetCount = $nextCount
    $nextVirtualMachines = runStartVM -VirtualMachines $nextVirtualMachines -resultRun ([ref]$resultRun) -SkipStatus $SkipStatus
    $nextCount = $nextVirtualMachines.count

    if ($nextCount -eq 0) {
        break
    }
}


Write-Information "仮想マシンの${StartStop}処理を終了します。"

exit $resultRun
```

startやステータスの箇所を修正して起動用とは別に停止用の関数を作成します。

# VMのタグ設定
対象のVMにタグを設定します。
タグの名前はAzure Functionsの構成で指定した名称にして、値は以下の書式で設定します。


<起動/停止時刻>,<曜日[0-6]>,<先行VM名>

* 起動/停止時刻
    * 時刻を指定します（例：20:00）
* 曜日（省略可能）
    * 実行したい曜日を0-6で指定します（0が日曜、順に1が月曜、2が火曜…etc）
* 先行VM名（省略可能）
    * 先に起動/停止しておく必要があるVMがある場合にVM名を入れます

以下のような感じでタグをつけます。
毎日17:00にtamura-vm-winというVMが起動している場合に起動するようになります。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604991/026a9f6c-2236-08c7-bc37-5f72b77c9727.png)

# Functinosの実行
3台のVMを順番に起動してみます。
各種VMのタグ設定は以下の感じです。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604991/202401d4-ea6c-cf38-1efd-d4772de44ebd.png)
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604991/7d936fa9-8b82-e4c6-83fe-de261fa1b452.png)
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604991/7fdc52f4-d6ae-d617-16ce-1dfe5543bcbe.png)



実行前
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604991/930f8b7a-1906-bd70-5c0c-ce5b6ca372b8.png)

実行結果のログです。
一部文字化けしていますがVSCodeでログ表示させたことが原因かもしれません。
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604991/92ae0d92-650b-9278-97e8-56f62da81343.png)

実行後。
無事起動しました。
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604991/3898e8d7-87db-853b-b593-56d438b052fa.png)



# おわりに
これでVMのタグを編集するだけで起動や停止のスケジュールを変更できます。