Azureコラム 第10回「Azure Virtual Desktop 運用自動化 Vol.1」

Microsoft

2024.09.04

     

早いものでAzureコラム 第7回「Azure Virtual Desktop システム構成 Vol.3」から3年、細々と行ってきた運用に満を持して自動化のメスを入れることにしました。
Vol.1は月次更新運用の自動化をお届けします。

条件・制約

  • !!!!! スクリプトの実行は自己責任でお願いします !!!!!
  • PowerShellモジュールは2024年8月20日時点で最新化して動作確認を行っています。

自動化の効果

最後まで読んでいただいて「なんだその程度か」も申し訳ありませんので、冒頭に本稿の自動化の効果を記載しておきます。

  • 作業量:約30画面の操作 → 5つのスクリプト実行
  • 作業時間:約30分 → 5分 ※デプロイ待ち時間除く
  • 所感:既定値の入力をしなくて良くなるのが精神衛生的に効果あり!

月次更新フロー

月次更新は更新プログラム公開に合わせて行っており、毎月下記のフローを回しています。

Step フロー 補足
1 マスターVMのデプロイ 前回更新時に取得したスナップショットからマスターVMをデプロイ
2 マスターVMの更新① 1. 更新プログラムの適用
2. (あれば)FSLogixの更新
3. ディスククリーンアップ
※ 本稿の自動化対象外
3 スナップショット取得 マスターVMのスナップショット取得
4 マスターVMの更新② sysprep実行 ※ 本稿の自動化対象外
5 イメージのキャプチャ マスターVMをギャラリーイメージにキャプチャ
6 テストAVDのデプロイ ギャラリーイメージからテストAVDをデプロイ
7 テスト実施 テストAVDにて接続テストを実施 ※ 本稿の自動化対象外
8 本番AVDのデプロイ ギャラリーイメージから本番AVDをデプロイ
9 リソースクリーンアップ 古いAVDなど不要なリソースをクリーンアップ

それでは次セクションから各ステップの運用自動化スクリプトをご紹介していきます。

Step1:マスターVMのデプロイ

Step1ではスナップショットからマスターVMをデプロイします。

パラメーター:

パラメーター
$resourceGroupName 展開先リソースグループ名 avd-master
$location 展開先リージョン japaneast
$snapshotName デプロイ元のスナップショット名 AVDMaster_OSDisk_1_19000101
$storageType ディスクSKU Standard_LRS
$diskSize ディスクサイズ(GB) 128
$subnetId 展開先のサブネットID(リソースID形式) /subscriptions/~/subnets/master
$vmName VM名 AVDMaster
$vmSize VMサイズ Standard_B2ls_v2

コマンド:

$ErrorActionPreference = "Stop"

$resourceGroupName = "avd-master"
$location = "japaneast"
$snapshotName = "AVDMaster_OSDisk_1_19000101"
$storageType="Standard_LRS"
$diskSize = 128
$subnetId = "/subscriptions/01234567-8901-2345-6789-012345678901/resourceGroups/avd/providers/Microsoft.Network/virtualNetworks/avd-nw/subnets/master"
$vmName = "AVDMaster"
$vmSize = "Standard_B2ls_v2"

# Connect to Azure
if (-not (Get-AzContext)) {Connect-AzAccount}

