Proxmox上にAzure Localを構築する際の注意事項

# 本記事について
Azure LocalをMS-01にインストールしたProxmox VE上の仮想マシンとして構築し、クラスターをデプロイする際のポイントを記載しています。
Proxmoxの環境に無理やりAzure Localを構築するための試行錯誤の情報ですので、純粋にAzure Localの検証がおこないたいという目的であればHyper-V上に構築するのがおススメです。
Azure Localのバージョンは24H2を利用しています。

# ProxmoxのVM設定
VM設定のレベルでAzure Localを実行する際に考慮しておくべきものが3つあります。
1. ハードディスクのSSDエミュレート
2. ハードディスクのシリアル番号の重複排除
3. cpuのフラグ設定

## 1. ハードディスクのSSDエミュレート
Azure LocalではディスクがSSDであることが必須なため、ハードディスクの設定でSSDエミュレーションを有効化しておきます。
![SSD_emu.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481267/ec6efc20-8c51-4412-a1d8-9de0cd507097.jpeg)

## 2. ハードディスクのシリアル番号の重複排除
Azure Localのデプロイ中にクラスターの検証がおこなわれますが、この時にストレージスペースダイレクトのための検証でクラスターを構成するすべてのノードのディスクでシリアル番号が重複していないかのチェックがおこなわれます。
重複があるとデプロイが途中で失敗するため、事前にハードディスクのシリアル番号が重複しないようにしておきます。

ProxomoxのVM設定でハードディスクのシリアル番号を指定することもできると思われますが、
余分にディスクを付与してシリアル番号を確認した上で重複がないように各ノードで余剰なディスクを削りました。
ディスクのシリアル番号は```Get-PhysicalDisk```コマンドで確認できます。
![disk_serial.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481267/7f85f2cd-b9e8-4f4d-b586-9a26be257294.png)

## 3. cpuのフラグ設定
Nestedの仮想化機能を利用するためにcpuのフラグ設定をおこないます。
Proxmox (QEMU)ではcpuの機能を制限したり、パフォーマンスの最適化のためにcpuフラグの設定ができます。
以下のドキュメント等に、ある程度まとまった解説があるため、これらを参考に設定しています。

