【Azure】Log Analytics単体では不可能なクエリをPowerShellで補完するTech TIPS

Azureの多機能なログ分析サービス「Log Analytics」でも、サポートされていないクエリや機能は存在する。そのような場合に「Azure PowerShell」のスクリプトを組んで代替あるいは補完する方法について説明する。

» 2024年01月31日 05時00分 公開
[島田広道デジタルアドバンテージ]
「Tech TIPS」のインデックス

連載目次

Azure Log Analyticsでは不可能なクエリをPowerShellで補完

対象:Azure Log Analytics、Azure PowerShell


 Azureのログ分析サービス「Log Analytics」は多機能で、活用できればさまざまな分析ができる便利なサービスだ。

 ただ、Log Analyticsは万能ではなく、サポートされないクエリや機能もある。実例を挙げると、IPアドレスからDNSの逆引きによってホスト名を得ることは、Log Analytics単体ではできない。ちなみに、この機能がなくて困っているのは筆者だけではないらしく、Log Analyticsへの機能追加要望としてMicrosoftの掲示板に投稿されていたのを見かけたことがある。

 このような場合、別のサービスやツールで代替あるいは補完せざるを得ない。そうした選択肢の中から、本Tech TIPSではPowerShellで補完する方法の一例を紹介したい。具体的には、Log Analyticsへのクエリを実行して集計結果を得て、そこからDNSの逆引きもして集計結果にまとめる、という一連の作業をPowerShellスクリプトで実装してみようと思う。

 この作業を行うのはローカルのWindows 10/11搭載PCとする。またPowerShellAzure用モジュールのインストールとセットアップは完了している、さらにLog AnalyticsへのクエリはAzureポータルで実行したことがあるものとして話を進める。

■執筆時の各種ツール/APIのバージョン


Azureにサインインしてテナントとサブスクリプションを選ぶ

 以下では、1つのPowerShellスクリプトを機能ごとに分割して、解説とともに掲載していく。

 まずはAzureにサインインして、テナントとサブスクリプションを選ぶ必要がある。ここでは「Connect-AzAccount」コマンドレットを用いる。

# Azureにサインイン。テナントとサブスクリプションも選択
$tenantId = '<テナントID>'
$subscriptionName = '<サブスクリプション名>'
Connect-AzAccount `
  -Tenant $tenantId `
  -Subscription $subscriptionName > $null

Azureにサインインしてテナントとサブスクリプションを選ぶ

 <テナントID>には、Azureポータルで[サブスクリプション]−[<対象サブスクリプション名>]−[概要]とクリックして、「基本」欄の「親管理グループ」に表示されているIDを指定する。

 上記のコードを実行すると、Webブラウザが起動してサインインのページが表示される。そこで、対象のLog Analyticsに対してクエリを実行できる権限を持つアカウントでサインインを済ませる。するとPowerShellスクリプトの実行が再開されるはずだ。

Log Analyticsに対するクエリを用意する

 サインインしたら、Log Analyticsに対して実行する「クエリ」を用意する。知っての通り、これは「Kusto Query Language(KQL)」で記述する必要がある。KQLの詳細はMicrosoft Learnの「Kusto 照会言語 (KQL) の概要」を参照していただきたい。

 ここではCDNやリバースプロキシの機能を持つ「Azure Front Door」に対するクライアントからのリクエスト数を集計するクエリを例に挙げる。

 以下のリストで、「$query」変数に保存しているがクエリの本体だ。「$numOfClients」「$path」の各変数を展開できるよう、PowerShellのヒアドキュメントのうち、ダブルクオートの方で記述している。

$numOfClients = 10 # 集計するクライアント数
$path = '<パス>' # 集計対象のリクエストのパス
# ↑例:「/path/to/file.ext

# Front Doorで特定パスのリクエスト数を集計するためのクエリ
$query = @"
AzureDiagnostics
| where ResourceProvider == 'MICROSOFT.CDN'
| where Category == 'FrontDoorAccessLog'
| extend path = parse_url(requestUri_s)['Path']
| where path startswith '$path'
| summarize reqCount = count() by clientIP = clientIp_s
| top $numOfClients by reqCount desc
"@