try {
    # Create a new disk from the snapshot
    Write-Output "Get the snapshot: ${snapshotName}"
    $snapshot = Get-AzSnapshot -ResourceGroupName $resourceGroupName -SnapshotName $snapshotName

    # Create a new disk
    $osDiskName = "${vmName}_OSDisk_1"
    Write-Output "Deploy the disk: ${osDiskName}"
    $diskConfig = New-AzDiskConfig  `
        -SkuName $storageType `
        -Location $location `
        -CreateOption Copy  `
        -SourceResourceId $snapshot.Id `
        -DiskSizeGB $diskSize
    $osDisk = New-AzDisk `
        -Disk $diskConfig `
        -ResourceGroupName $resourceGroupName `
        -DiskName $osDiskName

    # Create a new NIC
    $nicName = "${vmName}_NetworkInterface"
    Write-Output "Deploy the nic: ${nicName}"
    $nic = New-AzNetworkInterface `
        -Name $nicName `
    -ResourceGroupName $resourceGroupName `
    -Location $location `
    -SubnetId $subnetId

    # Create a new VM
    Write-Output "Deploy the vm: ${vmName}"
    $vm = New-AzVMConfig -VMName $vmName -VMSize $vmSize
    $vm = Set-AzVMOSDisk -VM $vm -ManagedDiskId $osDisk.Id -CreateOption Attach -DeleteOption Delete -Windows
    $vm = Add-AzVMNetworkInterface -VM $vm -Id $nic.Id -DeleteOption Delete
    $vm = Set-AzVMBootDiagnostic -VM $vm -Disable
    New-AzVM `
        -ResourceGroupName $resourceGroupName `
        -Location $location `
        -VM $vm `
        -DisableBginfoExtension

    Write-Output "Successfully deployed the vm: ${vmName}"
}
catch {
    Write-Error "Failed to deploy the vm: $_"
}

補足:

  • ディスクは {仮想マシン名}_OSDisk_1 という規則で命名
  • ネットワークインターフェースは {仮想マシン名}_NetworkInterface という規則で命名
  • ディスクとネットワークインターフェースはVMで強制削除
  • ブート診断無効化
  • BGInfo無効化

Step2:マスターVMの更新①

Step2はマスターVMにリモートデスクトップして手動で実施します。

  • 1.更新プログラムの適用
  • 2.(あれば)FSLogixの更新
  • 3.ディスククリーンアップ

次のステップでスナップショットを取得しますので、このステップではまだsysprepは実行しません

Step3:スナップショット取得

Step3ではマスターVMのスナップショットを取得します。

パラメーター:

パラメーター
$resourceGroupName スナップショット取得先リソースグループ名 avd-master
$vmName VM名 AVDMaster
$location スナップショット取得先リージョン japaneast

コマンド:

$ErrorActionPreference = "Stop"

$resourceGroupName = "avd-master"
$vmName = "AVDMaster"
$location = "japaneast"

# Connect to Azure
if (-not (Get-AzContext)) {Connect-AzAccount}

try {
    # Get the VM
    Write-Output "Get the vm: ${vmName}"
    $vm = Get-AzVM -ResourceGroupName $resourceGroupName -Name $vmName

    # Create a snapshot
    $osDiskId = $vm.StorageProfile.OsDisk.ManagedDisk.Id
    $osDiskName = $vm.StorageProfile.OsDisk.Name
    $date = Get-Date -Format "yyyyMMdd"
    $snapshotName = "${osDiskName}_${date}"
    Write-Output "Create the snapshot: ${snapshotName}"
    $snapshot =  New-AzSnapshotConfig `
        -SourceUri $osDiskId `
        -Location $location `
        -CreateOption copy
    New-AzSnapshot `
        -Snapshot $snapshot `
        -SnapshotName $snapshotName `
        -ResourceGroupName $resourceGroupName

    Write-Output "Successfully created the snapshot: ${snapshotName}"
}
catch {
    Write-Error "Failed to create the snapshot: $_"
}

補足:

  • スナップショットは {OSディスク名}_{yyyyMMdd} という規則で命名

Step4:マスターVMの更新②

再度マスターVMにリモートデスクトップしてsysprepを実行します。

Step5:イメージのキャプチャ

Step5ではマスターVMをギャラリーのイメージにキャプチャします。

パラメーター:

パラメーター
$vmResourceGroupName VMのリソースグループ名 avd-master
$vmName VM名 AVDMaster
$galleryName イメージ格納先のギャラリー名 AVDImage
$imageDefinition イメージ定義名 AVDMaster
$resourceGroupName ギャラリーのリソースグループ名 avd
$location リソースグループのリージョン japaneast
$targetRegion イメージ格納先のリージョン @{Name = 'japaneast'} ※配列
$replicaCount イメージのレプリカ数 1

コマンド:

$ErrorActionPreference = "Stop"

$vmResourceGroupName = "avd-master"
$vmName = "AVDMaster"
$galleryName = "AVDImage"
$imageDefinition="AVDMaster"
$resourceGroupName = "avd"
$location = "japaneast"
$targetRegion = @{Name = 'japaneast'}
$replicaCount = 1

