Table Storageに溜まり続けるWindowsイベントログのクリーニング

# この記事の内容

- [Azure VM（Windows）の診断ログ保管先にまつわる小噺](https://qiita.com/qNeumann/items/59e200ec07d412d965cc#azure-vmwindows%E3%81%AE%E8%A8%BA%E6%96%AD%E3%83%AD%E3%82%B0%E4%BF%9D%E7%AE%A1%E5%85%88%E3%81%AB%E3%81%BE%E3%81%A4%E3%82%8F%E3%82%8B%E5%B0%8F%E5%99%BA)
- [Automation・Runbook（Powershell）による Table クリーニング処理の実装例](https://qiita.com/qNeumann/items/59e200ec07d412d965cc#automationrunbookpowershell%E3%81%AB%E3%82%88%E3%82%8B-table-%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%8B%E3%83%B3%E3%82%B0%E5%87%A6%E7%90%86%E3%81%AE%E5%AE%9F%E8%A3%85%E4%BE%8B)

`※ またしても、Linuxサーバーの話は出てきません。`

# Azure VM（Windows）の診断ログ保管先にまつわる小噺

### VM診断ログの保管先は？

クラウド環境で Windows Server を VM として構築する場合、やはり Microsoft 社の Azure は、Windows Server との親和性が高いような気がします。ポータル画面から、更新プログラムの管理や、インベントリなどの構成管理を行えるあたり、もはや Windows のためのクラウドサービスなのではないかと思ってしまう程。（流石に言い過ぎか）

<img width="688" alt="01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97680/3d3c7e6b-fb72-41f1-2e23-3542c03cbe50.png">


で、Windows Server を構築した場合、時々話題になるのが**イベントログの保管**です。特に、金融系システムなどの高いセキュリティ要件が求められる場合、ログの保管先をどうするか、何年間保管するのか、ログのクリーニングはどうするのか、などで頭を抱えるシーンも多いのではないでしょうか？少なくとも私は頭を抱えて身悶えしておりました。

Azure VM のログ保管先を検討する場合、以下の選択肢が存在します。

　1. VM のローカルディスクに保管
　2. Log Analytics に保管
　3. ストレージアカウントに保管

**1.** の方法が一番シンプルですが、マネージドディスクはストレージアカウント（Blob ストレージや Table ストレージなど）と比較するとどうしても割高になってしまいます。少しでも安いストレージを使いたいのが人の性というものではないでしょうか。また、イベントログのプロパティでは「サイズ上限に達した場合に削除」というコントロールのみで、日時指定のクリーニングは処理を作りこまないとできません。（と言っても、今回の記事も処理を作り込んでるんだけど）

参考：[Managed Disks の価格](https://azure.microsoft.com/ja-jp/pricing/details/managed-disks/)

**2.** の Log Analytics でもログデータの保管は可能ですが、Log Analytics のデータ保有期間は 30～730 日であり、2年を超えるログの長期保管先としては不向きです。また、費用は **1.** よりも割高で、まったくお得感はありません。

参考：[Azure Monitor（Log Analytics ワークスペース） の価格](https://azure.microsoft.com/ja-jp/pricing/details/monitor/)

というわけで、**3.** のストレージアカウントへの保管が妥当だろう、という帰結になります。というか、意図的にそういう帰結になる誘導めいた構成にしました、すみません。Azureは各種サービスの関係性が初学者には大変分かりにくいため、ストレージアカウントと各種ストレージサービスの構成を下図にまとめます。

<img width="380" alt="02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97680/3a2e745f-cd2f-9d99-1ba7-6a517e7ea481.png">


とりわけ、今回の記事では「Blob ストレージ」と「Table ストレージ」に絞った内容とさせていただきます。というのは、Azure VM の診断設定を有効化した場合、ログの転送先が上記２種のストレージだからです。念のため、ストレージの料金のリンクも貼っておきます。

参考：[ブロック BLOB の価格](https://azure.microsoft.com/ja-jp/pricing/details/storage/blobs/)
参考：[Azure Table Storage の価格](
https://azure.microsoft.com/ja-jp/pricing/details/storage/tables/)


### ストレージアカウントへの転送先

VM の診断設定を有効化すると、指定したストレージアカウントにログが定期的に転送されます。すべてBlobストレージに転送されているのかなと思いきや、実は既定 [^1] では、下表のように**転送先はログの種類により異なります。**（当然ですが、診断設定により転送されるログ種類も変動します）

| ログの種類 | 転送先 | テーブル名 |
|:-----------|:------:|:----------|
| インフラログ | Table | WADDiagnosticInfrastructureLogsTable |
| 拡張診断ログ | Table | WADMetricsExtensionLogsTable |
| 診断メトリック | Table | WADMetrics* |
| パフォーマンスカウンター | Table | WADPerformanceCountersTable |
| イベントログ | Table | WADWindowsEventLogsTable |
| IISログ | Blob | - |
| クラッシュダンプ | Blob | - |
| その他独自ログ | Blob | - |

参考：[データストレージ | Windows Azure Diagnostics 拡張機能 (WAD) のインストールと構成](https://docs.microsoft.com/ja-jp/azure/azure-monitor/platform/diagnostics-extension-windows-install#data-storage)

まぁ、転送先が Blob だろうが Table だろうが、間違いなく安価な永続ストレージに転送されているのは確かで、これで万事解決だと安堵できるのも束の間、ここで困ったことが発生します。それは、Table ストレージのライフサイクル管理です。


### Table ストレージのログローテ

Blob ストレージであれば、Azure ポータルのライフサイクル管理により、〇日後にアーカイブ層に移動させるとか、〇日後に削除するとか、そういった設定が可能です。

<img width="563" alt="04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97680/be20e002-fc86-6bfd-93ef-78618ed88f59.png">

ところが、**Table ストレージにはこういった仕組みが存在しません。**そのため、Table ストレージに対するライフサイクル管理を行うためには、ログローテの処理を実装する必要があります。処理の実装は、Azure Automation での処理が個人的には一番ラクで手っ取り早いと思います。

### Azure Automation と Runbook

<img width="451" alt="05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97680/8be8b49b-b6b1-e71a-dc03-7d492bbc87b0.png">

Azure Automation は、Azure 環境での処理をスケジューリングして自動化できる優れたサービスです。冒頭で少し触れた更新プログラムの管理や、VMの自動起動・自動停止などの処理も自動化できてしまいます。ふと、「Functionsとの違いってなんや？」という疑問が湧いたりしたので念のため整理しておくと、

> Functions　　…　サーバレスコンピューティング
> Automation　…　運用の自動化

なので、まぁ思想も立場で全然違うわけですね。今回のようなバッチ処理であれば Automation で実装すべきでしょう。（AWS の場合は Lambda と Cloudwatch でやったりしますが）

Automation では、Runbookという処理の器を作成し、その中に運用上必要な処理を記述していきます。Runbookの種類としては、

- Powershell
- Python
- グラフィカル（GUIエディター）

のみが現在ではサポートされております。GUI に慣れない、かつ Powershell が Azure との親和性が高い（AWS ならもちろん Python 一択ですけどね…）ので、今回はすべてPowewrshell で実装することにします。


# Automation・Runbook（Powershell）による Table クリーニング処理の実装例

さて、長い前置きがようやく終わったので、そろそろ本題に入りましょう。

くどくどした解説は飛ばして、とりあえず処理の全量をドカンと貼ります。ポイント的な部分はコードの中のコメントに記載しているので、興味のある方はそちらをご覧ください。例外処理や変数名など、適当な部分が多いのは否めないので、お気をつけ遊ばせ。

```powershell:Sweep-WinVMLogsInTable
# 変数の宣言
# Automation共有リソースから変数を取り出すと尚良い。
$rgn = (対象リソースグループ名)
$san = (対象ストレージアカウント名)
$log = 5    #ログの保存年数

# Automationアカウントと同時に作成される証明書でAzureに接続
# Automationチュートリアルに記載の接続処理をそのまま転用。
# ここではAutomation共有リソースから証明書を取得している。
# ただし、証明書の期限は1年間なので、都度更新が必要となる。
# パスワード無期限のバッチユーザーを作成して、
# 資格情報で接続する方がメンテ不要なのでオススメ。
$con = "AzureRunAsConnection"
$spc = Get-AutomationConnection -Name $con
$nul = Add-AzureRmAccount `
    -ServicePrincipal `
    -TenantId $spc.TenantId `
    -ApplicationId $spc.ApplicationId `
    -CertificateThumbprint $spc.CertificateThumbprint 

# システム時刻の取得（JSTに変換してログ保全終了日まで遡及）
# なぜかSet-TimeZoneできなかったので、力技で+9:00する。
# AddYearsを、AddDaysやAddMonthsに変えて保全単位を変更できる。
$date = ((Get-Date).AddHours(+9)).AddYears(-1 * $log)
Write-Output ("====================================")
Write-Output ("Start  : " + (Get-Date).AddHours(+9))
Write-Output ("====================================")
Write-Output ("DelPoint : " + $date.ToString("yyyy-MM-ddTHH:mm:ss"))

# Tableストレージに接続
$sto = Get-AzureRmStorageAccount -ResourceGroupName $rgn -Name $san
$tbls = Get-AzureStorageTable -Context $sto.Context

# ストレージ内のテーブルごとに処理
foreach ($tbl in $tbls){

    # テーブル名取得
    $cld = $tbl.CloudTable
    Write-Output ("------------------------------------")
    Write-Output ("Table  : " + $cld)

    # Windows Azure Diagnostics拡張機能(WAD)以外はスキップ
    if ($cld.ToString().Substring(0, 3) -ne 'WAD') {
        Write-Output ("Skip the deletion process.")
        continue
    }

    try {
        # 指定日時よりタイムスタンプが古いエンティティを指定
        # フィルターに渡すDataTimeの文字列はODataの形式で指定する。
        $queryDel = New-Object Microsoft.WindowsAzure.Storage.Table.TableQuery
        $queryDel.FilterString = "Timestamp le datetime'{0}'" -F $date.ToString("yyyy-MM-ddTHH:mm:ss")

        # 該当するエンティティを削除
        # $cld.ExecuteQuery($queryDel)の結果をそのままDeleteに渡したいが、
        # クエリの実行結果はDynamicTableEntity型で返る。それに対し、
        # DeleteメソッドはITableEntity型のオブジェクトしか受け取れない。
        # そのため、foreachでエンティティを1つずつフェッチして処理する。
        $del = 0
        foreach ($ent in $cld.ExecuteQuery($queryDel)) {
            $nul = $cld.Execute([Microsoft.WindowsAzure.Storage.Table.TableOperation]::Delete($ent))
            $del = $del + 1
        }
        Write-Output ("Delete : " + $del)

        # PartitionKeyのみを選択
        # カラムを絞ることで処理速度の向上が期待できる。
        $list = New-Object System.Collections.Generic.List[string] 
        $list.Add("PartitionKey")
        $querySel = New-Object Microsoft.WindowsAzure.Storage.Table.TableQuery
        $querySel.SelectColumns = $list
        
        # 残エンティティの件数取得
        # Measureオブジェクトにパイプで渡してCountプロパティを取得する。
        $ents = $cld.ExecuteQuery($querySel)
        $cnt = ($ents | measure).Count
        Write-Output ("Remain : " + $cnt)

    } catch {
        # エラー発生時はエラー内容を出力
        # Powershellでは、発生した最新の例外は$error[0]に格納される。
        Write-Output ("An error occurred while processing " + $cld)
        Write-Output ("Error  : " + $error[0])
    }

}

Write-Output ("====================================")
Write-Output ("Finish : " + (Get-Date).AddHours(+9))
Write-Output ("====================================")

```

「5年前」じゃなくて「1日前」に設定を変更して実行すると、結果は下記の通りになりました。

```
====================================
Start  : 05/09/2020 15:12:02
====================================
DelPoint : 2020-05-08T15:12:02
------------------------------------
Table  : LinuxSyslogVer2v0
Skip the deletion process.
------------------------------------
Table  : SchemasTable
Skip the deletion process.
------------------------------------
Table  : WADDiagnosticInfrastructureLogsTable
Delete : 0
Remain : 0
------------------------------------
Table  : WADMetricsExtensionLogsTable
Delete : 3777
Remain : 6966
------------------------------------
Table  : WADMetricsPT1HP10DV2S20200424
Delete : 784
Remain : 0
------------------------------------
Table  : WADMetricsPT1HP10DV2S20200504
Delete : 2024
Remain : 248
------------------------------------
Table  : WADMetricsPT1MP10DV2S20200424
Delete : 38866
Remain : 0
------------------------------------
Table  : WADMetricsPT1MP10DV2S20200504
Delete : 121168
Remain : 20660
------------------------------------
Table  : WADPerformanceCountersTable
Delete : 62968
Remain : 10330
------------------------------------
Table  : WADWindowsEventLogsTable
Delete : 4269
Remain : 401
====================================
Finish : 05/09/2020 15:32:30
====================================
```

233,856件のログを削除するのに、およそ20分かかりましたが、思ったより処理は早かったので何よりです。**Automation プロセスの最長実行時間は3時間**という縛りがありますが、今回は余裕で収まる結果になりました。

参考：[Automation の制限 | Azure サブスクリプションとサービスの制限、クォータ、制約](https://docs.microsoft.com/ja-jp/azure/azure-resource-manager/management/azure-subscription-service-limits#automation-limits)

ただし、年単位で蓄積されたログをクリーニングする場合の処理時間は、残念ながら未知数です。削除処理時間はスケジュール実行間隔次第なので、日単位でスケジュール実行するなら上記サンプルと大きく変わらないと思いますが、全量が多いと当然その分のスキャンに時間がかかることは容易に想像できます。


# 終わりに

Table ストレージのクリーニングを Automation で行うにあたり、StackOverFlow など参考になりそうなサイトをあちこち駆け回ったのですが、Powershell による処理の全量が記載されているものが見つからなかったため、重すぎる腰を上げて筆を取りました。

余談ですが、本投稿に記載した Powershell の処理は、AzureRM ベースのコマンドレットを使用しておりますが、現在は Az モジュールが最新であり、Microsoft 社も Az モジュールへの移行を促しているようです。一応、Automation でも Az モジュールは使用可能らしいですが、面倒くさそうなので今回は見送りました。興味がある方はお試しあれ。

[Azure Automation での Azure PowerShell Az モジュールのサポート](https://azure.microsoft.com/ja-jp/updates/azure-automation-supports-the-azure-powershell-az-module/)
[Azure Automation の Azure PowerShell モジュールを更新する](https://docs.microsoft.com/ja-jp/azure/automation/automation-update-azure-modules)

気が向いたら、アカウント作って Runbook ギャラリーでも公開しようと思います。
また、あくまで執筆時点の情報なので、古くなった際はご容赦いただきたく。

記載の誤り等あればぜひご指摘をお願いします。


## Special Thanks（すごく参考になったサイト）

[Windows Azure Diagnostics ログ出力 - Azureの小ネタ （改）](https://statemachine.hatenablog.com/entry/20100726/1280155947)
[Azure Storage TableをTimeStampを使ってクエリする - YOMON8.NET](https://yomon.hatenablog.com/entry/2015/02/23/125326)
[PowershellでAzureTableStorageのEntityを削除したい | teratail](https://teratail.com/questions/42534)
[Get the Azure table Row count | stackoverflow](https://stackoverflow.com/questions/28667601/get-the-azure-table-row-count)


[^1]: Windows Diagnostics 拡張機能の「PublicConfig 要素」とやらで、転送先をTable か Blob か指定できる、と [Microsoft 社公式ドキュメント](https://docs.microsoft.com/ja-jp/azure/azure-monitor/platform/diagnostics-extension-schema-windows#publicconfig-element)には記載されているので、すべて Blob に指定できるなら、この記事の存在価値はない…