PowerShell CIMでHW情報

PowerShell

本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。

PowerShell CIMで大規模Windows環境のハードウェア情報を効率的に取得する

PowerShellのCIMコマンドレットを活用し、Windowsサーバーのハードウェア情報を効率的かつ堅牢に取得する手法を解説します。大規模環境での並列処理、エラーハンドリング、ロギング、そして性能計測の重要性について深く掘り下げます。

目的と前提 / 設計方針(同期/非同期、可観測性)

目的は、多数のWindowsサーバーからCPU、メモリ、ディスクなどのハードウェア情報を一括で、かつ効率的に取得することです。この自動化により、資産管理、キャパシティプランニング、トラブルシューティングの迅速化に貢献します。

前提として、ターゲットとなるリモートサーバーに対してWinRM(Windows Remote Management)が構成されており、PowerShellスクリプト実行ユーザーが対象サーバーに対する適切な管理者権限(ローカルAdministratorsグループのメンバーなど)を保持している必要があります。

設計方針としては、以下の点を重視します。

  • 非同期/並列処理: 単一サーバーへの同期的な問い合わせでは、大規模環境において処理時間が膨大になります。Runspace Poolを活用した並列処理を導入し、スループットを最大化します。
  • 堅牢性: ネットワークの問題やサーバーの応答遅延、CIMサービスの障害に備え、再試行メカニズムとタイムアウト処理を実装します。エラーが発生した場合でもスクリプトが停止せず、詳細なエラー情報を収集できるようにします。
  • 可観測性: スクリプトの実行状況、収集されたデータ、発生したエラーを明確に記録するため、Transcriptログと構造化ログ(CSV形式)を併用します。

Get-CimInstanceGet-WmiObjectの後継であり、よりモダンなWMI通信プロトコルであるWS-Managementを使用します。Get-CimInstanceは、DCOMベースのGet-WmiObjectよりもネットワークパフォーマンスが向上し、ファイアウォール設定も容易になるため推奨されます。

コア実装(並列/キューイング/キャンセル)

ここでは、ターゲットサーバーからハードウェア情報を取得し、エラーハンドリング、再試行、タイムアウト処理を含む関数と、その関数をRunspace Poolで並列実行するスクリプトを実装します。

ハードウェア情報取得関数(エラーハンドリング・再試行付き)

まず、単一サーバーからハードウェア情報を取得する関数Get-HardwareInfoWithRetryを定義します。この関数は、-ComputerNameパラメーターを受け取り、CIMコマンドレットを使用してCPU、メモリ、ディスクなどの情報を収集します。通信エラーやタイムアウトが発生した場合には、指定回数だけ再試行を行います。