# Connect to Azure
if (-not (Get-AzContext)) {Connect-AzAccount}

try {
    # Stop the VM and generalize it
    Write-Output "Stop the vm and generalize it: ${vmName}"
    $vm = Get-AzVM -ResourceGroupName $vmResourceGroupName -Name $vmName
    Stop-AzVM -ResourceGroupName $vmResourceGroupName -Name $vmName -Force
    Set-AzVM -ResourceGroupName $vmResourceGroupName -Name $vmName -Generalized

    # Create a new image version
    $imageVersion = (Get-Date).ToString("1.yyyy.MM")
    Write-Output "Create a new image version: ${imageDefinition}/${imageVersion}"
    New-AzGalleryImageVersion `
        -GalleryName $galleryName `
        -GalleryImageDefinitionName $imageDefinition `
        -GalleryImageVersionName $imageVersion `
        -ResourceGroupName $resourceGroupName `
        -Location $location `
        -TargetRegion $targetRegion `
        -SourceImageVMId $vm.Id.ToString() `
        -ReplicaCount $replicaCount

    Write-Output "Successfully captured vm: ${vmName}"    
}
catch {
    Write-Error "Failed to capture the vm: $_"
}

補足:

  • イメージバージョンは 1.{yyyy}.{MM} という規則で命名

Step6~8:テストAVDのデプロイ~本番AVDのデプロイ

テストAVDと本番AVDのデプロイは中身が同じですのでまとめて。

パラメーター:

パラメーター
$resourceGroupName ARMテンプレートのリソースグループ名 avd-prod
$hostpoolResourceGroup AVDホストプールのリソースグループ名 avd-prod
$hostPoolName AVDホストプール名 AVDProd
$vmResourceGroup VM展開先のリソースグループ名 avd-prod
$vmLocation VM展開先のリージョン japaneast
$vmImageType イメージの種類 CustomImage
$vmCustomImageSourceId イメージのリソースID /subscriptions/~/images/AVDMaster/versions/1.0.0
$vmNamePrefix VM名のプレフィックス avdprode
$vmInitialNumber VM名連番の最初の番号 0
$vmNumberOfInstances VM数 1
$availabilityOption 可用性ゾーン設定 NONE ※ゾーン冗長しない
$vmSize VMサイズ Standard_B2ls_v2
$vmDiskType ディスクSKU StandardSSD_LRS
$virtualNetworkResourceGroupName 仮想ネットワークのリソースグループ名 avd
$existingVnetName 仮想ネットワーク名 avd-nw
$existingSubnetName サブネット名 prod
$administratorAccountUsername ドメイン参加用の管理者名 avdadmin@012345678901.onmicrosoft.com
$administratorAccountPassword ドメイン参加用の管理者パスワード 012345678901
$vmAdministratorAccountUsername VMのローカル管理者名 avdadmin
$vmAdministratorAccountPassword VMのローカル管理者パスワード 012345678901
$domain 参加先ドメイン名 012345678901.onmicrosoft.com
$shutdownStatus 自動シャットダウン設定 Enabled ※自動シャットダウン有効化
$shutdownTime 自動シャットダウン時間 20:00
$shutdownTimezone 自動シャットダウン時間タイムゾーン Tokyo Standard Time

コマンド:

$ErrorActionPreference = "Stop"

$resourceGroupName = "avd-prod"
$hostpoolResourceGroup = "avd-prod"
$hostPoolName = "AVDProd"
$vmResourceGroup = "avd-prod"
$vmLocation = "japaneast"
$vmImageType = "CustomImage"
$vmCustomImageSourceId = "/subscriptions/01234567-8901-2345-6789-012345678901/resourceGroups/avd/providers/Microsoft.Compute/galleries/AVDImage/images/AVDMaster/versions/1.0.0"
$vmNamePrefix = "AVDProd"
$vmInitialNumber = 0
$vmNumberOfInstances = 1
$availabilityOption = "None"
$vmSize = "Standard_B2ls_v2"
$vmDiskType = "StandardSSD_LRS"
$virtualNetworkResourceGroupName = "avd"
$existingVnetName = "avd-nw"
$existingSubnetName = "prod"
$administratorAccountUsername = "avdadmin@012345678901.onmicrosoft.com"
$administratorAccountPassword = "012345678901"
$vmAdministratorAccountUsername = "avdadmin"
$vmAdministratorAccountPassword = "012345678901"
$domain = "012345678901.onmicrosoft.com"
$shutdownStatus = "Enabled"
$shutdownTime = "20:00"
$shutdownTimezone = "Tokyo Standard Time"