[Proxmox VE Administration Guide](https://pve.proxmox.com/pve-docs/pve-admin-guide.html#qm_virtual_machines_settings)
[archlinux | QEMU](https://wiki.archlinux.jp/index.php/QEMU)
[QEMU | Hyper-V Enlightenments](https://www.qemu.org/docs/master/system/i386/hyperv.html)

ただ、私のMS-01の環境では上記の情報だけではVMでの仮想化機能（Hyper-VやWSL等）の利用に問題が出ていました。
Hyper-V等を有効化した場合にVMが起動しない or 起動ループに陥る事象が発生します。
同様の事例はProxomoxでの仮想化機能（Hyper-V、WSL）の利用で調べると何件か情報が出てきまして、
cpuフラグに```-cpu host```を設定するなどの情報が出てくるのですが私の環境では解決しませんでした。

結論としては以下のサイトで提示されている```level=30```のオプションを付けることで解決しています。
[PROXMOX FORUMS | Windows Guest Boot Hangs after Enabling Hyper-V in Windows Guest on 13th Gen Intel CPU](https://forum.proxmox.com/threads/windows-guest-boot-hangs-after-enabling-hyper-v-in-windows-guest-on-13th-gen-intel-cpu.131950/)
[GitLab qemu-project | Intel 12th Gen CPU not working with QEMU Hyper-V nested virtualization](https://gitlab.com/qemu-project/qemu/-/issues/1031)

これはcpuidのlevelを定義する設定のようですが、level=30が具体的にどういった機能の定義に該当するのかは調べきれていません。
なお、levelではなく具体的なcpu（例：SandyBridge等）で指定することも試してみましたが、こちらの方法では解決できませんでした。

以上を踏まえて、最終的には以下のcpuフラグの設定を投入しています。
Hyper-V Enlightenmentのフラグでベストプラクティスな設定かどうかは十分に吟味できていませんが、特に問題なく動作はしています。
設定はGUI上ではおこなえないため、VMの設定ファイルに以下の行を追加して設定します。
- /etc/pve/qemu-server/**\<VM ID>**.conf
    ```
    cpu host
    args: -cpu host,level=30,hv-relaxed,hv-vapic,hv-spinlocks=0x1fff,hv-vpindex,hv-runtime,hv-time,hv-synic,hv-stimer,hv-tlbflush,hv-ipi,hv-frequencies,hv-evmcs,hv-stimer-direct,hv-emsr-bitmap,hv-xmm-input,hv-tlbflush-ext,hv-tlbflush-direct
    ```

# Azure Localのハードウェアの検証回避
Azure Localのデプロイ時には環境チェッカーが実行されます。
環境チェッカーではインターネットへの接続性やADの準備状況の確認などがチェックされるのですが、
チェック項目の1つにハードウェアコンポーネントのシステム要件のチェックがあります。

[MS Learn | 環境の準備状況を評価する](https://learn.microsoft.com/ja-jp/azure/azure-local/manage/use-environment-checker)

最近のAzure Localではメモリの要件にECCメモリであることが追加されるなど、おうち環境で検証するにはハードルになるチェック項目が出てきています。
また、ネットワークアダプターについてもNdisPhysicalMediumの値が"14"（Ethernet）であることのチェックがなされるのですが、このチェックがProxomoxでは問題になります。

Proxomox (virtio)ではネットワークアダプターの設定でNdisPhysicalMediumの値が"0"に設定されています。

- virtioのネットワークアダプター（NetKVM\2k25\amd64\netkvm.infの抜粋）
    ```
    [kvmnet6.ndi]
    Characteristics = 0x84 ; NCF_PHYSICAL  | NCF_HAS_UI
    BusType     = 5 ; PCI
    AddReg          = kvmnet6.Reg, Parameters
    CopyFiles       = kvmnet6.CopyFiles, netkvmp.CopyFiles
    *IfType         = 6
    *MediaType      = 0     ; NdisMedium802_3
    *PhysicalMediaType = 0      ; NdisPhysicalMediumUnspecified
    ```
    [NdisPhysicalMediumの値の参考](https://learn.microsoft.com/ja-jp/windows/win32/api/netioapi/ns-netioapi-mib_if_row2)

そのため、ECCメモリとNdisPhysicalMediumの値チェックを回避する必要があるのですが、どちらも正攻法で突破する方法が見出せないため、環境チェッカーのスクリプトを書き換えることで回避しています。
環境チェッカーのスクリプトの書き換えはもちろんサポートされるものではありませんので、あくまで環境チェッカーの内容の理解のための参考情報として捉えていただければと思います。
該当の環境チェッカーのスクリプトは以下のファイルです。
このスクリプト内で以下の箇所を修正して、ノードを再起動することでデプロイ時の環境チェックをパスすることができています。
```C:\Program Files\WindowsPowerShell\Modules\AzStackHci.EnvironmentChecker\AzStackHciHardware\AzStackHci.Hardware.Diagnostic.Helpers.psm1```

- ECCメモリ
    <details><summary>function Test-MemoryProperties</summary>

    ```diff_powershell
    function Test-MemoryProperties
    {
        <#
        .SYNOPSIS
            Test Memory
        .DESCRIPTION
            Test Memory
        #>
        [CmdletBinding()]
        param (
            [System.Management.Automation.Runspaces.PSSession[]]
            $PsSession
        )
        try
        {
            $sb = {
                $cimParams = @{
                    ClassName = 'Win32_PhysicalMemory'
                    Property  = '*'
                }
                $cimData = @(Get-CimInstance @cimParams)
                return (New-Object PsObject -Property @{
                    ComputerName = $ENV:ComputerName
                    cimData = $cimData
                })
            }
            $remoteOutput = if ($PsSession)
            {
                Invoke-Command -Session $PsSession -ScriptBlock $sb
            }
            else
            {
                Invoke-Command -ScriptBlock $sb
            }

            $cimTest = Test-CimData -Data $remoteOutput -ClassName PhysicalMemory -Severity WARNING
            $cimData = $remoteOutput.cimData

            # Add EEC property
            $eccSb = {
                if ($this.TotalWidth -gt $this.DataWidth)
                {
                    return $true
                }
                else
                {
    -               return $false
    +               return $true
                }
            }
            $cimData | Add-Member -MemberType ScriptProperty -Name ECC -Value $eccSb -Force

            $PropertyResult = @()
            $PropertySyncResult = @()
            $matchProperty = @(
    -           'ConfiguredClockSpeed'
    +           #'ConfiguredClockSpeed'
    -           'ConfiguredVoltage'
    +           #'ConfiguredVoltage'
    -           'MaxVoltage'
    +           #'MaxVoltage'
                'MemoryType'
    -           'SMBIOSMemoryType'
    +           #'SMBIOSMemoryType'
    -           'Speed'
    +           #'Speed'
    -           'TotalWidth'
    +           #'TotalWidth'
    -           'TypeDetail'
    +           #'TypeDetail'
            )

            $warningdesiredPropertyValue = @{
    -           DataWidth  = @{ value = 64; hint = '64-bit' } # x64
    +           #DataWidth  = @{ value = 64; hint = '64-bit' } # x64
                FormFactor = @{ value = 8; hint = 'DIMM' } # DIMM
            }

            $criticaldesiredPropertyValue = @{
                ECC  = $true # Error Correction Code (ECC) memory
            }
            Log-CimData -cimData $cimData -Properties $warningdesiredPropertyValue,$criticaldesiredPropertyValue,$matchProperty
            # Check property sync for nodes individually
            $SystemNames = $cimData.CimSystemProperties.ServerName | Sort-Object | Get-Unique
            foreach ($systemName in $SystemNames)
            {
                $sData = $CimData | Where-Object { $_.CimSystemProperties.ServerName -eq $systemName }
                $instanceIdStr = 'Write-Output "Machine: $($Instance.CimSystemProperties.ServerName), Class: $ClassName, Instance: $($instance.DeviceLocator), Tag: $($instance.Tag)"'
                $PropertyResult += Test-DesiredProperty -CimData $sData -desiredPropertyValue $warningdesiredPropertyValue -InstanceIdStr $InstanceIdStr -ValidatorName Hardware -Severity Warning
                $PropertyResult += Test-DesiredProperty -CimData $sData -desiredPropertyValue $criticaldesiredPropertyValue -InstanceIdStr $InstanceIdStr -ValidatorName Hardware -Severity CRITICAL
                $PropertySyncResult += Test-PropertySync -CimData $sData -MatchProperty $matchProperty -ValidatorName Hardware -Severity Warning
            }
            # Check property sync for all nodes as well
            $PropertySyncResult += Test-PropertySync -CimData $cimData -MatchProperty $matchProperty -ValidatorName Hardware -Severity Warning
            return @($PropertyResult + $PropertySyncResult + $cimTest)
        }
        catch
        {
            throw $_
        }
    }
    ```
    </details>
- ネットワークアダプターのNdisPhysicalMedium
    <details><summary>function Test-NetAdapter</summary>

    ```diff_powershell
    function Test-NetAdapter
    {
        <#
        .SYNOPSIS
            Test Network Adapter
        .DESCRIPTION
            Test Network Adapter
        #>
        [CmdletBinding()]
        param (
            [System.Management.Automation.Runspaces.PSSession[]]
            $PsSession
        )
        try
        {
            $sb = {
    -            $cimData = @(Get-NetAdapter -Physical | Where-Object { $_.NdisMedium -eq 0 -and $_.Status -eq 'Up' -and $_.NdisPhysicalMedium -eq 14 -and $_.PnPDeviceID -notlike 'USB\*'})
    +            $cimData = @(Get-NetAdapter -Physical | Where-Object { $_.NdisMedium -eq 0 -and $_.Status -eq 'Up' -and $_.NdisPhysicalMedium -eq 0 -and $_.PnPDeviceID -notlike 'USB\*'})
                return (New-Object PsObject -Property @{
                    ComputerName = $ENV:ComputerName
                    cimData = $cimData
                })
            }
            $remoteOutput = if ($PsSession)
            {
                Invoke-Command -Session $PsSession -ScriptBlock $sb
            }
            else
            {
                Invoke-Command -ScriptBlock $sb
            }

            $cimTest = Test-CimData -Data $remoteOutput -ClassName NetAdapter -Severity CRITICAL -Detail $lhwTxt.NICSupportExplanation
            $cimData = $remoteOutput.cimData

            $PropertyResult = @()
            $PropertySyncResult = @()
            $GroupResult = @()
            $CountResult = @()

            # Blocking properties
            $criticalMatchProperty = @(
                'DriverDate'
                'DriverDescription'
                'DriverMajorNdisVersion'
                'DriverMinorNdisVersion'
                'DriverProvider'
                'DriverVersionString'
                'MajorDriverVersion'
                'MinorDriverVersion'
            )

            # non-block warning properties
            $warningMatchProperty = @(
                'ActiveMaximumTransmissionUnit'
                'ReceiveLinkSpeed'
                'Speed'
                'TransmitLinkSpeed'
                'VlanID'
                'MtuSize'
            )

            $desiredPropertyValue = @{
                AdminLocked                                      = $false
                ConnectorPresent                                 = $true
                EndpointInterface                                = $false
                ErrorDescription                                 = $null
                FullDuplex                                       = $true
                HardwareInterface                                = $true
                Hidden                                           = $false
                IMFilter                                         = $false
                InterfaceAdminStatus                             = @{ value = 1; hint = 'Up' } # Up
                InterfaceOperationalStatus                       = @{ value = 1; hint = 'Up' } # Up
                iSCSIInterface                                   = $false
                LastErrorCode                                    = $null
                MediaConnectState                                = @{ value = 1; hint = 'Connected' } # Connected
                MediaDuplexState                                 = 2
                NdisMedium                                       = @{ value = 0; hint = '802.3' } # 802.3
    -            NdisPhysicalMedium                               = @{ value = 14; hint = '802.3' } # 802.3
    +            NdisPhysicalMedium                               = @{ value = 0; hint = '802.3' } # 802.3
                OperationalStatusDownDefaultPortNotAuthenticated = $false
                OperationalStatusDownInterfacePaused             = $false
                OperationalStatusDownLowPowerState               = $false
                OperationalStatusDownMediaDisconnected           = $false
                #PromiscuousMode                                  = $false
                State                                            = @{ value = 2; hint = 'Started' } # 802.3  # Started
                #Status                                           = 'Up'
                Virtual                                          = $false
            }

            $groupProperty = @(
                'DriverDescription'
            )

            Log-CimData -cimData $cimData -Properties $desiredPropertyValue,$warningMatchProperty,$criticalMatchProperty

            $minimum = 1
            $instanceIdStr = 'Write-Output "Machine: $($instance.SystemName), ClassName: $ClassName, Instance: $($instance.Name), Description: $($instance.InterfaceDescription), Address: $($instance.PermanentAddress)"'
            # Check property sync for nodes individually
            $SystemNames = $cimData.CimSystemProperties.ServerName | Sort-Object | Get-Unique
            foreach ($systemName in $SystemNames)
            {
                $sData = $CimData | Where-Object { $_.CimSystemProperties.ServerName -eq $systemName }
                Log-Info -Message ($lhwTxt.NicCount -f $systemName, $sData.Count)
                # Make sure each system has the requisite number of Network Adapters
                $PropertyResult += Test-DesiredProperty -CimData $sData -desiredPropertyValue $desiredPropertyValue -InstanceIdStr $InstanceIdStr -ValidatorName Hardware -Severity Critical
                $CountResult += Test-Count -CimData $sData -minimum $minimum -ValidatorName 'Hardware' -Severity Critical
            }

            # Check property sync for all nodes as well
            $GroupResult += Test-GroupProperty -CimData $cimData -GroupProperty $groupProperty -MatchProperty $warningMatchProperty -ValidatorName Hardware -Severity Warning
            $GroupResult += Test-GroupProperty -CimData $cimData -GroupProperty $groupProperty -MatchProperty $criticalMatchProperty -ValidatorName Hardware -Severity Critical
            $InstanceCount += Test-InstanceCount -CimData $cimData -Severity Critical -ValidatorName 'Hardware'
            $InstanceCountByGroup += Test-InstanceCountByGroup -CimData $cimData -ValidatorName 'Hardware' -GroupProperty $groupProperty -Severity Critical
            # Finally, the all properties from the $matchProperty array have to be compared for all instances across all nodes.
            return @($PropertyResult + $GroupResult + $CountResult + $InstanceCountByGroup + $InstanceCount + $cimTest)
        }
        catch
        {
            throw $_
        }
    }
    ```
    </details>

# まとめ
Hyper-V上で構築する上では大きな問題は無かったAzure Localですが、Proxomoxでの構築にはいくつかつまずきポイントがありました。
cpuidのところは調べきれていないところがあったり、環境チェッカーは強引な回避で突破していたりと不十分なところはありますが今後の参考のために記録しておきます。