function Get-HardwareInfoWithRetry {
    [CmdletBinding(DefaultParameterSetName='Default', SupportsShouldProcess=$true, ConfirmImpact='Low')]
    param(
        [Parameter(Mandatory=$true)]
        [string]$ComputerName,
        [int]$RetryCount = 3,
        [int]$RetryDelaySec = 5,
        [int]$OperationTimeoutSec = 60
    )

    if ($PSCmdlet.ShouldProcess("コンピューター '$ComputerName' からハードウェア情報を取得", "情報取得")) {
        $hwInfo = $null
        for ($i = 0; $i -lt $RetryCount; $i++) {
            try {
                Write-Verbose "Attempt $($i+1) to get hardware info from $ComputerName"

                # Win32_ComputerSystem から基本情報を取得
                $system = Get-CimInstance -ClassName Win32_ComputerSystem -ComputerName $ComputerName -OperationTimeoutSec $OperationTimeoutSec -ErrorAction Stop

                # Win32_Processor からプロセッサ情報を取得
                $processor = (Get-CimInstance -ClassName Win32_Processor -ComputerName $ComputerName -OperationTimeoutSec $OperationTimeoutSec -ErrorAction Stop | Select-Object -First 1).Name # 複数CPU対応

                # Win32_PhysicalMemory から物理メモリ情報を取得し合計容量を算出
                $memory = Get-CimInstance -ClassName Win32_PhysicalMemory -ComputerName $ComputerName -OperationTimeoutSec $OperationTimeoutSec -ErrorAction Stop
                $totalMemoryGB = [Math]::Round(($memory | Measure-Object -Property Capacity -Sum).Sum / 1GB, 2)

                # Win32_DiskDrive からディスクドライブ情報を取得
                $disks = Get-CimInstance -ClassName Win32_DiskDrive -ComputerName $ComputerName -OperationTimeoutSec $OperationTimeoutSec -ErrorAction Stop
                $diskCount = ($disks | Measure-Object).Count
                $totalDiskCapacityGB = [Math]::Round(($disks | Measure-Object -Property Size -Sum).Sum / 1GB, 2)

                return [PSCustomObject]@{
                    ComputerName = $ComputerName
                    Manufacturer = $system.Manufacturer
                    Model = $system.Model
                    Processor = $processor
                    TotalMemoryGB = $totalMemoryGB
                    DiskCount = $diskCount
                    TotalDiskCapacityGB = $totalDiskCapacityGB
                    Status = "Success"
                    Timestamp = (Get-Date)
                }
            }
            catch {
                Write-Warning "Failed to get hardware info from $ComputerName (Attempt $($i+1)): $($_.Exception.Message)"
                if ($i -lt ($RetryCount - 1)) {
                    Write-Verbose "Retrying in $RetryDelaySec seconds..."
                    Start-Sleep -Seconds $RetryDelaySec
                }
                else {
                    Write-Error "Max retry attempts reached for $ComputerName. Aborting." -ErrorAction Continue
                }
            }
        }
        # 全ての再試行が失敗した場合
        return [PSCustomObject]@{
            ComputerName = $ComputerName
            Status = "Failed"
            ErrorMessage = "All retry attempts failed or unexpected error occurred."
            Timestamp = (Get-Date)
        }
    }
}

並列処理のフロー

Mermaid図で、並列処理の全体的なフローを示します。

flowchart TD
    A["スクリプト開始"] --> B{"対象サーバーリスト"};
    B --> C["Runspace Pool初期化"];
    C --> D{"サーバーごとにPowerShellオブジェクト生成"};
    D --> E["Get-HardwareInfoWithRetry関数とパラメーターをPowerShellオブジェクトに設定"];
    E --> F["Runspace Poolにタスク投入 (BeginInvoke)"];
    F --> G{"全タスクの完了を待機"};
    G --|タスク完了| H["結果収集 (EndInvoke)"];
    H --|エラー発生| I["エラーログ記録"];
    I --|成功| J["成功ログ記録"];
    J --> K["Runspace Pool解放"];
    K --> L["集計結果出力"];
    L --> M["スクリプト終了"];
    G --|待機中| G;

Runspace Poolによる並列処理とスループット計測

以下のスクリプトは、Get-HardwareInfoWithRetry関数をRunspace Poolを用いて複数のターゲットサーバーに対して並列実行し、処理時間を計測します。ロギングも同時に行い、結果を構造化データとして保存します。

# スクリプトのパスを取得し、Get-HardwareInfoWithRetry関数のソースコードを読み込む
$ScriptPath = $MyInvocation.MyCommand.Path
$HardwareInfoFunctionScriptBlock = (Get-Content $ScriptPath | Out-String)

# サンプルターゲットリスト (テスト用に存在しないものも含む)
$TargetServers = @(
    "localhost",
    "NonExistentServer01",
    "127.0.0.1",
    "NonExistentServer02",
    "AnotherLocalhost",
    "NonExistentServer03"
) | Select-Object -Unique # 重複を除去

$MaxThreads = 5 # 並列実行数。環境のキャパシティに応じて調整。