Log Analyticsに対するクエリを用意する
これはFront Doorからクライアントごとのリクエスト数を集計するためのクエリの例だ。

 「where ResourceProvider〜」はリソース(Front Door)、「where Category 〜」はログの種類をそれぞれ絞り込んでいる。

 「extend path 〜」は、クライアントからリクエストされたURLからパスの部分を抽出している。

 このクエリの中核は「summarize」のところで、その上の行までで抽出されたレコード数(すなわちリクエスト数)を「clientIP」(アクセス元のIPアドレス)ごとに集計するというものだ。さらに、次行の「top」で、リクエスト数の多い方から「$numOfClients」の数だけ抽出している。

 クエリの構文を誤って指定すると、「BadRequest」というエラーが返ってくる。例えば、「where path 〜」でパスを絞り込む際、「'$path'」のようにシングルクオートでくくるのを忘れると、このエラーが発生するので注意したい。

Log Analyticsに対してクエリを実行して集計結果を取得する

 次は、前出のクエリをLog Analyticsに対して実行し、その集計結果を変数に保存する。

 クエリの実行には「Invoke-AzOperationalInsightsQuery」コマンドレットを用いる。このとき、「-WorkspaceId」オプションで対象の<Log AnalyticsワークスペースのID>を、「-Query」オプションでクエリのテキスト本体をそれぞれ指定する必要がある。

 測定期間については、「New-TimeSpan」コマンドレットでTimeSpanオブジェクトに変換したものを「-Timespan」オプションで指定している。

 測定期間はクエリ内で指定する方法もある。ただ10分間→1時間→4時間→12時間というように測定期間を変えることはよくある。それをクエリ内に記述すると、クエリが使い回しにくくなりがちなので、筆者はTimeSpanオブジェクトの方を主に利用している。

# クエリに必要なパラメーター
$workspaceId = "<Log AnalyticsワークスペースのID>"
$timeSpanMinutes = 10 # クエリ時の測定(抽出)期間。分単位
$timeSpan = New-TimeSpan -Minute $timeSpanMinutes
# ↑TimeSpanオブジェクトに変換
$queryDatetime = Get-Date # 現在日時を測定日時として保存

