オンプレミスとAzure上のラボ環境のHyper-Vホストには、Hyper-V仮想マシン(VM)の管理に役立つPowerShellスクリプトを配置しています。自分の作業を効率化するために作成したスクリプトですが、せっかくなので皆さんに公開し、使い方を説明したいと思います。
VMの作成と管理のための5つのPowerShellスクリプト
ラボ環境のHyper-Vホストには、現状、以下の5つのPowerShellスクリプトを用意しています。
[enable-nestedvirt.ps1](.txt形式でダウンロード)
Param($Arg1)
$vmname = $Arg1
Set-VMProcessor -VMName $vmname -ExposeVirtualizationExtensions $true
Get-VMNetworkAdapter -VMName $vmname | Set-VMNetworkAdapter -MacAddressSpoofing On
Install-WindowsFeature -Name Hyper-V -IncludeManagementTools -Restart
[offlinepatch.ps1](.txt形式でダウンロード)
Param($PackageDir,$TargetVhd,$mountdir)
if ($PSBoundParameters.Count -ne 3) { Write-host "Error: -PackageDir <path> -TargetVhd <path> -MountDir <path>" ;exit }
If ( -not (Test-Path $packagedir)) { Write-host "Error: -PackageDir <path> does not exist." ;exit }
If ( -not (Test-Path $targetvhd)) { Write-host "Error: -TargetVhd <path> does not exist." ;exit }
If ( -not (Test-Path $mountdir)) { Write-host "Error: -MountDir <path> does not exist." ;exit }
$packages = (Get-ChildItem -Path $packagedir| where {$_.extension -eq ".msu"} | Sort Name | %{$_.FullName})
$success = $true
if ($packages.Count -gt 0){
DISM /Mount-Image /Imagefile:$targetvhd /index:1 /mountdir:$mountdir
#if (Test-Path -Path $mountdir"\Windows") {
if ($LASTEXITCODE -eq 0) {
foreach ($package in $packages){
DISM /Add-Package /Image:$mountdir /PackagePath:$package
if (!($LASTEXITCODE -eq 0)) {
$success = $false
Write-Host "Failed: "$package
}
}
} else {
"Failed: DISM /Mount-Image"
"Failed: VHD(x) was not updated."
exit
}
if ($success) {
DISM /Unmount-Image /mountdir:$mountdir /Commit
if (!($LASTEXITCODE -eq 0)) {
DISM /Unmount-Image /mountdir:$mountdir /Discard
"Failed: DISM /Commit, VHD(x) was not updated. "
exit
}
} else {
DISM /Unmount-Image /mountdir:$mountdir /Discard
"Failed: DISM /Add-Package, VHD(x) was not updated."
exit
}
}
exit
[cleanup-image.ps1](.txt形式でダウンロード)
Param($TargetVhd,$mountdir)
if ($PSBoundParameters.Count -ne 2) { Write-host "Error: -TargetVhd <path> -MountDir <path>" ;exit }
If ( -not (Test-Path $targetvhd)) { Write-host "Error: -TargetVhd <path> does not exist." ;exit }
If ( -not (Test-Path $mountdir)) { Write-host "Error: -MountDir <path> does not exist." ;exit }
$success = $true
DISM /Mount-Image /Imagefile:$targetvhd /index:1 /mountdir:$mountdir
#if (Test-Path -Path $mountdir"\Windows") {
if ($LASTEXITCODE -eq 0) {
DISM /Image:$mountdir /Cleanup-Image /StartComponentCleanup /ResetBase
if (!($LASTEXITCODE -eq 0)) {
$success = $false
}
} else {
"Failed: DISM /Mount-Image"
"Failed: VHD(x) was not updated."
exit
}
if ($success) {
DISM /Unmount-Image /mountdir:$mountdir /Commit
if (!($LASTEXITCODE -eq 0)) {
DISM /Unmount-Image /mountdir:$mountdir /Discard
"Failed: DISM /Commit, VHD(x) was not updated. "
exit
}
} else {
DISM /Unmount-Image /mountdir:$mountdir /Discard
"Failed: DISM /Cleanup-Image, VHD(x) was not updated."
exit
}
exit
[optimize-vhd.ps1](.txt形式でダウンロード)
Param($Arg1)
$targetvhd = $Arg1
if (($targetvhd -eq $null) -or !(Test-Path $targetvhd)) {
Write-Host "Error: targetvhd missing"
exit
}
#$targetvhd = "D:\VM\WIN2012\Virtual Hard Disks\demog1vm.vhdx";
dir $targetvhd
Mount-VHD $targetvhd -NoDriveLetter -Readonly
Optimize-VHD $targetvhd -Mode Quick
Optimize-VHD $targetvhd -Mode Quick
Dismount-VHD $targetvhd
dir $targetvhd
[find-childvhd.ps1](.txt形式でダウンロード)
Param($parentvhd, $searchpath)
if ($PSBoundParameters.Count -ne 2) { Write-Host "Error: .\find-childvhd.ps1 -ParentVhd <Parent VHD(x) full path> -SearchPath <Root path> "; exit }
if (!(Test-Path($parentvhd))) { Write-Host "Error: VHD file ($parentvhd) not found."; exit }
if (!(Test-Path($searchpath))) { Write-Host "Error: Path ($searchpath) not found."; exit }
#$parentvhd = Split-path $parentvhd -Leaf
#Get-ChildItem -Path $searchpath -Include "*.*vhd*" -Recurse | Get-VHD|Where {$_.ParentPath -like "*$parentvhd" } |Select Path, ParentPath
Get-ChildItem -Path $searchpath -Include "*.*vhd*" -Recurse | Get-VHD|Where {$_.ParentPath -eq "$parentvhd" } |Select Path, ParentPath
入れ子になった仮想化の有効化
Hyper-Vを実行する物理マシン、および入れ子になった仮想化が有効なHyper-V VMおよびAzure VMでは、自身のHyper-V環境に作成するVMでさらに入れ子になった仮想化を有効にすることができます。入れ子になった仮想化が有効なVMでは、Hyper-Vを有効化できるほか、Hyper-Vハイパーバイザーに依存する機能(Microsoft Defender Application Guard≪MDAG≫、Windowsサンドボックス、Windows Subsystem for Linuxバージョン2≪WSL2≫、仮想化ベースのセキュリティ≪VBS≫など)を使用することができます。
「Hyper-Vマネージャー」スナップン(virtmgmt.msc)には、入れ子になった仮想化を有効にするオプションはありません。その設定はPowerShellで行う必要があります。その方法については「vol.18 ラボ環境 on Azureを作る(6) - VMテンプレートを作成する」で説明しました。「enable-nestetvirt.ps1」は、PowerShellによる設定を簡素化するものです。
| PS C:¥> .¥enable-nestedvirt.ps1 "仮想マシン名" |
Hyper-VホストがWindows Admin Center(WAC)で管理されている場合、WACの「仮想マシン」ツールを使用すると、GUIで入れ子になった仮想化を有効化できることも紹介しておきます(画面2)。