# ロギング設定
$LogDirectory = Join-Path -Path $PSScriptRoot -ChildPath "Logs"
if (-not (Test-Path $LogDirectory)) {
    New-Item -ItemType Directory -Path $LogDirectory | Out-Null
}
$Timestamp = (Get-Date -Format "yyyyMMdd_HHmmss")
$TranscriptLogFile = Join-Path -Path $LogDirectory -ChildPath "HwInfoCollection_$Timestamp.log"
$SuccessCsvFile = Join-Path -Path $LogDirectory -ChildPath "HwInfo_Success_$Timestamp.csv"
$FailedCsvFile = Join-Path -Path $LogDirectory -ChildPath "HwInfo_Failed_$Timestamp.csv"

# Transcriptロギング開始
Start-Transcript -Path $TranscriptLogFile -Append -Force -ErrorAction SilentlyContinue

Write-Host "開始: ハードウェア情報収集 (並列処理)"
Write-Host "対象サーバー数: $($TargetServers.Count)"
Write-Host "並列実行数: $MaxThreads"

# エラーアクション設定 (Runspace内で発生するエラーはcatchで処理)
$ErrorActionPreference = 'Stop' # Runspace外のスクリプト本体のエラーに適用

$Results = [System.Collections.Generic.List[PSObject]]::new()
$FailedServers = [System.Collections.Generic.List[PSObject]]::new()

# スループット計測
$Measure = Measure-Command {
    # Runspaceごとに異なるセッション状態を作成し、関数を読み込ませる
    $SessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
    $SessionState.LanguageMode = 'FullLanguage' # スクリプトブロック内で完全な言語モードを許可

    # Runspace Poolの作成とオープン
    $RunspacePool = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspacePool(1, $MaxThreads, $SessionState, $Host)
    $RunspacePool.Open()

    $Jobs = [System.Collections.Generic.List[System.Management.Automation.PowerShell]]::new()

    foreach ($computer in $TargetServers) {
        $PowerShell = [System.Management.Automation.PowerShell]::Create()
        $PowerShell.RunspacePool = $RunspacePool

        # 関数定義を含むスクリプトブロックとパラメーターをRunspaceに渡す
        $PowerShell.AddScript($HardwareInfoFunctionScriptBlock).AddScript({
            param($ComputerName)
            Get-HardwareInfoWithRetry -ComputerName $ComputerName -Verbose
        }).AddParameter("ComputerName", $computer) | Out-Null

        $Jobs.Add($PowerShell) | Out-Null
        $null = $PowerShell.BeginInvoke() # ジョブを開始
    }

    # 全てのジョブの完了を待機し、結果を収集
    while ($Jobs.IsRunning) {
        $completedJobs = $Jobs | Where-Object {$_.IsCompleted}
        $pendingJobs = $Jobs | Where-Object {!$_.IsCompleted}
        Write-Progress -Activity "ハードウェア情報収集中" -Status "完了: $($completedJobs.Count) / 全体: $($Jobs.Count)" -PercentComplete (($completedJobs.Count / $Jobs.Count) * 100)
        Start-Sleep -Milliseconds 200
    }

    foreach ($job in $Jobs) {
        try {
            $result = $job.EndInvoke()
            # エラーがあった場合、$resultはnullまたはエラーオブジェクトを含むことがある
            if ($result) {
                if ($result.Status -eq "Failed") {
                    $FailedServers.Add($result)
                } else {
                    $Results.Add($result)
                }
            } else {
                # EndInvokeが何も返さない場合 (例: スクリプトブロックで明示的なreturnがない場合)
                $FailedServers.Add([PSCustomObject]@{
                    ComputerName = $job.Commands.Commands[1].Parameters.Item("ComputerName").Value # パラメータからComputerNameを取得
                    Status = "Failed"
                    ErrorMessage = "No result returned from Runspace. Possible unhandled error."
                    Timestamp = (Get-Date)
                })
            }
        }
        catch {
            # Runspace内で捕捉されなかった致命的なエラー
            $FailedServers.Add([PSCustomObject]@{
                ComputerName = $job.Commands.Commands[1].Parameters.Item("ComputerName").Value
                Status = "Failed"
                ErrorMessage = "Fatal error in Runspace for $($job.Commands.Commands[1].Parameters.Item("ComputerName").Value): $($_.Exception.Message)"
                Timestamp = (Get-Date)
            })
        }
        finally {
            $job.Dispose() # PowerShellインスタンスを解放
        }
    }

    $RunspacePool.Close()
    $RunspacePool.Dispose()
}

