Azure 仮想マシンのサイズと稼働時間をCSVファイルに出力する

#はじめに
　[前回の記事](https://qiita.com/y447093040/items/07ec223d0e7c1eb3b777)ではAzure Log Analyticsの検索クエリをREST APIを用いて実行する方法について記載しました。今回はその応用例として、Azure 仮想マシンの稼働時間とサイズを定期的に出力する方法について検証した結果を記載します。

#基本方針
　Azure仮想マシンの稼働時間はLog Analytics workspaceに収集されるHeartbeatの件数で近似することができます(HeartBeatは1分毎に取得される為)。従ってLog Analyticsの検索クエリを用いて確認することができます。
　一方でAzure仮想マシンのサイズは仮想マシンのプロパティ情報の一つであるためAzure PowerShellの「Get-AzVM」コマンドレットで取得することが出来ます(※Azure PowerShell はAz モジュールを前提)。
　そこで、これらの取得結果を1つのcsvファイルに出力するPowerShellスクリプトを用意し、このスクリプトを記載したAzure AutomationのRunbookを作成して定期的に自動実行させます。

#事前準備等
　前回の記事に記載した手順が完了しておりLog Analytics APIを用いてAzure Log Analyticsの検索クエリを実行する準備ができていることを前提とします。出力結果を保存する Azure Blob StorageとPowerShellスクリプトを自動実行するためのAzure Automation アカウントが作成済みであることも前提とします。また、Azure Blob StorageやAzure Automationの基本的な設定は本記事では割愛し、Powershell スクリプトの内容を中心に記載します。

#Runbookに記載するPowerShell スクリプトについて

スクリプトは以下の流れで記載します。
(1)Azureへログイン
(2)パラメータ設定
(3)認証Tokenの取得
(4)検索クエリ
(5)REST APIの呼び出し(検索クエリの実行)
(6)検索クエリ結果の整形
(7)Heartbeat数の抽出
(8)仮想マシン一覧の取得
(9)集計
(10)ファイルの出力

※(3)、(5)、(6)については前回の記事と同様であるため詳細な説明は割愛します。

(1)Azureへログイン
Azure Automationを使用する時の作法として以下の内容を記載します。

```
$connectionName = "AzureRunAsConnection"
$servicePrincipalConnection=Get-AutomationConnection -Name $connectionName         

Login-AzAccount `
        -ServicePrincipal `
        -TenantId $servicePrincipalConnection.TenantId `
        -ApplicationId $servicePrincipalConnection.ApplicationId `
        -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint 
```

(2)パラメータ設定
Log Analytics APIを使用する為に必要なパラメータについては前回の記事と同様ですが、それに加えてファイル出力先であるストレージアカウントのパラメータが必要です。

```
$tenantId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
$clientId       = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"      
$clientSecret   = "********************************"  

$subscriptionId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
$workspaceRgName="DefaultResourceGroup-EUS"
$workspacename="DefaultWorkspace-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-EUS"

$strageAccountRgName "<ストレージアカウントのリソースグループ名>"
$strageAccountName "<ストレージアカウント名>"
$containerName = "<コンテナー名>"
```

(3)認証Tokenの取得

```
$loginURL       = "https://login.microsoftonline.com/$tenantId/oauth2/token"
$resource       = "https://management.azure.com/"         
$body           = @{grant_type="client_credentials";resource=$resource;client_id=$clientId;client_secret=$clientSecret}
$oauth          = Invoke-RestMethod -Method Post -Uri $loginURL -Body $body
$headerParams = @{"Authorization" = "Bearer " + $oauth.access_token} 
```

(4)検索クエリ
週末に定期実行するようにAzure Automationのスケジュールを作成する前提として過去7日分のhearbeat件数を集計する検索クエリを設定します。

```
$body = @{query = 'Heartbeat | where TimeGenerated > ago(7d) '}| ConvertTo-Json
```

(5)REST APIの呼び出し(検索クエリの実行)

```
$url="https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$workspaceRgName/providers/Microsoft.OperationalInsights/workspaces/$workspacename/api/query?api-version=2017-01-01-preview"
$result = Invoke-RestMethod -UseBasicParsing -Headers $headerParams -Uri $url -Method Post -Body $body -ContentType "application/json"
```

(6)検索クエリ結果の整形

```
$headerRow = $null
$headerRow = $result.tables[0].columns | Select-Object ColumnName
$columnsCount = $headerRow.Count
$logData = @()
foreach ($row in $result.tables[0].rows) {
    $data = new-object PSObject
    for ($i = 0; $i -lt $columnsCount; $i++) {
        $data | add-member -membertype NoteProperty -name $headerRow[$i].ColumnName -value $row[$i]
    }
    $logData += $data
    $data = $null
}
```

(7)Heartbeat数の抽出
以下のようにして(5)の\$resultに格納されたデータをComputer名で集計しComputer名とHeartbeatの件数を取得します。

```
$hbCount = $logData | group Computer | Select-Object Name,Count
```

(8)仮想マシン一覧の取得
以下のコマンドレット用いて仮想マシンの一覧を取得します。
※取得対象仮想マシンを限定する場合はリソースグループ名等のパラメーターも設定します。

```
$VMs = Get-AzVM
```

(9)集計
\$hbcountと\$VMsに格納された値をVM名が一致するようにして結合します。その際ユーザー定義オブジェクトを使用します。ユーザー定義オブジェクトの使用については以下を参考にしました。
・[PowerShellを使ってユーザー定義オブジェクトを作成](https://qiita.com/sukakako/items/b9d4c10b75cf7c2fe965)

また、\$VMsに格納された仮想マシンのプロパティについては、仮想マシン名・リソースグループ名・VMサイズの3つのみを出力することとします。それぞれ以下のプロパティとして設定されています。
・仮想マシン名：Name
・リソースグループ名:ResourceGroupName
・VMサイズ:HardwareProfile.VmSize

```
#配列(ユーザー定義オブジェクト格納先)
$agg = @()

#ユーザー定義オブジェクトの初期値設定
 #プロパティは項番、VM名、リソースグループ名、VMサイズ、稼働時間の5つとする)
$p = @{index=-1; name=""; rgname=""; size="";minute=0}

#ユーザー定義オブジェクトの作成
for($i=0; $i -lt $VMs.Count; $i++){
$agg += New-Object PSObject -Property $p | Select-Object index, name, rgname, size, minute

#ユーザー定義オブジェクトの要素設定
 #項番
$agg[$i].index = $i+1
 #VM名
$agg[$i].name  = $VMs[$i].Name
 #リソースグループ名
$agg[$i].rgname  = $VMs[$i].ResourceGroupName
 #VMサイズ
$agg[$i].size  = $VMs[$i].HardwareProfile.VmSize
 #稼働時間
 for ($j=0; $j -lt $VMs.Count; $j++){
  #$VMsのNameと$hbCountのNameが一致する$hbCountの値を検索し要素追加
  if($agg[$i].name -eq $hbCount[$j].Name){
  $agg[$i].minute   = $hbCount[$j].Count
  break 
  }else{
  }
 }
}
```

(10)ファイルの出力
最後にCSVファイルとして出力しBlob Storageへ保存します。その際に現在時刻をファイル名に含めます。

```
#現在時刻の取得(日本時間に合わせるため9時間加算)
$filename = (Get-Date).AddHours(9).ToString("yyyyMMddHHmmss")

#Azure Automationのローカル環境へCSVファイルとして出力
$agg | export-csv .\agg.csv -NoTypeInformation

# blob Storageへのアップロード
$storageAccount = Get-AzStorageAccount -ResourceGroupName $strageAccountRgName -Name $strageAccountName
$ctx = $storageAccount.Context
Set-AzStorageBlobContent -File ".\agg.csv" -Container $containerName -Blob "agg_$filename.csv" -Context $ctx -Force
```

※改めて、スクリプト全体を以下に記載します。

```
##Azure へログイン
$connectionName = "AzureRunAsConnection"
$servicePrincipalConnection=Get-AutomationConnection -Name $connectionName         

Login-AzAccount `
        -ServicePrincipal `
        -TenantId $servicePrincipalConnection.TenantId `
        -ApplicationId $servicePrincipalConnection.ApplicationId `
        -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint 

#パラメータ設定
$tenantId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
$clientId       = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"      
$clientSecret   = "********************************"  

$subscriptionId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
$workspaceRgName="DefaultResourceGroup-EUS"
$workspacename="DefaultWorkspace-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-EUS"

$strageAccountRgName = "test1"
$strageAccountName = "teststorage124790127830"
$containerName = "container01"

#Tokenの取得
$loginURL       = "https://login.microsoftonline.com/$tenantId/oauth2/token"
$resource       = "https://management.azure.com/"         
$body           = @{grant_type="client_credentials";resource=$resource;client_id=$clientId;client_secret=$clientSecret}
$oauth          = Invoke-RestMethod -Method Post -Uri $loginURL -Body $body
$headerParams = @{"Authorization" = "Bearer " + $oauth.access_token} 

#検索クエリ
$body = @{query = 'Heartbeat | where TimeGenerated > ago(7d) '}| ConvertTo-Json

#APIの呼び出し(検索クエリの実行)
$url="https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$workspaceRgName/providers/Microsoft.OperationalInsights/workspaces/$workspacename/api/query?api-version=2017-01-01-preview"
$result = Invoke-RestMethod -UseBasicParsing -Headers $headerParams -Uri $url -Method Post -Body $body -ContentType "application/json"

#検索クエリ結果の整形
$headerRow = $null
$headerRow = $result.tables[0].columns | Select-Object ColumnName
$columnsCount = $headerRow.Count
$logData = @()
foreach ($row in $result.tables[0].rows) {
    $data = new-object PSObject
    for ($i = 0; $i -lt $columnsCount; $i++) {
        $data | add-member -membertype NoteProperty -name $headerRow[$i].ColumnName -value $row[$i]
    }
    $logData += $data
    $data = $null
}

#Heartbeat数の抽出
$hbCount = $logData | group Computer | Select-Object Name,Count

#VM一覧の取得
$VMs = Get-AzVM

#集計
#配列(ユーザー定義オブジェクト格納先)
$agg = @()

#ユーザー定義オブジェクトの初期値設定
 #プロパティは項番、VM名、リソースグループ名、VMサイズ、稼働時間の5つとする)
$p = @{index=-1; name=""; rgname=""; size="";minute=0}

#ユーザー定義オブジェクトの作成
for($i=0; $i -lt $VMs.Count; $i++){
$agg += New-Object PSObject -Property $p | Select-Object index, name, rgname, size, minute

#ユーザー定義オブジェクトの要素設定
 #項番
$agg[$i].index = $i+1
 #VM名
$agg[$i].name  = $VMs[$i].Name
 #リソースグループ名
$agg[$i].rgname  = $VMs[$i].ResourceGroupName
 #VMサイズ
$agg[$i].size  = $VMs[$i].HardwareProfile.VmSize
 #稼働時間
 for ($j=0; $j -lt $VMs.Count; $j++){
  #$VMsのNameと$hbCountのNameが一致する$hbCountの値を検索し要素追加
  if($agg[$i].name -eq $hbCount[$j].Name){
  $agg[$i].minute   = $hbCount[$j].Count
  break 
  }else{
  }
 }
}

#ファイルの出力
#現在時刻の取得
$filename = (Get-Date).AddHours(9).ToString("yyyyMMddHHmmss")

#Azure Automationのローカル環境へCSVファイルとして出力
$agg | export-csv .\agg.csv -NoTypeInformation

# blob Storageへのアップロード
$storageAccount = Get-AzStorageAccount -ResourceGroupName $strageAccountRgName -Name $strageAccountName
$ctx = $storageAccount.Context
Set-AzStorageBlobContent -File ".\agg.csv" -Container $containerName -Blob "agg_$filename.csv" -Context $ctx -Force
```

#動作確認
　Azure Automationのスケジュールを設定してスクリプトを実行させ、指定したストレージアカウントのコンテナーにcsvファイルが保存されていることを確認します。
　※以下はAzure Automationのテストウィンドウで当該Runbookを実行した時の画面です。csvファイルが保存されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531201/d09d647d-4fdd-9048-7944-8e0289aeebc0.png)
　
　併せてcsvファイルの内容が想定通りであることを確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531201/4f514ce2-2487-ac56-c1da-2708770aa9e7.png)
※Log Analyticsに接続されていない仮想マシンはHeartbeatが収集されないので、実際には起動していたとしても「minute」の値は初期値として設定した「0」です。

#参考情報
・[PowerShellを使ってユーザー定義オブジェクトを作成](https://qiita.com/sukakako/items/b9d4c10b75cf7c2fe965)