Start/Stop VMs v2 の内部動作について知る

Start/Stop VMs v2 は Azure で提供されている仮想マシンの起動と停止を行うサービスです。
スケジュールによる操作が可能なので、業務開始前に起動しておく、終業後に止める等の操作を通して VM 使用コストの低減が実現可能です。
以前提供されていた Start/Stop VMs during off-hours と同等の機能を提供しますが、ベースとなる技術が異なる (Automation と Functions) ため、元の使用方法でカスタマイズしている場合には異なる挙動となる場合もあります。

この記事では、Start/Stop VMs v2 のワークフローに着目して、どのような動作になっているかを追ってみましょう。


# 免責事項
Start/Stop VMs v2 のソースコードは非公開ですが、筆者は立場上 (*Microsoft Azure の Support Engineer) 参照可能です。
ただし、この記事に書かれている内容は、ソースコードを参照しておらず、製品の動作ログなどから確認できる範囲に限定されます。


# Start/Stop VMs during off-hours からの移行
残念ながら移行方法は提供されていません。
既存の設定を元に、Start/Stop VMs v2 で改めて設定が必要です。


# Start/Stop VMs v2 で動作する各関数について
Start/Stop VMs v2 の全体像を示しますが、Logic Apps で指定した VM を直接起動しているわけではなく、処理は小分けされてキューやテーブルを介して Azure Functions により操作が行われます。
![SSVMsV2 overview.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349915/12e4c077-8c1a-8498-6d5d-3913f9d2954a.jpeg)