Write-Host "終了: ハードウェア情報収集 (並列処理)"
Write-Host "処理時間: $($Measure.TotalSeconds) 秒"

# 結果の表示と構造化ログへの出力
Write-Host "`n--- 成功したサーバー ---"
if ($Results.Count -gt 0) {
    $Results | Format-Table -AutoSize
    $Results | Export-Csv -Path $SuccessCsvFile -NoTypeInformation -Encoding UTF8
    Write-Host "成功したサーバーの結果は $(Split-Path $SuccessCsvFile -Leaf) に保存されました。"
} else {
    Write-Host "成功したサーバーはありませんでした。"
}

Write-Host "`n--- 失敗したサーバー ---"
if ($FailedServers.Count -gt 0) {
    $FailedServers | Format-Table -AutoSize
    $FailedServers | Export-Csv -Path $FailedCsvFile -NoTypeInformation -Encoding UTF8
    Write-Host "失敗したサーバーの結果は $(Split-Path $FailedCsvFile -Leaf) に保存されました。"
} else {
    Write-Host "失敗したサーバーはありませんでした。"
}

# Transcriptロギング停止
Stop-Transcript -ErrorAction SilentlyContinue

検証(性能・正しさ)と計測スクリプト

上記の並列処理スクリプトは、Measure-Commandによって全体の処理時間を計測しています。この計測結果は、単一スレッドで同じ処理を行った場合と比較することで、並列化による性能向上の度合いを評価できます。

性能検証: * $TargetServersに異なる数のサーバー(例: 10台、50台、100台)を設定し、それぞれの処理時間を比較します。 * $MaxThreadsの値を変更し、最適な並列数を特定します。過剰な並列数は、リソースの競合により性能を低下させる可能性があります。

正しさの検証: * HwInfo_Success_*.csvに出力されたCSVファイルを開き、各サーバーから取得されたデータ(Manufacturer, Model, TotalMemoryGBなど)が正しいか、ランダムに数件サンプリングして確認します。特に、メモリやディスク容量の単位変換が正しく行われているか注意が必要です。 * HwInfo_Failed_*.csvには、接続に失敗したサーバーや情報取得に失敗したサーバーとそのエラーメッセージが記録されていることを確認します。

運用:ログローテーション/失敗時再実行/権限

ロギング戦略: * Transcriptログ: Start-Transcriptにより、スクリプトの実行コンソール出力をすべてテキストファイルに記録します。これはスクリプトの実行履歴全体を追跡するのに役立ちます。 * 構造化ログ: Export-Csvにより、成功したデータと失敗したデータをそれぞれCSVファイルとして出力します。これは後続のデータ分析や失敗時の再実行リスト生成に利用できます。 * ログローテーション: ログファイル名にタイムスタンプを含めることで、世代管理を実現しています。古いログファイルを定期的にクリーンアップするスクリプトを別途作成し、ディスク容量の肥大化を防ぎます。例:7日以上前のログファイルを削除するタスクをスケジュールする。

失敗時再実行: スクリプトが生成するHwInfo_Failed_*.csvは、失敗したサーバーのリストを含んでいます。このCSVファイルからComputerName列を抽出し、それを新しい$TargetServersリストとして、再度スクリプトを実行することで、失敗したサーバーのみを対象とした再実行が可能です。

# 例:前回の失敗リストから再実行ターゲットを生成
$PreviousFailedCsv = "C:\Path\To\Logs\HwInfo_Failed_20231026_100000.csv" # 実際のファイルパスに置き換える
if (Test-Path $PreviousFailedCsv) {
    $RetryServers = (Import-Csv -Path $PreviousFailedCsv).ComputerName | Select-Object -Unique
    Write-Host "再実行対象サーバー: $($RetryServers -join ', ')"
    # $RetryServers を $TargetServers に代入してメインスクリプトを再実行
} else {
    Write-Warning "前回の失敗ログが見つかりません: $PreviousFailedCsv"
}