# クエリを実行
$queryResults = Invoke-AzOperationalInsightsQuery `
  -WorkspaceId $workspaceId `
  -Query $query `
  -Timespan $timeSpan

$countTemp = $queryResults.Results # 集計結果のみ格納

Log Analyticsに対してクエリを実行して集計結果を取得する

 クエリの結果は、Invoke-AzOperationalInsightsQueryの戻り値の「Results」プロパティに格納されている。これを「ForEach-Object」コマンドレットに渡せば、簡単に抽出や加工ができる(実際の例は後述)。

集計したIPアドレスからホスト名を取得しつつ結果を1つにまとめる

 次は、クエリで得られたクライアントIPアドレスをDNSの逆引きにかけてホスト名を取得し、それを集計結果にまとめていく。

 DNSの名前解決には「Resolve-DnsName」コマンドレットを用いる。「-Type」オプションに「PTR」と指定すると、逆引きの結果が得られる。

 「-ErrorAction SilentlyContinue」オプションを指定しているのは、逆引きレコードが未設定のIPアドレスでエラーメッセージが表示されるのを抑えるためだ。

 また「-QuickTimeout」オプションを指定しているのは、DNSクエリに時間がかかり過ぎる場合に、より短いタイムアウトで終了させるためだ(DNSクエリに失敗する可能性も高まりそうだが)。

$countWithRDNS = @() # 逆引きホスト名も含む集計結果の配列

# 集計したIPアドレスをDNSで逆引きしてホスト名を取得しつつまとめる
$countTemp | ForEach-Object {
  $clientIP = $_.clientIP # クライアント(アクセス元)IPアドレス
  $reqCount = [Int32]$_.reqCount # リクエスト数
  # ↑集計時は文字列型なので整数に変換

  # DNSクエリ中であることを明示。時間がかかることがあるので
  Write-Host "DNS逆引き中……: $clientIP"

  # DNS逆引きを実行。タイムアウト短縮。エラーは表示せずに続行
  $revDNSResults = Resolve-DnsName $clientIP -Type PTR `
    -QuickTimeout `
    -ErrorAction SilentlyContinue
  # PTR(逆引き)レコードのみ抽出
  $revDNSResult = $revDNSResults | Where-Object { $_.Type -eq 'PTR' }
  # 逆引きホスト名を抽出。逆引きが未設定なら$null⇒空文字列が入る
  $revHostName = [string]$revDNSResult.NameHost

  # PSCustomObject(カスタムオブジェクト)の配列にまとめていく
  $countWithRDNS += [PSCustomObject]@{
    clientIP = $clientIP
    reqCount = $reqCount
    revHostName = $revHostName
  }
}

集計したIPアドレスからDNSの逆引きでホスト名を取得しつつ結果をまとめる

 逆引きホスト名が得られたら、クライアントIPアドレスおよびリクエスト数とともにカスタムオブジェクトへ格納して、配列に加えていく。

集計結果を一覧表で表示する

 ここまでの処理で、集計結果をカスタムオブジェクトで配列にまとめた。その内容を一覧表に整形するのは難しくない。

 例えば行単位で並び順を変える(ソートする)には、「Sort-Object」コマンドレットに当該オブジェクトの配列をそのままパイプで渡せば済む。並び順は、基準とするカラム名を「-Property」オプションで指定すればよい。

 また、表のカラム(列)の種類や順番を指定するには、「Format-Table」コマンドレットの「-Property」オプションで、各カラム名をコンマ区切りで指定する。

$queryDatetimeStr = $queryDatetime.ToString('yyyy/MM/dd HH:mm:ss')
Write-Host "――――― リクエスト数の多いクライアント一覧($queryDatetimeStr から $timeSpanMinutes 分間) ―――――"

# 行はリクエスト数が多い順
# 列は左からIPアドレス、逆引きホスト名、リクエスト数の順
$countWithRDNS | `
  Sort-Object -Property reqCount -Descending | `
  Format-Table -Property clientIP,revHostName,reqCount

集計結果を一覧表で表示する

 以下の画面は、前述の全リストをまとめて実行したときの出力である。

本記事の全リストをまとめたPowerShellスクリプトの実行時の出力 本記事の全リストをまとめたPowerShellスクリプトの実行時の出力

集計結果をCSVファイルにエクスポートする

 カスタムオブジェクトでまとめた集計結果は、「Export-CSV」コマンドレットで簡単にCSVファイルへエクスポートできる。

# エクスポート先のCSVファイル名
$CSVFileName = "countWithRDNS.csv"

# 行はリクエスト数が少ない順
# 列は左からIPアドレス、逆引きホスト名、リクエスト数の順
# 逆引きホスト名のみ引用符でくくる。文字コードはUTF-16リトルエンディアン
$countWithRDNS | `
  Sort-Object -Property reqCount | `
  Select-Object -Property clientIP,revHostName,reqCount | `
  Export-Csv $CSVFileName -QuoteFields revHostName -Encoding unicode

集計結果をCSVファイルにエクスポートする

 列を絞り込んだり並び順を変えたりするには、Export-Csvコマンドレットの前に「Select-Object」を実行する。

 Export-Csvコマンドレットには、CSVを整形するためのオプションが複数用意されている。例えば「-QuoteFields」オプションを用いると、特定のカラムのみ引用符でくくることが可能だ。また文字コード(エンコーディング)は「-Encoding」オプションで指定できる。その他はMicrosoft Learnのレファレンスを参照していただきたい。

 エクスポートされたCSVファイルの例を以下に示す。

clientIP,"revHostName",reqCount
2400:####:###:####:####:####:####:92d,"",2
240b:####:###:####:####:####:####:71be,"",2
1.###.###.49,"sp1-###-###-49.###.spmode.ne.jp",3
103.###.###.141,"",3
240f:3e:####:1:###:####:####:7dfd,"",4
126.###.###.5,"om126######005.##.openmobile.ne.jp",5
202.###.###.40,"#####.#####.co.jp",5
2001:###:####:####:####:####:####:4767,"",6
58.###.###.210,"58x###x###x210.####.ftth.ucom.ne.jp",9
126.###.###.209,"ai126######209.##.access-internet.ne.jp",13

エクスポートされたCSVファイルの例
「#」は伏せ字で、本来は数値またはドメイン名。行の順番はSort-Objectコマンドレットで、列の順番はSelect-Objectコマンドレットでそれぞれ変更されている。またExport-Csvコマンドレットの-QuoteFieldsオプションにより、逆引きホスト名の列だけが引用符でくくられている。

「Tech TIPS」のインデックス

Tech TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。