# Connect to Azure
if (-not (Get-AzContext)) {Connect-AzAccount}

try {
    # Get the host pool registration token
    Write-Output "Get the host pool registration token: ${hostPoolName}"
    $expirationTime = (Get-Date).ToString("yyyy-MM-ddT23:59:59Z")
    $hostPool = New-AzWvdRegistrationInfo `
        -ResourceGroupName $hostpoolResourceGroup `
        -HostPoolName $hostPoolName `
        -ExpirationTime $expirationTime
    $secureToken = ConvertTo-SecureString -String $hostPool.Token -AsPlainText -Force
    $administratorAccountPassword = ConvertTo-SecureString -String $administratorAccountPassword -AsPlainText -Force
    $vmAdministratorAccountPassword = ConvertTo-SecureString -String $vmAdministratorAccountPassword -AsPlainText -Force

    # Deploy the host
    $deploymentId = New-Guid
    Write-Output "Add host to hostpool: ${hostPoolName}"
    $deployment = New-AzResourceGroupDeployment `
        -ResourceGroupName $resourceGroupName `
        -TemplateFile "../Common/Add_Host_template.json" `
        -TemplateParameterFile "../Common/Add_Host_parameters.json" `
        -hostpoolResourceGroup $hostpoolResourceGroup `
        -hostPoolName $hostPoolName `
        -hostpoolToken $secureToken `
        -vmResourceGroup $vmResourceGroup `
        -vmLocation $vmLocation `
        -vmImageType $vmImageType `
        -vmCustomImageSourceId $vmCustomImageSourceId `
        -vmNamePrefix $vmNamePrefix `
        -vmInitialNumber $vmInitialNumber `
        -vmNumberOfInstances $vmNumberOfInstances `
        -availabilityOption $availabilityOption `
        -vmSize $vmSize `
        -vmDiskType $vmDiskType `
        -virtualNetworkResourceGroupName $virtualNetworkResourceGroupName `
        -existingVnetName $existingVnetName `
        -existingSubnetName $existingSubnetName `
        -administratorAccountUsername $administratorAccountUsername `
        -administratorAccountPassword $administratorAccountPassword `
        -vmAdministratorAccountUsername $vmAdministratorAccountUsername `
        -vmAdministratorAccountPassword $vmAdministratorAccountPassword `
        -domain $domain `
        -deploymentId $deploymentId
    Write-Output "Successfully added host to hostpool: ${hostPoolName}"

    # Set auto-shutdown for the VMs
    $vms = $deployment.Outputs.rdshVmNamesObject.Value | ForEach-Object {$_.Value.ToString() | ConvertFrom-Json}
    foreach ($vm in $vms) {
        Write-Output "Setting auto-shutdown for VM: $($vm.name)"
        New-AzResourceGroupDeployment `
            -ResourceGroupName $resourceGroupName `
            -TemplateFile "../Common/Set_AutoShutdown_template.json" `
            -TemplateParameterFile "../Common/Set_AutoShutdown_parameters.json" `
            -vmName $vm.name `
            -shutdownStatus $shutdownStatus `
            -shutdownTime $shutdownTime `
            -shutdownTimezone $shutdownTimezone -ErrorAction Stop
    }
    Write-Output "Successfully set auto-shutdown for VMs"

} catch {
    Write-Error "Failed to deploy the host: $_"
}

補足:

  • コマンドの中で使用しているARMテンプレートは後述のGitHubから取得してください。
  • すみません、可用性ゾーン展開などのカスタマイズパターンはテストできていません……。

Step9:リソースクリーンアップ

ご参考程度に、本稿では下記リソースのクリーンアップを行っています。

  • マスター
    • VM
    • ディスク
    • ネットワークインターフェース
    • 最も古い世代のスナップショット
    • 最も古い世代のギャラリーイメージ
  • 古いAVD
    • セッションホスト
    • VM
    • ディスク
    • ネットワークインターフェース

コマンド:

$masterResourceGroupName = "avd-master"
$masterVMName = "AVDMaster"
$imageResourceGroupName = "avd"
$galleryName = "AVDImage"
$imageDefinitionName = "AVDMaster"
$testResourceGroupName = "avd-test"
$testHostPoolName = "AVDTest"
$testAVDName = "avdtest-0.012345678901.onmicrosoft.com"
$testVMName = "avdtest-0"
$prodResourceGroupName = "avd-prod"
$prodHostPoolName = "AVDProd"
$prodAVDNameSuffix = "avdprode"
$prodVMNameSuffix = " avdprode"

# Connect to Azure
if (-not (Get-AzContext)) {Connect-AzAccount}

# Master
# VM
Get-AzVM -ResourceGroupName $masterResourceGroupName -Name $masterVMName | Remove-AzVm -ForceDeletion $true
# Snapshot
Get-AzSnapshot -ResourceGroupName $masterResourceGroupName | `
    Where-Object { $_.Name -like "${masterVMName}*" } | `
    Sort-Object { $_.TimeCreated } | `
    Select-Object -First 1 | `
    Remove-AzSnapshot
# Image
Get-AzGalleryImageVersion -ResourceGroupName $imageResourceGroupName -GalleryName $galleryName -GalleryImageDefinitionName $imageDefinitionName | `
    Sort-Object { $_.PublishingProfile.PublishedDate } | `
    Select-Object -First 1 | `
    Remove-AzGalleryImageVersion

# Test
# SessionHost
Remove-AzWvdSessionHost -ResourceGroupName $testResourceGroupName -HostPoolName $testHostPoolName -Name $testAVDName
# VM
$vmConfig = Get-AzVM -ResourceGroupName $testResourceGroupName -Name $testVMName
$vmConfig.StorageProfile.OsDisk.DeleteOption = 'Delete'
$vmConfig.StorageProfile.DataDisks | ForEach-Object { $_.DeleteOption = 'Delete' }
$vmConfig.NetworkProfile.NetworkInterfaces | ForEach-Object { $_.DeleteOption = 'Delete' }
$vmConfig | Update-AzVM
$vmConfig | Remove-AzVm -ForceDeletion $true

#Prod
# SessionHost
Get-AzWvdSessionHost -ResourceGroupName $prodResourceGroupName -HostPoolName $prodHostPoolName | `
    Where-Object { $_.Name -like "*${prodAVDNameSuffix}*" } | `
    Remove-AzWvdSessionHost
# VM
Get-AzVM -ResourceGroupName $prodResourceGroupName | `
    Where-Object { $_.Name -like "${prodVMNameSuffix}*" } | `
    ForEach-Object{
        $_.StorageProfile.OsDisk.DeleteOption = 'Delete'
        $_.StorageProfile.DataDisks | ForEach-Object { $_.DeleteOption = 'Delete' }
        $_.NetworkProfile.NetworkInterfaces | ForEach-Object { $_.DeleteOption = 'Delete' }
        $_ | Update-AzVM
        $_ | Remove-AzVm -ForceDeletion $true
    }

別添

ご紹介したスクリプトは下記のGitHubにて公開しておりますのでARMテンプレートと合わせてご参照ください。
https://github.com/mstechcenter/avd-operation

おわりに

Azure Virtual Desktop 運用自動化いかがでしたでしょうか?
まだパラメーターを修正してコマンドを叩くという手動の手順は残りますが、自動化によりマウス入力や画面遷移がなくなっただけでも結構な労力の削減になるということを実感しました。
Vol.2ではさらなる自動化を目指すべく、Azure DevOpsもしくはGitHubとの連携を進めていきたいと思います。

  • 文中の商品名、会社名、団体名は、一般に各社の商標または登録商標です。

Azureコラム 第10回「Azure Virtual Desktop 運用自動化 Vol.1」