権限: リモートWinRM接続には、ターゲットサーバー上で管理者権限が必要です。最小権限の原則に基づき、専用のサービスアカウントを用意し、そのアカウントに必要最小限の権限のみを付与することが推奨されます。 よりセキュアな運用のためには、JEA(Just Enough Administration)やJIT(Just-in-Time)アクセス管理の導入を検討します。これにより、特定のタスク実行時のみ一時的に昇格された権限を付与し、恒久的な管理者権限の付与を避けることができます。 資格情報の管理には、Get-Credentialコマンドレットでインタラクティブに入力する方法や、SecretManagementモジュールを利用して、安全に保存・取得する方法があります。SecretManagementは、資格情報を安全なVaultに格納し、必要に応じて取得できるため、スクリプト内にハードコードすることを避ける上で非常に有効です。

落とし穴(例:PowerShell 5 vs 7の差、スレッド安全性、UTF-8問題)

  • PowerShell 5.1 と PowerShell 7 の差: ForEach-Object -ParallelコマンドレットはPowerShell 7の新機能です。PowerShell 5.1環境では利用できず、本記事で示したRunspace Poolを直接操作する方法が主流となります。PowerShell 7ではForEach-Object -Parallelが簡潔に並列処理を記述できるため、利用可能な場合は検討してください。
  • スレッド安全性: Runspace Poolで並列処理を行う際、複数のRunspaceから共有される変数やオブジェクトへのアクセスは注意が必要です。今回の例では、各Runspaceは独立して関数を実行し、結果は[System.Collections.Generic.List[PSObject]]に格納していますが、このリストへの書き込みはメインスレッド(スクリプト本体)で行っています。もしRunspace内で共有変数に書き込む必要がある場合は、[System.Collections.Concurrent.ConcurrentBag[object]]のようなスレッドセーフなコレクションを使用するか、ロック機構を実装する必要があります。
  • UTF-8問題: Export-Csvで日本語などのマルチバイト文字を含むデータをエクスポートする場合、-Encoding UTF8パラメーターを明示的に指定しないと、デフォルトのエンコーディング(PowerShell 5.1ではASCIIベース、PowerShell 7ではUTF-8 BOM付き)により文字化けが発生する可能性があります。特に、異なるPowerShellバージョンやOS環境でデータをやり取りする場合に注意が必要です。
  • WinRM設定とファイアウォール: ターゲットサーバーのWinRMサービスが実行されており、WindowsファイアウォールでWinRMのポート(通常5985/HTTP, 5986/HTTPS)が許可されていることを確認する必要があります。また、Set-Item WSMan:\localhost\Client\TrustedHosts -Value "*"などで信頼済みホストの設定が必要な場合がありますが、これはセキュリティリスクを伴うため、特定ホストのみを許可するよう厳密に設定することが推奨されます。
  • リソース消費: 並列数を$MaxThreadsで指定しますが、過度に大きな値を設定すると、クライアント側のCPUやメモリ、ネットワーク帯域を消費しすぎ、パフォーマンスが低下する可能性があります。環境のキャパシティと、ターゲットサーバーへの負荷を考慮して適切な値を設定することが大切です。

まとめ

PowerShellのCIMコマンドレットとRunspace Poolを組み合わせることで、大規模Windows環境のハードウェア情報を効率的かつ堅牢に取得するスクリプトを構築できます。エラーハンドリング、再試行、タイムアウト処理を適切に実装し、Transcriptログと構造化ログで可観測性を確保することが、運用の安定性を高めます。さらに、JEAやSecretManagementといったセキュリティプラクティスを導入することで、より安全な自動化運用を実現可能です。

ライセンス:本記事のテキスト/コードは特記なき限り CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。

コメント

タイトルとURLをコピーしました