画面2 WACを導入している場合、WACの「仮想マシン」ツールを使用して入れ子になった仮想化を有効化することもできる
VM/VMテンプレートのオフラインパッチ
「offlinepatch.ps1」は、「vol.31 VMテンプレートのオフラインパッチスクリプト|続・ラボ環境 on Azure(3)」で説明したものと同じものです。PackageDirで指定したフォルダーに、更新対象のWindows向けの更新プログラムパッケージ(.msu)をダウンロードして実行すれば、VMをオフラインにしたまま更新プログラムのインストールができるというものです(画面3)。
| PS C:¥> .¥offlinepatch.ps1 -PackageDir "<パッケージパス>" -TargetVhd "<VHD(x)のパス>" -MountDir "<マウント先パス>" |

画面3 オフラインのVHD(x)に対して更新プログラムを適用するスクリプト「offlinepatch.ps1」
このスクリプトはVMテンプレートの更新を想定していますが、作成済みのVMのVHD(x)を更新するのにも利用できます。その場合は、更新対象のVHD(x)に差分ディスクやチェックポイント(.avhd(x))が存在しないようにしてください。差分ディスクやチェックポイントが存在する場合、親のVHD(x)の更新により、親子のチェーンが壊れ、差分ディスクやチェックポイントが使用不能になります。
VM/VMテンプレートの更新プログラムのクリーンアップ
物理デバイスや仮想マシンのC:ドライブは更新プログラムのインストールにより、ディスク使用が増加していきます。オンラインで次のコマンドラインを実行すると、更新プログラムの管理に使用されるコンポーネントベースサービス(CBS)が最適化され、ディスク使用を大幅に節約できる場合があります。ただし、実行には数時間かかる場合がある他、実行後に過去の更新プログラムのアンインストールができなくなることには注意してください。
| C:¥> DISM /Online /Cleanup-Image /StartComponentCleanup /ResetBase |
上記のコマンドラインは、/Onlineを/Imageに置き換えることで、ローカルマウントしたオフラインイメージに対して実行することもできます。「cleanup-image.ps1」は、VHD(x)のローカルマウント、DISM /Cleanup-Imageの実行、VHD(x)のマウント解除の一連の操作をスクリプト化したものです。
| PS C:¥> .¥cleanup-image.ps1 -TargetVhd "<VHD(x)のパス>" -MountDir "<マウント先パス>" |
「cleanup-image.ps1」は、次の「optimize-image.ps1」を実行する前に実行すると、サイズ縮小効果のアップが期待できます。
容量可変VHD(x)の最適化(サイズ縮小)
「Hyper-Vマネージャー」スナップインから実行できる「仮想ハードディスクの編集ウィザード」は、容量可変タイプのVHD(x)の縮小が期待できる「最適化」オプションがあります(画面4)。しかし、このオプションを選択して実行しても、期待したほどサイズが縮小されない場合があります。