「VM の起動停止にしては、複雑すぎない？(起動停止スクリプトってそんなに長くないよ)」と思う人もいるかも知れませんが、タスクスケジューラで実行していたような「時間のかかるスクリプトの直列実行」は、Azure Functions を含めたクラウド、現代的なアーキテクチャでの実行はあまり一般的とは言えません (最適な例とは言えませんが [systemd への移行](https://postd.cc/why-systemd/) においても、マルチコア CPU 等の恩恵を受けるためには並列・非同期を使って単位時間あたりの処理数を稼ぐ、全体の処理時間を短くするのが今流です)。
また、関数名を見ると [Durable Functions](https://learn.microsoft.com/ja-jp/azure/azure-functions/durable/durable-functions-overview?tabs=in-process%2Cnodejs-v3%2Cv1-model) の雰囲気もありますが、実際には普通の Azure Functions 関数であり、操作状態の保存などを考慮した [Azure Functions のベストプラクティス](https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-best-practices?tabs=csharp) にも準拠した使い方になっています。

それでは、各処理の流れを追っていきましょう

## LogicApps: Scheduled, Sequenced からの操作
Logic Apps に配置されている、ststv2_vms_Scheduled_start と ststv2_vms_Scheduled_stop はスケジュール設定による起動と停止、ststv2_vms_Sequenced_start と ststv2_vms_Sequenced_stop はタグで指定された順番での起動と停止を行います。
一方で、これらの Logic Apps からの要求は、全て HTTP トリガー関数の Scheduled に POST で発行されます。
![SSVMsV2 schedule.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349915/f7c7f41c-fcaf-0e85-eb51-32655d4578ba.jpeg)



POST 要求では、起動・停止、どのリソースグループか、Sequenced かどうかが指定された本文が POST されており、具体的な以下のような内容が指定されています (ちなみに Logic Apps の要求本文にそのまま書かれています)。

```[ststv2_vms_Scheduled_start]
{
  "Action": "start",
  "EnableClassic": false,
  "RequestScopes": {
    "ResourceGroups": [
      "/subscriptions/11111111-2222-3333-4444-555555555555/resourceGroups/rg1/",
      "/subscriptions/11111111-2222-3333-4444-555555555555/resourceGroups/rg2/"
    ]
  }
}

```

```[ststv2_vms_Sequenced_stop]
{
  "Action": "stop",
  "RequestScopes": {
    "ResourceGroups": [
      "/subscriptions/11111111-2222-3333-4444-555555555555/resourceGroups/rg1/",
      "/subscriptions/11111111-2222-3333-4444-555555555555/resourceGroups/rg2/"
    ]
  },
  "Sequenced": true
}
```

### Scheduled 関数
Scheduled 関数は、LogicApps の ststv2_vms_Scheduled_start, ststv2_vms_Scheduled_stop, ststv2_vms_Sequenced_start, ststv2_vms_Sequenced_stop の各ジョブからの HTTP 要求をトリガーとして実行されます。
この関数は受取った要求を元に起動停止のスケジューリングを行い、orchestration-request キューと requeststoretable テーブルに対して受け付けた要求を記録します。
requeststoretable テーブルには、今回受け付けた要求の詳細と進行状況が記録されます。

orchestration-request キューには、以下のようなメッセージが記録され、ActionType で起動または停止、TargetsOperationReferenceId で requeststoretable テーブルに登録されたリソース情報を探すようになっています。
```[]
{
    "ScenarioType": null,
    "Description": null,
    "Sequenced": false,
    "DependsOn": null,
    "Action": {
        "ActionType": "stop",
        "TargetsOperationReferenceId": "29df5ae0-89b0-4020-8f28-a20a61d95109",
        "ChildAction": null
    }
}
```


### VirtualMachineRequestOrchestrator 関数
VirtualMachineRequestOrchestrator 関数は、orchestration-request キューのイベントをきっかけとして開始されます。

この関数では、リソースグループ単位で渡された要求に対して、具体的な VM リソースを特定し起動停止操作の指示を行っているようです。
execution-request キューに以下のようなメッセージを送っています。

```
{
    "Description": null,
    "ScenarioType": "Scheduled",
    "OperationReferenceId": "29df5ae0-89b0-4020-8f28-a20a61d95109",
    "AzureRequestDetails": {
        "ResponseCode": 0,
        "ReasonPhrase": null,
        "RequestId": null,
        "CorrelationId": null
    },
    "ActionType": "stop",
    "Target": "/subscriptions/d7fd****-****-****-****-****1562/resourceGroups/rg-ssvm-testvms/providers/Microsoft.Compute/virtualMachines/vm-test1"
}
```



### VirtualMachineRequestExecutor 関数
VirtualMachineRequestExecutor 関数は、execution-request キューのイベントをきっかけとして開始されます。

この関数では、メッセージで指定された VM の起動または停止を要求します。Start/Stop VMs v2 の肝となる関数となります。
処理が完了すると、subscriptionrequeststoretable テーブルに結果を記録します。


### 補足
- 起動停止要求時に登録される、実行状況を示す requeststoretable のレコードは、適宜更新されます。
- Sequenced の場合、sequencestart タグで指定された順番で 1 グループ目、2 グループ目…と順番にスケジュールします。
  定期的に VirtualMachineRequestOrchestrator 関数が状況を確認しており、順番にスケジューリングしています。


## LogicApps: AutoStop からの操作

Logic Apps に配置されている、ststv2_vms_AutoStop は VM の CPU 使用率を監視し、閾値以下 (アイドルと見なせるような状態) の場合に VM の停止をおこなします。
この Logic Apps からの要求は HTTP トリガー関数で CreateAutoStopAlertExecutor に POST で発行されます。

POST する内容は以下の通りであり、基本的に停止する VM のリソースグループと停止条件 (CPU 使用率の閾値) を渡しています。
```
{
    "Action": "stop",
    "AutoStop_Condition": "LessThan",
    "AutoStop_Description": "Alert to stop the VM if the CPU % exceed the threshold",
    "AutoStop_Frequency": "00:05:00",
    "AutoStop_MetricName": "Percentage CPU",
    "AutoStop_Severity": "2",
    "AutoStop_Threshold": "5",
    "AutoStop_TimeAggregationOperator": "Average",
    "AutoStop_TimeWindow": "06:00:00",
    "EnableClassic": false,
    "RequestScopes": {
        "ResourceGroups": [
            "/subscriptions/d7fd****-****-****-****-****1562/resourceGroups/rg-ssvm-testvms/"
        ]
    }
}
```

### AutoStop 関数
AutoStop 関数は、Logic Apps の AutoStop ジョブをきっかけとして開始されます。
受取った HTTP 要求を元に、具体的な VM リソース名を取得して、create-alert-request キューにメッセージを送っています。

```
{
    "AutoStop_Enabled": true,
    "AutoStop_MetricName": "Percentage CPU",
    "AutoStop_Condition": "LessThan",
    "AutoStop_Description": "Alert to stop the VM if the CPU % exceed the threshold",
    "AutoStop_Frequency": "00:05:00",
    "AutoStop_Severity": "2",
    "AutoStop_Threshold": "5",
    "AutoStop_TimeAggregationOperator": "Average",
    "AutoStop_TimeWindow": "06:00:00",
    "AutoStop_ActionGroupName": "sierra47-vm-test1",
    "Target": {
        "Tags": null,
        "Id": "/subscriptions/d7fd****-****-****-****-****1562/resourceGroups/rg-ssvm-testvms/providers/Microsoft.Compute/virtualMachines/vm-test1",
        "Type": "Microsoft.Compute/virtualMachines",
        "ClassicDomainName": null,
        "Location": "eastus"
    },
    "ClassicDomainGroup": null
}
```


### CreateAutoStopAlertExecutor 関数
CreateAutoStopAlertExecutor 関数は、create-alert-request キューのイベントをきっかけとして開始されます。

この関数では、指定された VM リソースについて、CPU の使用率の確認と停止操作が行われます。
実行結果は requeststoretable テーブルに記録が行われます。




## それ以外 (Azure Functions タイマートリガーからの実行)
### CostAnalyticsFunction 関数、SavingsAnalyticsFunction 関数
CostAnalyticsFunction 関数、SavingsAnalyticsFunction 関数は、タイマートリガーによるスケジュールをきっかけとして開始されます。

ドキュメントには「この関数は、お客様全体の Start/Stop V2 の集計削減額を見積もる際に Microsoft が使用します。 この関数は、Start/Stop V2 の機能には影響しません。」と記載されており、VM の起動停止操作自体には関与しません。

### TriggerAutoUpdate 関数、UpdateStartStopV2 関数
TriggerAutoUpdate 関数は、タイマートリガーによるスケジュールをきっかけとして開始されます。
Cron 式には ```"0 0 0 * * *"``` と設定されており、毎日 0:00 UTC (9:00 JST) に実行されます。

Start/Stop VMs V2 は、VM の起動停止のプログラムを Azure Functions で保持しており、プログラム自体も不定期で更新されます。
この関数は、新しいプログラムが公開された際に、自分自身を更新するための動作を行います。

実行ログを見ると、ARM テンプレートからのデプロイが試みられており、Azure Functions リソースの設定の更新と、以下の URI で公開されているアプリケーションのデプロイ (更新) が行われます。
- https://startstopv2prod.blob.core.windows.net/artifacts/StartStopV2.zip
```
2023/8/29 16:53:21.270  Executing 'UpdateStartStopV2' (Reason='New queue message detected on 'auto-update-request-queue'.', Id=69289d43-c58c-4ca9-85a5-1b160bb878fe)
2023/8/29 16:53:21.274  Trigger Details: MessageId: 5aa9e746-b788-4475-a151-4b4f0d578210, DequeueCount: 1, InsertedOn: 2023-08-29T16:51:21.000+00:00
2023/8/29 16:53:21.299  Attempting update of Start Stop V2.
2023/8/29 16:53:21.299  Downloading latest ARM template and creating deployment parameters.
2023/8/29 16:53:21.317  Start processing HTTP request GET https://startstopv2prod.blob.core.windows.net/artifacts/ssv2autoupdate.json
2023/8/29 16:53:21.334  Sending HTTP request GET https://startstopv2prod.blob.core.windows.net/artifacts/ssv2autoupdate.json
2023/8/29 16:53:21.937  Received HTTP response headers after 597.7678ms - 200
2023/8/29 16:53:21.944  End processing HTTP request after 632.0112ms - 200
2023/8/29 16:53:21.952  Version 1.1.20221110.1 found in latest ARM template.
2023/8/29 16:53:21.952  Version 1.1.20221110.1 found in current installation.
2023/8/29 16:53:21.956  Start processing HTTP request GET https://startstopv2prod.blob.core.windows.net/artifacts/AutoUpdateRegionsGA.json
2023/8/29 16:53:21.956  Sending HTTP request GET https://startstopv2prod.blob.core.windows.net/artifacts/AutoUpdateRegionsGA.json
2023/8/29 16:53:22.099  Received HTTP response headers after 142.8003ms - 200
2023/8/29 16:53:22.100  End processing HTTP request after 143.5553ms - 200
2023/8/29 16:53:22.106  Your installation is enrolled to receive automatic updates and your version is currently up to date for your deployment ring. No action will be taken at this time.
2023/8/29 16:53:22.124  Executed 'UpdateStartStopV2' (Succeeded, Id=69289d43-c58c-4ca9-85a5-1b160bb878fe, Duration=1111ms)
```


### HeartBeatAvailabilityTest 関数
HeartBeatAvailabilityTest 関数は、タイマートリガーによるスケジュールをきっかけとして開始されます。
Cron 式には ```"0 */5 * * * *"``` と設定されており、5 分毎に実行されます。

用途は不明ですが、ドキュメントには「この関数は、プライマリ HTTP 関数の可用性を監視します。」とあり、自分自身の動作についての監視を行っているように見えます。




# Start/Stop VMs V2 を使用する上での注意点
実際使用する上での注意点がありますので、ご参考まで。

- VM に対する起動・停止操作は 1 度しか実行されない
Start/Stop VMs V2 はキューなどを使用して、処理の永続化を行っています。ただし、これは Azure Functions での処理が中断された場合に再開するために用意されているものであり、失敗した処理のリトライでは使用されません。
つまり、動作としては「コマンドを列挙したバッチをタスクスケジューラ等から実行する」場合と同じです。
<br/>

- 操作時にタイムアウトする可能性がある
ポータルなどで VM の操作を行った場合に、ごく希に VM の起動失敗が起きます。
原因は多種多様であり、ポータルの応答が無い場合もあれば、ストレージのマウント失敗、OS のクラッシュなど多岐にわたります。
Start/Stop VMs V2 は、指定されたリソースグループに所属する VM に対して、起動または停止の要求コマンドを実行するだけであり、結果 (VM の起動成功) については確認していません。
<br/>

- VM の数が多すぎると失敗する可能性がある
Start/Stop VMs V2 は個々の VM に対して順番に操作を行っています。
そのため、大量の要求を同時に行った場合、Azure REST API から要求が多すぎる (HTTP ステータスコード 429) として処理が拒否される可能性が出てきます。
特に Sequenced を使用して、順番で VM を起動している場合、1 グループ目、2 グループ目、…と順番に実行する (先のグループの完了を待つ) 都合上、全体の実行時間が長くなる場合があります。結果として、nグループ目以降ののスケジューリングが行われない (スケジューラの実行時間が長すぎてタイムアウトする) 場合があります。
検証や運用してみて上手く行かないようであれば、分割も検討ください。
<br/>

- 操作結果の確認は別途実施する
上記の通り、Start/Stop VMs V2 は、Infrastructure as Code の文脈で言われるような、「VM の状態を管理する」ものではなく、単純に「起動または停止のコマンドを実行する」仕組みです。
結果として、不幸にも起動要求をしたのに起動しなかった、失敗したと言ったことが起きる場合があります。
「ある時間までに起動していないと困る」と言う場合は、Start/Stop VMs V2 とは別に、VM が起動しているか、VM のアプリケーションの動作状況について確認する手段を別途用意し、必要に応じてリカバリーする手段を考える必要があります。

# まとめ
Start/Stop VMs V2 がどういった動きをするのか (たくさんの関数の相関関係) という記事がなかったので作ってみましたが、いかがでしたでしょうか。
情報量が膨大であり、資料の収集は夏休みにやったのに、整理にすっかり時間がかかり、公開がクリスマスシーンズンになってしまいました。

Start/Stop VMs V2 を使用されるのは、VM の管理者の方がほとんどでアプリケーション開発に携わる方は少ないのではないかと思いますが、どうして失敗するのか、この関数って何？という疑問の解決に役立てばと思います。
また、Azure Functions を使用される方も処理の分割やスケジューリングという観点で、アーキテクチャ検討の参考になれば幸いです (数少ない、マイクロソフトが公式に公開しているサーバレスアーキテクチャ/プログラムの一つではあると思うので)。