Hyper-Vで作るコンテナ 新しい仮想マシンの使い方-Vol.4

オリジナルのPowerShellコマンドの作成(1)

いよいよ、実際にHyper-Vを利用してコンテナ技術を実現するために、オリジナルのモジュールを定義してPowerShellのコマンドを作成していきましょう。

前提

モジュール名は「InvokeVContainer」とします。
前回配置した「C:\Users\<ユーザー名\Documents\WindowsPowerShell\Modules\InvokeVContainer.psm1」のモジュールファイルのコマンドを追加していきます。

最終的な完成版は、GitHub(https://github.com/InvokeV/InvokeV-Container)に掲載しています。

公開しているモジュールInvokeVContainer.psm1のコードは以下となります。

$RootPath = "D:\InvokeVContainer"

Function Get-InvokeVContanerRoot(){
     Return $RootPath
}

Function Import-ContainerImage($FilePath, $ImageName) { 
    $File = Get-ChildItem $FilePath
    If ($File.Extension -eq ".vhdx") {
        If ($File -match "[A-Fa-f0-9]{8}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{12}" -eq $True){
            Copy-Item $File -Destination ("$RootPath\Images\" + $File.Name)
        }Else{
            If($ImageName){$ImageName = $File.BaseName}
            Copy-Item $File -Destination ("$RootPath\Images\" + $ImageName + "_" + [Guid]::NewGuid() + ".vhdx") 
        }
    }Else{
        Expand-Archive -Path $FilePath -DestinationPath $RootPath\Images -Force
    } 
}

Function Get-ContainerImage { 
    Get-ChildItem $RootPath\Images *.vhdx | Where-Object {$_.Name -match "[A-Fa-f0-9]{8}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{12}"} | Select `
    @{Label="Name"; Expression={($_.BaseName.Substring(0, $_.BaseName.Length - ($_.BaseName.Split("_")[$_.BaseName.Split("_").Length - 1].Length) - 1))}}, `
    @{Label="Path"; Expression={($_.FullName)}}, 
    @{Label="Size(MB)"; Expression={($_.Length /1024/1024)}}, `
    @{Label="Created"; Expression={($_.LastWriteTime)}}, `
    @{Label="ParentPath"; Expression={(Get-VHD $_.FullName).ParentPath}} | Where-Object {$_.Name -ne ""}
}

Function Export-ContainerImage([String]$ImageName, [String]$ExportPath, [Switch]$Tree){
    [Reflection.Assembly]::LoadWithPartialName( "System.IO.Compression.FileSystem" ) | Out-Null
    $Archive = [System.IO.Compression.ZipFile]::Open($ExportPath, "Update")
    $CompressionLevel = [System.IO.Compression.CompressionLevel]::Optimal
    $Image = Get-ChildItem (Get-ContainerImage | Where {$_.Name -eq "$ImageName"}).Path
    [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($Archive, $Image.FullName, $Image.Name, $CompressionLevel) | Out-Null  

    If($Tree){ 
        $ParentFile = (Get-ContainerImage | Where {$_.Name -eq "$ImageName"}).ParentPath  
        While ($ParentFile -ne ""){
            If($ParentFile -ne ""){
                [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($Archive, $ParentFile, (Get-ChildItem $ParentFile).Name, $CompressionLevel) | Out-Null
                $ParentFile = (Get-ContainerImage | Where {$_.Path -eq "$ParentFile"}).ParentPath
            }
        }        
    }
    $Archive.Dispose()
}

Function New-ContainerImage([String]$ContainerName, [String]$ImageName) {    
    $ImageName = $ImageName + "_" + [Guid]::NewGuid() 
    $VM = Get-VM $ContainerName
    $Disk = Get-VMHardDiskDrive $VM   
    If($VM.State -eq "Off"){
        Get-VMHardDiskDrive $VM | Copy-Item -Destination "$RootPath\Images\$ImageName.vhdx"
     }Else{
        Checkpoint-VM $VM -SnapshotName "$ContainerName"
        Copy-Item (Get-VHD (Get-VMHardDiskDrive $VM).Path).ParentPath -Destination "$RootPath\Images\$ImageName.vhdx"
        Remove-VMSnapshot $VM –Name "$ContainerName" 
    }   
}

Function Merge-ContainerImage([String]$ImageName, [String]$NewImageName, [Switch]$Del) { 
    $ImagePath = (Get-ContainerImage | Where {$_.Name -eq "$ImageName"}).Path  
    $ParentPath = (Get-VHD "$ImagePath").ParentPath
    $NewImageID = $NewImageName + "_" + [Guid]::NewGuid() 
    Copy-Item "$ParentPath" -Destination "$RootPath\Images\$NewImageID.vhdx" 
    Copy-Item "$ImagePath" -Destination "$RootPath\Images\$NewImageID.avhdx"
    Set-VHD -Path "$RootPath\Images\$NewImageID.avhdx" –ParentPath "$RootPath\Images\$NewImageID.vhdx"
    Merge-VHD –Path "$RootPath\Images\$NewImageID.avhdx" –DestinationPath "$RootPath\Images\$NewImageID.vhdx"  
    If($Del){ Remove-Item $ImagePath }
}

Function Remove-ContainerImage[String]$ImageName, [Switch]$Tree) { 
    $ImagePath = (Get-ContainerImage | Where {$_.Name -eq "$ImageName"}).Path
    If($ImagePath) {
        $ChildItems = Get-ChildItem -Path "$RootPath" -Include "*.vhdx" -Recurse | Get-VHD | Where {$_.ParentPath -eq "$ImagePath"}
        If($ChildItems.Count -eq 0){
            Remove-Item $ImagePath -Recurse
        }Else{   
            If($Tree){
                ForEach ($ChildItem in $ChildItems) {
                    If($ChildItem.Path.ToString().ToUpper().StartsWith("$RootPath\Images".ToString().ToUpper())){
                        $ChildImageName = (Get-ChildItem ($ChildItem.Path)).Name
                        $ImageName = ($ChildImageName.Substring(0, $ChildImageName.Length - ($ChildImageName.Split("_")[$ChildImageName.Split("_").Length - 1].Length) - 1)) 
                        Remove-ContainerImage $ImageName -Tree
                    }Else{
                        $ContainerName =  (Get-ChildItem ($ChildItem.Path)).BaseName
                        Remove-Container($ContainerName)
                    }
                }            
                Remove-Item $ImagePath -Recurse
            }Else{
                Write-Host """$ImageName"" is used other container images or containers." -ForegroundColor Red
            }
        }
    }Else{
         Write-Host """$ImageName"" is not container image name." -ForegroundColor Red
    }
}

Function New-Container([String]$ContainerName, [String]$ImageName, [Long]$Memory=1024MB, [Int]$Processer=1, [String]$SwitchName) {
    $ImagePath = (Get-ContainerImage | Where {$_.Name -eq "$ImageName"}).Path 
    $VHD = New-VHD -Path "$RootPath\Containers\$ContainerName\$ContainerName.vhdx" -Differencing -ParentPath "$ImagePath"
    $VM = New-VM -Name "$ContainerName" -Generation 2 -MemoryStartupByte $Memory -VHDPath $VHD.Path -Path "$RootPath\Containers"
    Set-VM $VM -DynamicMemory -MemoryMaximumBytes $Memory -ProcessorCount $Processer
    If($SwitchName -ne ""){
        Get-VMNetworkAdapter $VM | Connect-VMNetworkAdapter –SwitchName $SwitchName
    }
    Set-VMFirmware $VM -EnableSecureBoot Off
    Set-VMProcessor $VM -ExposeVirtualizationExtensions $True
} 

Function Get-Container { 
    Get-VM | Where-Object {Test-Path ("$RootPath\Containers\" + $_.Name)} | Select `
    @{Label="Name"; Expression={$_.Name}}, 
    State, 
    @{Label="Path"; Expression={((Get-VMHardDiskDrive $_.Name | Where-Object {$_.ControllerLocation -eq 0}).Path)}}, `
    @{Label="ParentPath"; Expression={(Get-VHD(Get-VMHardDiskDrive $_.Name | Where-Object {$_.ControllerLocation -eq 0}).Path).ParentPath}}
}

Function Start-Container([String]$ContainerName) {   
    Start-VM $ContainerName
}

Function Stop-Container([String]$ContainerName) {   
    Stop-VM $ContainerName -Force
}

Function Remove-Container([String]$ContainerName) { 
    $VM = Get-VM "$ContainerName"
    If($VM.State -eq "Running"){
        Stop-VM $VM -TurnOff
    }  
    Remove-VM $VM -Force
    Remove-Item "$RootPath\Containers\$ContainerName" -Recurse
}

Function Run-Container([String]$ContainerName, [String]$ImageName, [Long]$Memory=1024MB, [Int]$Processer=1, [String]$SwitchName, [String]$IPAddress, [String]$Subnet, [String]$Gateway, [String]$DNS = @()){
    New-Container -ContainerName "$ContainerName" -ImageName "$ImageName" -Memory $Memory -Processer $Processer -SwitchName "$SwitchName"
    Start-Container "$ContainerName"
    Wait-ContainerBoot "$ContainerName"
    Set-ContainerIPConfig -ContainerName "$ContainerName" -IPAddress $IPAddress -Subnet $Subnet -Gateway $Gateway -DNS $DNS
}

Function Wait-ContainerBoot([String]$ContainerName){
    $Flg = $False
    $TimeCount = 0
    Do
    { 
        If((Get-ContainerIPAddress $ContainerName) -ne ""){
            $Flg = $True 
        }Else{
            $TimeCount = $TimeCount + 1
            If($TimeCount -eq 180){
                 $Flg = $True 
            }
        }
        Start-Sleep -s 1
    }
    While ($Flg -eq $False)
}

Function Get-ContainerIPAddress([String]$ContainerName){
    $ManagementService = Get-WmiObject -Namespace root\virtualization\v2 -Class Msvm_VirtualSystemManagementService 
    $ComputerSystem = Get-WmiObject -Namespace root\virtualization\v2 -Class Msvm_ComputerSystem -Filter "ElementName = '$ContainerName'" 
    $SettingData = $ComputerSystem.GetRelated("Msvm_VirtualSystemSettingData") | Where-Object { $_.ElementName -eq "$ContainerName" }    
    $NetworkAdapters = $SettingData.GetRelated('Msvm_SyntheticEthernetPortSettingData') 
    $TargetNetworkAdapter = (Get-VMNetworkAdapter $ContainerName)[0] 
    $NetworkSettings = @()
    ForEach ($NetworkAdapter in $NetworkAdapters) {
        If ($NetworkAdapter.Address -eq $TargetNetworkAdapter.MacAddress) {
            $NetworkSettings = $NetworkSettings + $NetworkAdapter.GetRelated("Msvm_GuestNetworkAdapterConfiguration")
        }
    }  
    If($NetworkSettings.Length -eq 0){
        Return ""
    }Else{
        If( $NetworkSettings[0].IPAddresses.Length -eq 0){
            Return ""
        }Else{
            Return $NetworkSettings[0].IPAddresses[0]    
        }        
    }
}

Function Set-ContainerIPConfig([String]$ContainerName, [String]$IPAddress = @(), [String]$Subnet = @(), [String]$Gateway = @(), [String]$DNS = @()){
    $ManagementService = Get-WmiObject -Namespace root\virtualization\v2 -Class Msvm_VirtualSystemManagementService 
    $ComputerSystem = Get-WmiObject -Namespace root\virtualization\v2 -Class Msvm_ComputerSystem -Filter "ElementName = '$ContainerName'" 
    $SettingData = $ComputerSystem.GetRelated("Msvm_VirtualSystemSettingData") | Where-Object { $_.ElementName -eq "$ContainerName" }    
    $NetworkAdapters = $SettingData.GetRelated('Msvm_SyntheticEthernetPortSettingData') 
    $TargetNetworkAdapter = (Get-VMNetworkAdapter $ContainerName)[0] 
    $NetworkSettings = @()
    ForEach ($NetworkAdapter in $NetworkAdapters) {
        If ($NetworkAdapter.Address -eq $TargetNetworkAdapter.MacAddress) {
            $NetworkSettings = $NetworkSettings + $NetworkAdapter.GetRelated("Msvm_GuestNetworkAdapterConfiguration")
        }
    }
    $NetworkSettings[0].DHCPEnabled = $False
    $NetworkSettings[0].IPAddresses = $IPAddress
    $NetworkSettings[0].Subnets = $Subnet
    $NetworkSettings[0].DefaultGateways = $Gateway
    $NetworkSettings[0].DNSServers = $DNS
    $NetworkSettings[0].ProtocolIFType = 4096  
    $ManagementService.SetGuestNetworkAdapterConfiguration($ComputerSystem.Path, $NetworkSettings.GetText(1)) | Out-Null
    #Write-Host "IP was configured."
}

Function Get-TreeView() { 
    $Global:Tree = "`r`n"
    $RootFiles = Get-ChildItem $RootPath\Images *.vhdx | Get-VHD | Where {$_.ParentPath -eq ""}
    ForEach($File in $RootFiles){ 
        Set-TreeView $File.Path 0    
    }
    Write-Host $Global:Tree
}

Function Set-TreeView([String]$ParentFile, [Int]$Level) { 
    $Files = Get-ChildItem -Path $RootPath -Include "*.vhdx" -Recurse | Get-VHD | Where {$_.ParentPath -eq $ParentFile} 
    $BaseName = (Get-ChildItem $ParentFile).BaseName
    If($BaseName -match "[A-Fa-f0-9]{8}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{12}"){
        $Name = ($BaseName.Substring(0, $BaseName.Length - ($BaseName.Split("_")[$BaseName.Split("_").Length - 1].Length) - 1))
    }Else{
        $Name = $BaseName
    }
    If((Get-ChildItem $ParentFile).FullName.ToUpper().StartsWith(("$RootPath\Images").ToUpper())){
        $Name = "[" + $Name + "]"
    }Else{
        $Name = " " + $Name    
    }
    $Space = ""
    For ( $i = 0; $i -lt (($Level - 1) * 12); $i++ ){ 
        $Space += " "
    }
    If($Level -ne 0){
        $Space += " |"
        For ( $i = 0; $i -lt (10 - $Nam.Length) ; $i++ ){ 
            $Space += "-"
        }
    }
    $Global:Tree += $Space + $Name +  "`r`n`r`n"
    If($Files.Count -eq 0){  
    }Else{    
        ForEach($File in $Files){     
            Set-TreeView $File.Path ($Level + 1)    
        }
    }
}

Function Correct-ContainerImage() { 
    $Images = Get-ChildItem -Path "$RootPath\Images" -Include "*.vhdx" -Recurse
    ForEach($Image in $Images){ 
        $ParentPath = (Get-VHD $Image).ParentPath
        If ($ParentPath){  
            Set-VHD -Path $Image.FullName –ParentPath $ParentPath -IgnoreIdMismatch  
            Write-Host $Image.Name ">" (Get-ChildItem $ParentPath).Name
        }
    }
}

今回は主にコンテナイメージの操作について紹介します。

ルートフォルダの指定

はじめにInvokeVコンテナのルートフォルダを指定しておきます。

$RootPath = "D:\InvokeVContainer"

コードでは $RootPath = “InvokeVContainer” としていますが、スクリプトのセットアップ時に動的にの部分を書き換えるようにしています。

ルートパスの取得

Get-InvokeVContanerRoot

Function Get-InvokeVContanerRoot(){
Return $RootPath
}

ルートパスはGet-InvokeVContanerRootコマンドで確認できるようにしておきます。

コンテナイメージの操作

コンテナイメージのインポート

まずは親となるコンテナイメージの作成です。前回、「コンテナイメージの準備」としてOSインストール済みの仮想ディスクを作成しました。こちらは、通常のHyper-Vの仮想マシンを作成するのと同じ手順で、Windows Server 2016 のデスクトップエクスペリエンス(GUI付き)をインストールしておいたものです。この仮想ハードディスク(vhdxファイル)をコンテナイメージとしてインポート(コピー)することで、最初のコンテナイメージとします。

これまでの仮想マシンでは、同様の仮想マシンを作成する場合、構成や仮想ハードディスクを毎回コピーする必要がありましたが、コンテナイメージからコンテナを作成する場合や、コンテナから新しいコンテナイメージを作成する場合は、差分仮想ハードディスクを作成するだけなので、大幅な時間の短縮が実現されます。さらにコンテナの利点として、簡単にエクスポートして配布したりインポートしたりできるといった点を考慮すると、親となるコンテナイメージは唯一のものであることを明確にする必要があります。そのために、コンテナイメージのファイル名にGUIDを利用して名前付けを行い、他のファイルと重複しないようにしておきます。

(* GUID:時刻やMACアドレスなどから生成されるおよそ重複しない一意の文字列)

01 GUIを利用してイメージファイルの名前付け

Import-ContainerImage

Function Import-ContainerImage($FilePath, $ImageName) {
  $File = Get-ChildItem $FilePath
  If ($File.Extension -eq ".vhdx") {
    If ($File -match "[A-Fa-f0-9]{8}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{12}" -eq $True){
    Copy-Item $File -Destination ("$RootPath\Images\" + $File.Name)
}Else{
  If($ImageName){$ImageName = $File.BaseName}
  Copy-Item $File -Destination ("$RootPath\Images\" + $ImageName + "_" + [Guid]::NewGuid() + ".vhdx")
 }
}Else{
  Expand-Archive -Path $FilePath -DestinationPath $RootPath\Images -Force
}
}

・最初にインポートするファイルの拡張子が「*.vhdx」の場合の処理を行います。
・GUIDは”b8baa8dd-a936-4a4c-ab73-4169b8c8e38c”というような形式の文字列となるので、vhdxファイルをインポート(コピー)するときに、ファイル名にGUIDが含まれているか正規表現で判断して分岐しています。GUIDが含まれているファイル名の場合は、既存のコンテナイメージファイルと判断して、そのままイメージフォルダにコピーします。
・GUIDが含まれていない場合は、 [Guid]::NewGuid() でGUIDを取得してイメージ名と合わせてファイル名を生成し、コピーしてイメージファイルとします。
・最後の分岐は、インポートしようとしているファイルがvhdxファイル以外の場合です。この場合はコンテナイメージのエクスポートと関連していますが、zipファイルとしてエクスポートしたコンテナイメージを、イメージフォルダに解凍します。

コンテナイメージの詳細取得

Get-ContainerImage

Function Get-ContainerImage {
  Get-ChildItem $RootPath\Images *.vhdx | Where-Object {$_.Name -match "[A-Fa-f0-9]{8}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{12}"} | Select `
  @{Label="Name"; Expression={($_.BaseName.Substring(0, $_.BaseName.Length - ($_.BaseName.Split("_")[$_.BaseName.Split("_").Length - 1].Length) - 1))}}, `
  @{Label="Path"; Expression={($_.FullName)}},
  @{Label="Size(MB)"; Expression={($_.Length /1024/1024)}}, `
  @{Label="Created"; Expression={($_.LastWriteTime)}}, `
  @{Label="ParentPath"; Expression={(Get-VHD $_.FullName).ParentPath}} | Where-Object {$_.Name -ne ""}
}

・コンテナイメージの詳細を取得します。コンテナイメージは大元の親ファイル以下はすべてVHDX形式の差分仮想ハードディスクファイルとなっています。
・ルートフォルダ下のImageフォルダから、GUID付きのvhdxファイルの一覧を取得します。
・それぞれのファイルの属性から、イメージ名、ファイルパス、ファイルサイズ、作成時間を取得しています。
・さらに親ファイルがある場合は、Get-VHDコマンドでvhdxファイルの属性から親ファイルのパスを取得しています。これはHyper-Vマネージャーの「ディスクの検査」で確認できるものと同様です。

02 ディスクの検査から親ファイルの確認

Get-ContainerImageの実行結果

PS C:\> Get-ContainerImage

Name : Win2016
Path : D:\InvokeVContainer\Images\Win2016_b8baa8dd-a936-4a4c-ab73-4169b8c8e38c.vhdx
Size(MB) : 19076
Created : 2016/11/23 23:05:47
ParentPath :

Name : Win2016_Hyper-V
Path : D:\InvokeVContainer\Images\Win2016_Hyper-V_09f4f929-16c7-42d4-8b9b-5991d1e90c3c.vhdx
Size(MB) : 3091
Created : 2017/09/08 14:44:47
ParentPath : D:\InvokeVContainer\Images\Win2016_b8baa8dd-a936-4a4c-ab73-4169b8c8e38c.vhdx

コンテナイメージのエクスポート

コンテナイメージは、作成したコンテナからオリジナルとして作成することができます。コンテナ自体はコンテナイメージからすぐに作成できるメリットがあります。コンテナイメージをコピーして配布することで、簡単にコンテナのクローンを展開することが可能です。コンテナイメージのエクスポートでは、単一のコンテナイメージだけエクスポートする場合と、そのイメージと紐づいた親イメージすべてをエクスポートできるようにしています。他のサーバーに配布する場合、元々親イメージが展開されている場合は、単一のコンテナイメージだけの最小単位でコピーすることができます。新規に展開する場合は、親イメージのツリーごとすべてのコンテナイメージをコピーして展開する必要があります。

03 コンテナイメージのエクスポート

Export -ContainerImage

Function Export-ContainerImage([String]$ImageName, [String]$ExportPath, [Switch]$Tree){
  [Reflection.Assembly]::LoadWithPartialName( "System.IO.Compression.FileSystem" ) | Out-Null
  $Archive = [System.IO.Compression.ZipFile]::Open($ExportPath, "Update")
  $CompressionLevel = [System.IO.Compression.CompressionLevel]::Optimal
  $Image = Get-ChildItem (Get-ContainerImage | Where {$_.Name -eq "$ImageName"}).Path
[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($Archive, $Image.FullName, $Image.Name, $CompressionLevel) | Out-Null

If($Tree){
  $ParentFile = (Get-ContainerImage | Where {$_.Name -eq "$ImageName"}).ParentPath
  While ($ParentFile -ne ""){
    If($ParentFile -ne ""){
[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($Archive, $ParentFile, (Get-ChildItem $ParentFile).Name, $CompressionLevel) | Out-Null
$ParentFile = (Get-ContainerImage | Where {$_.Path -eq "$ParentFile"}).ParentPath
    }
   }
 }
 $Archive.Dispose()
}
vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
  • .Net Frameworkの“System.IO.Compression.FileSystem”クラスを利用してコンテナイメージをZipファイルとしてパッケージしています。まずはエクスポート対象のコンテナイメージファイルをZipファイルにまとめます。
  • 次に、$Treeスイッチがついていた場合は、コンテナイメージの親イメージファイルを、先ほど作成したGet-ContainerImageコマンドを利用して探し出します。
  • 親イメージファイルがあった場合は、Zipファイルに追加します。さらに、その親イメージファイルが無くなるまで(大元のイメージファイルにたどり着くまで)Zipファイルに追加することを繰り返します。
  • 最終的に、コンテナイメージのツリーファイルがZipファイルとしてひとまとまりとなって出力されます。Import-ContainerImageコマンドでは、このZipファイルをインポートすることで、解凍してルートのImagesフォルダに展開することになります。

コンテナイメージの作成

InvokeVコンテナでは、コンテナイメージはすべて読み取り専用となります。コンテナとして利用する場合は、必ずコンテナイメージの差分仮想ハードディスクファイルを作成してからコンテナとして起動します。コンテナにはアプリケーションをインストールしたり、様々な設定を行うことでオリジナルのコンテナとしてカスタマイズが可能です。カスタマイズされたコンテナはその状態をコンテナイメージとして保存しておくことで、コンテナのメリットである、“簡単に作成”、“簡単に削除”が可能となります。

04 コンテナからコンテナイメージを作成

New-ContainerImage

Function New-ContainerImage([String]$ContainerName, [String]$ImageName) {
  $ImageName = $ImageName + "_" + [Guid]::NewGuid()
  $VM = Get-VM $ContainerName
  $Disk = Get-VMHardDiskDrive $VM
  If($VM.State -eq "Off"){
    Get-VMHardDiskDrive $VM | Copy-Item -Destination "$RootPath\Images\$ImageName.vhdx"
  }Else{
    Checkpoint-VM $VM -SnapshotName "$ContainerName"
    Copy-Item (Get-VHD (Get-VMHardDiskDrive $VM).Path).ParentPath -Destination "$RootPath\Images\$ImageName.vhdx"
    Remove-VMSnapshot $VM –Name "$ContainerName"
  }
}
  • コンテナイメージの作成は、元となるコンテナ名と作成するコンテナイメージ名とを引数としてコマンドを実行します。
  • イメージ名にはGUIDを生成して追加します。
  • InvokeVコンテナでは、コンテナはHyper-V上の仮想マシンとして作成されています。コンテナイメージは仮想マシンのvhdxファイルをコピーすることで作成されます。従ってGet-VMコマンドとGet-VMHardDiskDriveコマンドで仮想マシンの仮想ハードディスクのパスを取得します。
  • もし、仮想マシンが停止状態であれば、そのままvhdxファイルをコピーします。
  • ・起動中の場合は、そのままコピーするには問題がありますので、一度仮想マシンのチェックポイントを作成します。このとき、仮想マシンのハードディスクはavhdxファイルに差し替えられています。
  • その間にavhdxファイルの親ファイルである元のvhdxファイルをコンテナイメージとしてコピーします。
  • コピー完了後にチェックポイントを削除して元に戻しておきます。

コンテナイメージの結合

ツリー状に展開したコンテナイメージの実態は、Hyper-Vの差分仮想ハードディスクです。差分仮想ハードディスクは、子ファイルと親ファイルを結合して一つのファイルにすることができるようになっています。InvokeVコンテナではこの特性を利用して、コンテナイメージを結合できるようにしています。

05 コンテナイメージの結合

Merge-ContainerImage

Function Merge-ContainerImage([String]$ImageName, [String]$NewImageName, [Switch]$Del) {
  $ImagePath = (Get-ContainerImage | Where {$_.Name -eq "$ImageName"}).Path
  $ParentPath = (Get-VHD "$ImagePath").ParentPath
  $NewImageID = $NewImageName + "_" + [Guid]::NewGuid()
  Copy-Item "$ParentPath" -Destination "$RootPath\Images\$NewImageID.vhdx"
  Copy-Item "$ImagePath" -Destination "$RootPath\Images\$NewImageID.avhdx"
  Set-VHD -Path "$RootPath\Images\$NewImageID.avhdx" –ParentPath "$RootPath\Images\$NewImageID.vhdx"
  Merge-VHD –Path "$RootPath\Images\$NewImageID.avhdx" –DestinationPath "$RootPath\Images\$NewImageID.vhdx"
  If($Del){ Remove-Item $ImagePath }
}
  • コンテナイメージの結合は、コンテナイメージ名、結合後のコンテナイメージ名、結合後の削除可否を引数としています。
  • イメージのパスは、作成したGet-ContainerImageコマンドで取得します。
  • 結合する親ファイルはGet-VHDコマンドで取得します。
  • 結合後のコンテナイメージ名とGUIDを組み合わせてファイル名を作成します。
  • コンテナイメージの結合は、対象のコンテナイメージファイルをavhdxという拡張子でコピーしています(vhdxの拡張子でも可)。結合した場合は親ファイルのパスとなるので、親ファイルは新しいコンテナイメージのパスでコピーを作成します。コピーを作成して結合することで、元々のイメージファイルを残したまま、結合したコンテナイメージを新しく作成できます。
  • Set-VHDコマンドでコピーしたコンテナイメージファイル(avhd)の親ファイルを設定します(コピーした状態ですでに親ファイルの属性は設定されているが念のため実行)。
  • Merge-VHDコマンドで差分ファイルである子ファイルと親ファイルを結合して一つのファイルとします。
  • 最後に、$Delスイッチが引数で渡されている場合は、結合前のコンテナイメージファイルを削除します。このスイッチを利用する場合、紐づいたコンテナや子イメージがある場合は削除してしまうと整合性が取れなくなってしまうので、注意が必要です。通常はDelスイッチを利用せず、不要であれば後述のコンテナイメージの削除コマンドRemove-ContainerImageコマンドの利用をお勧めします。

コンテナイメージの削除

コンテナイメージを削除する際、紐づいたコンテナや子コンテナイメージがある場合、注意が必要です。安易に削除してしまうと、紐づいたコンテナやコンテナイメージの親ファイルが無くなってしまうことになるので、差分ファイルとしての整合性が取れなくなり、コンテナとして起動することができなくなります。Remove-ContainerImageでは、削除対象のコンテナイメージのvhdxファイルを親ファイルとしている差分ファイルがある場合、参照しているコンテナイメージとコンテナも削除する必要があるので、オプションスイッチをつけて関連づいているアイテムすべてを削除できるようにしています。

Remove-ContainerImage

Function Remove-ContainerImage[String]$ImageName, [Switch]$Tree) {
  $ImagePath = (Get-ContainerImage | Where {$_.Name -eq "$ImageName"}).Path
  If($ImagePath) {
    $ChildItems = Get-ChildItem -Path "$RootPath" -Include "*.vhdx" -Recurse | Get-VHD | Where {$_.ParentPath -eq "$ImagePath"}
    If($ChildItems.Count -eq 0){
      Remove-Item $ImagePath -Recurse
    }Else{
      If($Tree){
        ForEach ($ChildItem in $ChildItems) {
         If($ChildItem.Path.ToString().ToUpper().StartsWith("$RootPath\Images".ToString().ToUpper())){
            $ChildImageName = (Get-ChildItem ($ChildItem.Path)).Name
            $ImageName = ($ChildImageName.Substring(0, $ChildImageName.Length - ($ChildImageName.Split("_")[$ChildImageName.Split("_").Length - 1].Length) - 1))
            Remove-ContainerImage $ImageName -Tree
          }Else{
            $ContainerName = (Get-ChildItem ($ChildItem.Path)).BaseName
            Remove-Container($ContainerName)
          }
        }
        Remove-Item $ImagePath -Recurse
      }Else{
        Write-Host """$ImageName"" is used other container images or containers." -ForegroundColor Red
      }
   }
 }Else{
    Write-Host """$ImageName"" is not container image name." -ForegroundColor Red
 }
}
  • 削除するコンテナイメージ名と、関連アイテムすべてを削除するための$Treeスイッチを引数とします。
  • イメージのパスは作成したGet-ContainerImageコマンドで取得します。
  • コンテナイメージ名で指定したイメージファイルがある場合は、ルートフォルダ以下すべてで、削除対象のイメージファイルが親ファイルとして参照している子コンテナイメージファイルと、コンテナファイルを検索します。
  • もし、参照している子ファイルが無い場合は、コンテナイメージファイルを削除します。
  • 参照ファイルがあった場合で、$Treeスイッチがついている場合は、ファイルパス名が”$RootPath\Images”で始まるときにコンテナイメージファイルと判断します。
  • コンテナイメージファイルのパスからファイル名を取得します。
  • ファイル名からGUIDを除いてコンテナイメージ名を取得します。
  • 子コンテナイメージがさらに親ファイルとして参照されているものを削除するために、$Treeスイッチを付けてRemove-ContainerImageを再度呼び出します。これにより関連づいた子コンテナイメージすべてが削除されます。
  • 削除対象のイメージファイルを削除します。
  • ファイルパス名が”$RootPath\Images”で始まらない場合は、コンテナファイルとして判別します。
  • コンテナ名を取得します。
  • Remove-Containerコマンドでコンテナを削除します。コンテナイメージの削除はvhdxファイルを削除するだけですが、コンテナの場合は、仮想マシンとしてのコンテナの削除とファイルの削除が必要となります。後述の「コンテナの削除」で詳しく紹介します。
  • もし$Treeスイッチがついておらず、削除対象のイメージファイルが親ファイルとして参照されている場合は、エラーを表示します。
  • もし、指定したコンテナイメージが見つからない場合はエラーを表示します。

以上、今回は一通りコンテナイメージ関連のコマンドを解説しました。
次回はコンテナ関連のコマンドを紹介します。

著書の紹介欄

Hyper-Vで本格的なサーバー仮想環境を構築。仮想環境を設定・操作できる!

できるPRO Windows Server 2016 Hyper-V

◇Hyper-Vのさまざまな機能がわかる ◇インストールからの操作手順を解説 ◇チェックポイントやレプリカも活用できる Windows Server 2016 Hyper-Vは、仮想化ソフトウェア基盤を提供する機能であり、クラウドの実現に不可欠のものです。 本書では、仮想化の基礎知識から、Hyper-Vでの仮想マシンや仮想スイッチの設定・操作、プライベートクラウドの構築、Azureとの連携などを解説します。

初めてのWindows Azure Pack本が発売

Windows Azure Pack プライベートクラウド構築ガイド

本書は、Windows Azure PackとHyper-Vを利用し、企業内IaaS(仮想マシン提供サービス)を構成するための、IT管理者に向けた手引書です。試用したサーバーは、最小限度の物理サーバーと仮想マシンで構成しています。Windows Azure Packに必要なコンポーネントのダウンロード、実際にプライベートクラウド構築する過程を、手順を追って解説しています。これからプライベートクラウドの構築を検討するうえで、作業負担の軽減に役立つ一冊です。

ブログの著者欄

樋口 勝一

GMOインターネット株式会社

1999年6月GMOインターネットに入社。Windows Serverをプラットフォームとしたサービス開発から運用・保守まで幅広く担当。講演登壇や出版、ネット記事連載などでマイクロソフト社と強い信頼関係を構築。2007年より「マイクロソフトMVPアワード」を受賞し、インターネットソリューションのスペシャリストとして活躍。

採用情報

関連記事