画面4 「仮想ハードディスクの編集ウィザード」の「最適化」オプションは、なぜか期待したほど容量可変タイプのVHD(x)のサイズを縮小してくれない(この問題は以前のバージョンから)
「最適化」オプションは、「Optimize-VHD(Microsoft)」コマンドレットを実行しているものと想像しますが、Microsoftのドキュメントによると「To use Optimize-VHD, the virtual hard disk must not be attached or must be attached in read-only mode.(Optimize-VHDを使用するには、仮想ハードディスクがアタッチされていないか、読み取り専用モードでアタッチされている必要があります。)」と説明されています。「最適化」の実行は、そもそも接続されていない状態でなければ実行できません(実行中のVMのVHD(x)のウィザードには表示されません)。もしかしてと思い、読み取り専用モードでマウントしてPowerShellで実行してみたところ、Optimize-VHDを2回実行することで大幅なサイズ縮小を確認できました(その実験については、旧ブログの記事とそのリンク先記事を参照)。「optimize-vhd.ps1」はその実験を踏まえて一連の操作をスクリプト化したものです(画面5)。
| PS C:¥> .¥optimize-image.ps1 "<VHD(x)のパス>" |

画面5 元々19.8GBのVHD(x)にオフラインパッチしたところ23.2GBに増加、それを「cleanup-image.ps1」と「optimize-image.ps1」にかけると22.0GBに縮小された。ちなみに、「cleanup-image.ps1」を省略して「optimize-image.ps1」を実行した場合、23.1GBとなったことから、「cleanup-image.ps1」の効果がわかる
差分ディスクの探索
最後の「find-childvhd.ps1」は、VHD(x)のフルパスと、検索先フォルダーのルートを指定することで、指定したVHD(x)を親とする差分ディスクおよびチェックポイント(.avhd(x))を再帰的に検索するスクリプトです(画面6)。
| PS C:¥> .¥cleanup-image.ps1 -ParentVhd "<VHD(x)のパス>" -MountDir "<マウント先パス>" |

画面6 指定したVHD(x)を親に持つ差分ディスクとチェックポイントを再帰的に検索するスクリプト「find-childvhd.ps1」
評価、検証環境では、ディスク使用の節約とVMの作製時間の短縮のために、同じVHD(x)を親とした差分ディスクを作成して、複数のVMを作成することがあります。この方法を多用すると、誤って親VHD(x)を削除してしまったり、変更を加えてしまったりして、差分ディスクやチェックポイントとのチェーンが破損することがあります。このスクリプトは、そのような事故を未然に防ぐために作成しました。